@chrrxs/robloxstudio-mcp 3.0.4 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1313 -875
- package/package.json +2 -2
- package/studio-plugin/MCPPlugin.rbxmx +559 -353
|
@@ -16,6 +16,7 @@ local cleanupEditBridgeArtifacts = _EvalBridges.cleanupEditBridgeArtifacts
|
|
|
16
16
|
local ensureRuntimeBridgeInstalled = _EvalBridges.ensureRuntimeBridgeInstalled
|
|
17
17
|
local RuntimeLogBuffer = TS.import(script, script, "modules", "RuntimeLogBuffer")
|
|
18
18
|
local StopPlayMonitor = TS.import(script, script, "modules", "StopPlayMonitor")
|
|
19
|
+
local StudioEventStream = TS.import(script, script, "modules", "StudioEventStream")
|
|
19
20
|
local BreakpointHandlers = TS.import(script, script, "modules", "handlers", "BreakpointHandlers")
|
|
20
21
|
local RenderMonitor = TS.import(script, script, "modules", "RenderMonitor")
|
|
21
22
|
-- Track render-loop liveness so input/screenshot tools can report "window
|
|
@@ -32,8 +33,9 @@ RuntimeLogBuffer.install()
|
|
|
32
33
|
StopPlayMonitor.init(plugin)
|
|
33
34
|
BreakpointHandlers.init(plugin)
|
|
34
35
|
ServerUrlSettings.init(plugin)
|
|
36
|
+
local startupRole = ClientBroker.forkRole()
|
|
35
37
|
local function applyRememberedServerUrl()
|
|
36
|
-
if
|
|
38
|
+
if startupRole == "client" then
|
|
37
39
|
return nil
|
|
38
40
|
end
|
|
39
41
|
local rememberedServerUrl = ServerUrlSettings.readServerUrl()
|
|
@@ -49,6 +51,15 @@ local function applyRememberedServerUrl()
|
|
|
49
51
|
ClientBroker.setServerUrl(rememberedServerUrl)
|
|
50
52
|
end
|
|
51
53
|
applyRememberedServerUrl()
|
|
54
|
+
-- Play DataModels do not reliably fire Plugin.Unloading before Studio tears
|
|
55
|
+
-- down their VM. Close the persistent WebStreamClient while BindToClose still
|
|
56
|
+
-- gives this server peer a live execution context; otherwise each play cycle
|
|
57
|
+
-- can retain one native event stream until Studio itself is restarted. The
|
|
58
|
+
-- transport releases that native stream before yielding to unregister the
|
|
59
|
+
-- logical server peer.
|
|
60
|
+
if startupRole == "server" then
|
|
61
|
+
game:BindToClose(StudioEventStream.suspendForShutdown)
|
|
62
|
+
end
|
|
52
63
|
UI.init(plugin)
|
|
53
64
|
local elements = UI.getElements()
|
|
54
65
|
local ICON_DISCONNECTED = "rbxassetid://75876056391496"
|
|
@@ -86,12 +97,12 @@ end)
|
|
|
86
97
|
UI.updateUIState()
|
|
87
98
|
Communication.checkForUpdates()
|
|
88
99
|
task.delay(TOOLBAR_REGISTRATION_DELAY_SECONDS, registerToolbarButton)
|
|
89
|
-
-- Auto-activate per
|
|
90
|
-
--
|
|
91
|
-
--
|
|
92
|
-
--
|
|
93
|
-
|
|
94
|
-
local role =
|
|
100
|
+
-- Auto-activate per Peer. Runtime plugin VMs can load before their first
|
|
101
|
+
-- Heartbeat; task.delay() would then wait behind the very multiplayer startup
|
|
102
|
+
-- that needs this Peer to register. Start runtime initialization immediately,
|
|
103
|
+
-- while retaining the short UI settling delay for the edit Peer.
|
|
104
|
+
local function autoActivatePeer()
|
|
105
|
+
local role = startupRole
|
|
95
106
|
if role == "edit" then
|
|
96
107
|
cleanupEditBridgeArtifacts()
|
|
97
108
|
else
|
|
@@ -101,7 +112,7 @@ task.delay(2, function()
|
|
|
101
112
|
end
|
|
102
113
|
end
|
|
103
114
|
if role == "edit" or role == "server" then
|
|
104
|
-
pcall(function()
|
|
115
|
+
local activationOk, activationError = pcall(function()
|
|
105
116
|
local conn = State.getActiveConnection()
|
|
106
117
|
if not conn.isActive then
|
|
107
118
|
if role == "server" then
|
|
@@ -127,17 +138,28 @@ task.delay(2, function()
|
|
|
127
138
|
Communication.activatePlugin()
|
|
128
139
|
end
|
|
129
140
|
end)
|
|
141
|
+
if not activationOk then
|
|
142
|
+
warn(`[robloxstudio-mcp] Automatic {role} Peer activation failed: {activationError}`)
|
|
143
|
+
end
|
|
130
144
|
end
|
|
131
145
|
if role == "server" then
|
|
132
146
|
ClientBroker.setupServerBroker()
|
|
133
147
|
-- The play-server DM is the only one where StudioTestService:EndTest is
|
|
134
148
|
-- legal, so the stop-play monitor lives here. It consumes tokenized
|
|
135
149
|
-- stop requests from plugin settings and acknowledges EndTest results.
|
|
136
|
-
StopPlayMonitor.startMonitor(
|
|
150
|
+
StopPlayMonitor.startMonitor({
|
|
151
|
+
beforeEndTest = StudioEventStream.suspendForShutdown,
|
|
152
|
+
afterEndTestFailure = StudioEventStream.resumeAfterShutdownFailure,
|
|
153
|
+
})
|
|
137
154
|
elseif role == "client" then
|
|
138
155
|
ClientBroker.setupClientBroker()
|
|
139
156
|
end
|
|
140
|
-
end
|
|
157
|
+
end
|
|
158
|
+
if startupRole == "edit" then
|
|
159
|
+
task.delay(2, autoActivatePeer)
|
|
160
|
+
else
|
|
161
|
+
autoActivatePeer()
|
|
162
|
+
end
|
|
141
163
|
]]></string>
|
|
142
164
|
</Properties>
|
|
143
165
|
<Item class="Folder" referent="1">
|
|
@@ -278,17 +300,18 @@ local MicroProfilerHandlers = TS.import(script, script.Parent, "handlers", "Micr
|
|
|
278
300
|
local LuauExec = TS.import(script, script.Parent, "LuauExec")
|
|
279
301
|
local HttpDiagnostics = TS.import(script, script.Parent, "HttpDiagnostics")
|
|
280
302
|
local PluginSession = TS.import(script, script.Parent, "PluginSession")
|
|
303
|
+
local TopologyId = TS.import(script, script.Parent, "TopologyId")
|
|
304
|
+
local PeerRole = TS.import(script, script.Parent, "PeerRole")
|
|
281
305
|
local StudioTestService = game:GetService("StudioTestService")
|
|
282
|
-
--
|
|
283
|
-
--
|
|
284
|
-
--
|
|
285
|
-
--
|
|
286
|
-
-- in ReplicatedStorage; each player gets a logical proxy registration on the
|
|
287
|
-
-- MCP side, multiplexed over the play-server peer's physical event stream.
|
|
306
|
+
-- Client Peers cannot reach the MCP HTTP server, so the server transport
|
|
307
|
+
-- forwards requests over this RemoteFunction. The client supplies only its
|
|
308
|
+
-- VM Peer identity; the server broker assigns trusted process/group topology
|
|
309
|
+
-- from its own playtest context before publishing that Peer to MCP.
|
|
288
310
|
local DEFAULT_MCP_URL = "http://localhost:58741"
|
|
289
311
|
local mcpUrl = DEFAULT_MCP_URL
|
|
290
312
|
local BROKER_NAME = "__MCPClientBroker"
|
|
291
313
|
local BROKER_OWNER_ATTRIBUTE = "__MCPBrokerOwner"
|
|
314
|
+
local CLIENT_IDENTITY_KIND = "identity"
|
|
292
315
|
-- Endpoints the server-peer broker is allowed to forward to the client peer.
|
|
293
316
|
-- Each requires the client peer's plugin VM (because the buffer / require
|
|
294
317
|
-- cache / etc. lives there) so the server peer alone can't satisfy them.
|
|
@@ -309,13 +332,7 @@ local CLIENT_BROKER_ALLOWED_ENDPOINTS = {
|
|
|
309
332
|
["/api/focus-viewport"] = true,
|
|
310
333
|
}
|
|
311
334
|
local function forkRole()
|
|
312
|
-
|
|
313
|
-
return "edit"
|
|
314
|
-
end
|
|
315
|
-
if RunService:IsServer() then
|
|
316
|
-
return "server"
|
|
317
|
-
end
|
|
318
|
-
return "client"
|
|
335
|
+
return PeerRole.detect()
|
|
319
336
|
end
|
|
320
337
|
local function postJson(endpoint, body)
|
|
321
338
|
return pcall(function()
|
|
@@ -356,13 +373,11 @@ local function handleGetRuntimeLogs(data)
|
|
|
356
373
|
local since = d.since
|
|
357
374
|
local tail = d.tail
|
|
358
375
|
local filter = d.filter
|
|
359
|
-
-- "client" is the generic capture tag; MCP-side aggregation overrides it
|
|
360
|
-
-- with the specific role (e.g. "client-1") for capturedBy.
|
|
361
376
|
return RuntimeLogBuffer.query({
|
|
362
377
|
since = since,
|
|
363
378
|
tail = tail,
|
|
364
379
|
filter = filter,
|
|
365
|
-
}
|
|
380
|
+
})
|
|
366
381
|
end
|
|
367
382
|
local function handleMultiplayerTestState()
|
|
368
383
|
local argsOk, args = pcall(function()
|
|
@@ -435,10 +450,44 @@ local function handleMultiplayerTestLeaveClient()
|
|
|
435
450
|
localPlayer = localPlayer,
|
|
436
451
|
}
|
|
437
452
|
end
|
|
438
|
-
local
|
|
453
|
+
local proxyRetryDelay
|
|
454
|
+
local function sendClientIdentity(rf, attempt)
|
|
455
|
+
if PeerRole.detect() ~= "client" or rf.Parent == nil then
|
|
456
|
+
return nil
|
|
457
|
+
end
|
|
458
|
+
local identity = {
|
|
459
|
+
kind = "identity",
|
|
460
|
+
peerId = PluginSession.peerId,
|
|
461
|
+
}
|
|
462
|
+
local ok, response = pcall(function()
|
|
463
|
+
return rf:InvokeServer(identity)
|
|
464
|
+
end)
|
|
465
|
+
if ok and type(response) == "table" then
|
|
466
|
+
local acknowledgement = response
|
|
467
|
+
if acknowledgement.success == true then
|
|
468
|
+
return nil
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
if attempt == 0 then
|
|
472
|
+
warn(`[robloxstudio-mcp] client identity handshake failed; retrying`)
|
|
473
|
+
end
|
|
474
|
+
task.delay(proxyRetryDelay(attempt + 1), sendClientIdentity, rf, attempt + 1)
|
|
475
|
+
end
|
|
476
|
+
local function setupClientBroker(attempt)
|
|
477
|
+
if attempt == nil then
|
|
478
|
+
attempt = 0
|
|
479
|
+
end
|
|
480
|
+
if PeerRole.detect() ~= "client" then
|
|
481
|
+
return nil
|
|
482
|
+
end
|
|
439
483
|
local rf = ReplicatedStorage:WaitForChild(BROKER_NAME, 10)
|
|
440
484
|
if not rf or not rf:IsA("RemoteFunction") then
|
|
441
|
-
|
|
485
|
+
if attempt == 0 then
|
|
486
|
+
warn(`[robloxstudio-mcp] client: {BROKER_NAME} not found; retrying`)
|
|
487
|
+
end
|
|
488
|
+
if RunService:IsRunning() then
|
|
489
|
+
task.delay(proxyRetryDelay(attempt + 1), setupClientBroker, attempt + 1)
|
|
490
|
+
end
|
|
442
491
|
return nil
|
|
443
492
|
end
|
|
444
493
|
rf.OnClientInvoke = function(payload)
|
|
@@ -498,11 +547,12 @@ local function setupClientBroker()
|
|
|
498
547
|
error = `Unsupported client broker endpoint: {payload.endpoint}`,
|
|
499
548
|
}
|
|
500
549
|
end
|
|
550
|
+
task.spawn(sendClientIdentity, rf, 0)
|
|
501
551
|
end
|
|
502
552
|
local INITIAL_PROXY_RETRY_DELAY_SECONDS = 0.5
|
|
503
553
|
local MAX_PROXY_RETRY_DELAY_SECONDS = 5
|
|
504
554
|
local proxyByPlayer = {}
|
|
505
|
-
local
|
|
555
|
+
local proxyByPeerId = {}
|
|
506
556
|
local proxyRegisterFailuresByPlayer = {}
|
|
507
557
|
local pendingProxyDisconnects = {}
|
|
508
558
|
local serverBrokerStarted = false
|
|
@@ -520,50 +570,50 @@ local function unregisterProxy(player, entry)
|
|
|
520
570
|
proxy.generation += 1
|
|
521
571
|
local _player = player
|
|
522
572
|
proxyByPlayer[_player] = nil
|
|
523
|
-
local
|
|
524
|
-
|
|
573
|
+
local _peerId = proxy.peerId
|
|
574
|
+
proxyByPeerId[_peerId] = nil
|
|
525
575
|
local _player_1 = player
|
|
526
576
|
proxyRegisterFailuresByPlayer[_player_1] = nil
|
|
527
|
-
queueProxyDisconnect(proxy.
|
|
577
|
+
queueProxyDisconnect(proxy.peerId)
|
|
528
578
|
end
|
|
529
579
|
local function disconnectAllProxies()
|
|
530
580
|
for player, entry in proxyByPlayer do
|
|
531
581
|
unregisterProxy(player, entry)
|
|
532
582
|
end
|
|
533
583
|
table.clear(proxyByPlayer)
|
|
534
|
-
table.clear(
|
|
584
|
+
table.clear(proxyByPeerId)
|
|
535
585
|
table.clear(proxyRegisterFailuresByPlayer)
|
|
536
586
|
end
|
|
537
|
-
|
|
587
|
+
function proxyRetryDelay(attempt)
|
|
538
588
|
return math.min(INITIAL_PROXY_RETRY_DELAY_SECONDS * math.pow(2, math.max(attempt - 1, 0)), MAX_PROXY_RETRY_DELAY_SECONDS)
|
|
539
589
|
end
|
|
540
|
-
local function deliverProxyDisconnect(
|
|
541
|
-
local
|
|
542
|
-
if not (pendingProxyDisconnects[
|
|
590
|
+
local function deliverProxyDisconnect(peerId, attempt)
|
|
591
|
+
local _peerId = peerId
|
|
592
|
+
if not (pendingProxyDisconnects[_peerId] ~= nil) then
|
|
543
593
|
return nil
|
|
544
594
|
end
|
|
545
595
|
local ok, response = postJson("/disconnect", {
|
|
546
|
-
|
|
596
|
+
peerId = peerId,
|
|
547
597
|
})
|
|
548
598
|
if ok and response and response.Success then
|
|
549
|
-
local
|
|
550
|
-
pendingProxyDisconnects[
|
|
599
|
+
local _peerId_1 = peerId
|
|
600
|
+
pendingProxyDisconnects[_peerId_1] = nil
|
|
551
601
|
return nil
|
|
552
602
|
end
|
|
553
603
|
task.delay(proxyRetryDelay(attempt + 1), function()
|
|
554
|
-
deliverProxyDisconnect(
|
|
604
|
+
deliverProxyDisconnect(peerId, attempt + 1)
|
|
555
605
|
end)
|
|
556
606
|
end
|
|
557
|
-
function queueProxyDisconnect(
|
|
558
|
-
local
|
|
559
|
-
if pendingProxyDisconnects[
|
|
607
|
+
function queueProxyDisconnect(peerId)
|
|
608
|
+
local _peerId = peerId
|
|
609
|
+
if pendingProxyDisconnects[_peerId] ~= nil then
|
|
560
610
|
return nil
|
|
561
611
|
end
|
|
562
|
-
local
|
|
563
|
-
pendingProxyDisconnects[
|
|
564
|
-
task.spawn(deliverProxyDisconnect,
|
|
612
|
+
local _peerId_1 = peerId
|
|
613
|
+
pendingProxyDisconnects[_peerId_1] = true
|
|
614
|
+
task.spawn(deliverProxyDisconnect, peerId, 0)
|
|
565
615
|
end
|
|
566
|
-
local function parseAssignedRole(body)
|
|
616
|
+
local function parseAssignedRole(body, entry)
|
|
567
617
|
local decodeOk, decoded = pcall(function()
|
|
568
618
|
return HttpService:JSONDecode(body)
|
|
569
619
|
end)
|
|
@@ -571,7 +621,7 @@ local function parseAssignedRole(body)
|
|
|
571
621
|
return nil
|
|
572
622
|
end
|
|
573
623
|
local ready = decoded
|
|
574
|
-
if ready.success ~= true then
|
|
624
|
+
if ready.success ~= true or ready.peerId ~= entry.peerId or ready.instanceId ~= entry.instanceId or ready.multiplayerGroupId ~= entry.multiplayerGroupId then
|
|
575
625
|
return nil
|
|
576
626
|
end
|
|
577
627
|
local _assignedRole = ready.assignedRole
|
|
@@ -622,27 +672,18 @@ function registerProxyEntry(entry)
|
|
|
622
672
|
entry.registering = true
|
|
623
673
|
local expectedGeneration = entry.generation
|
|
624
674
|
local requestedRole = if entry.role == "client" then "client" else entry.role
|
|
625
|
-
local readyPayload = PluginSession.createReadyPayload(entry.
|
|
675
|
+
local readyPayload = PluginSession.createReadyPayload(entry.peerId, requestedRole, entry.instanceId, entry.multiplayerGroupId)
|
|
676
|
+
local ok, res = postJson("/ready", readyPayload)
|
|
626
677
|
local _player = entry.player
|
|
627
678
|
if proxyByPlayer[_player] ~= entry then
|
|
628
|
-
return nil
|
|
629
|
-
end
|
|
630
|
-
if entry.generation ~= expectedGeneration then
|
|
631
|
-
if not entry.registering then
|
|
632
|
-
task.spawn(registerProxyEntry, entry)
|
|
633
|
-
end
|
|
634
|
-
return nil
|
|
635
|
-
end
|
|
636
|
-
local ok, res = postJson("/ready", readyPayload)
|
|
637
|
-
local _player_1 = entry.player
|
|
638
|
-
if proxyByPlayer[_player_1] ~= entry then
|
|
639
679
|
if ok and res and res.Success then
|
|
640
|
-
queueProxyDisconnect(entry.
|
|
680
|
+
queueProxyDisconnect(entry.peerId)
|
|
641
681
|
end
|
|
642
682
|
return nil
|
|
643
683
|
end
|
|
644
684
|
if entry.generation ~= expectedGeneration then
|
|
645
|
-
|
|
685
|
+
entry.registering = false
|
|
686
|
+
if not entry.registered then
|
|
646
687
|
task.spawn(registerProxyEntry, entry)
|
|
647
688
|
end
|
|
648
689
|
return nil
|
|
@@ -652,30 +693,91 @@ function registerProxyEntry(entry)
|
|
|
652
693
|
failProxyRegistration(entry, formatPostJsonFailure("/ready", ok, res))
|
|
653
694
|
return nil
|
|
654
695
|
end
|
|
655
|
-
local assignedRole = parseAssignedRole(res.Body)
|
|
696
|
+
local assignedRole = parseAssignedRole(res.Body, entry)
|
|
656
697
|
if assignedRole == nil then
|
|
657
|
-
failProxyRegistration(entry, "invalid /ready response
|
|
698
|
+
failProxyRegistration(entry, "invalid /ready response for client Peer topology")
|
|
658
699
|
return nil
|
|
659
700
|
end
|
|
660
701
|
entry.role = assignedRole
|
|
661
702
|
entry.registered = true
|
|
662
703
|
entry.retryAttempt = 0
|
|
663
|
-
local
|
|
664
|
-
if proxyRegisterFailuresByPlayer[
|
|
665
|
-
local
|
|
666
|
-
proxyRegisterFailuresByPlayer[
|
|
704
|
+
local _player_1 = entry.player
|
|
705
|
+
if proxyRegisterFailuresByPlayer[_player_1] ~= nil then
|
|
706
|
+
local _player_2 = entry.player
|
|
707
|
+
proxyRegisterFailuresByPlayer[_player_2] = nil
|
|
667
708
|
print(`[robloxstudio-mcp] proxy registered for {entry.player.Name} as {assignedRole} via {mcpUrl}`)
|
|
668
709
|
end
|
|
669
710
|
end
|
|
670
|
-
local function
|
|
671
|
-
local
|
|
672
|
-
if
|
|
711
|
+
local function parseClientIdentity(payload)
|
|
712
|
+
local _payload = payload
|
|
713
|
+
if not (type(_payload) == "table") then
|
|
714
|
+
return nil
|
|
715
|
+
end
|
|
716
|
+
local identity = payload
|
|
717
|
+
local _condition = identity.kind ~= CLIENT_IDENTITY_KIND
|
|
718
|
+
if not _condition then
|
|
719
|
+
local _peerId = identity.peerId
|
|
720
|
+
_condition = not (type(_peerId) == "string")
|
|
721
|
+
if not _condition then
|
|
722
|
+
_condition = identity.peerId == ""
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
if _condition then
|
|
673
726
|
return nil
|
|
674
727
|
end
|
|
728
|
+
return {
|
|
729
|
+
kind = "identity",
|
|
730
|
+
peerId = identity.peerId,
|
|
731
|
+
}
|
|
732
|
+
end
|
|
733
|
+
local function registerProxy(player, rf, identity)
|
|
734
|
+
local _peerId = identity.peerId
|
|
735
|
+
local peerOwner = proxyByPeerId[_peerId]
|
|
736
|
+
if peerOwner ~= nil and peerOwner.player ~= player then
|
|
737
|
+
return false
|
|
738
|
+
end
|
|
739
|
+
local _player = player
|
|
740
|
+
local current = proxyByPlayer[_player]
|
|
741
|
+
local multiplayerGroupId = PluginSession.getMultiplayerGroupId()
|
|
742
|
+
-- Managed multiplayer launches one client Player per Studio process. The
|
|
743
|
+
-- server creates that process identity instead of trusting replicated game
|
|
744
|
+
-- code to claim an Instance or Multiplayer Group. Solo clients use the
|
|
745
|
+
-- server's process Instance because their VMs share one Studio process.
|
|
746
|
+
local retainedMultiplayerInstanceId = if current ~= nil and current.multiplayerGroupId == multiplayerGroupId then current.instanceId else nil
|
|
747
|
+
local _result
|
|
748
|
+
if multiplayerGroupId ~= nil then
|
|
749
|
+
local _condition = retainedMultiplayerInstanceId
|
|
750
|
+
if _condition == nil then
|
|
751
|
+
_condition = TopologyId.createInstanceId()
|
|
752
|
+
end
|
|
753
|
+
_result = _condition
|
|
754
|
+
else
|
|
755
|
+
_result = PluginSession.getInstanceId()
|
|
756
|
+
end
|
|
757
|
+
local instanceId = _result
|
|
758
|
+
if current ~= nil then
|
|
759
|
+
if current.peerId ~= identity.peerId then
|
|
760
|
+
unregisterProxy(player, current)
|
|
761
|
+
else
|
|
762
|
+
if current.instanceId ~= instanceId or current.multiplayerGroupId ~= multiplayerGroupId then
|
|
763
|
+
current.generation += 1
|
|
764
|
+
current.instanceId = instanceId
|
|
765
|
+
current.multiplayerGroupId = multiplayerGroupId
|
|
766
|
+
current.role = "client"
|
|
767
|
+
current.registered = false
|
|
768
|
+
current.registering = false
|
|
769
|
+
current.retryAttempt = 0
|
|
770
|
+
task.spawn(registerProxyEntry, current)
|
|
771
|
+
end
|
|
772
|
+
return true
|
|
773
|
+
end
|
|
774
|
+
end
|
|
675
775
|
local entry = {
|
|
676
776
|
player = player,
|
|
677
777
|
remote = rf,
|
|
678
|
-
|
|
778
|
+
peerId = identity.peerId,
|
|
779
|
+
instanceId = instanceId,
|
|
780
|
+
multiplayerGroupId = multiplayerGroupId,
|
|
679
781
|
role = "client",
|
|
680
782
|
registered = false,
|
|
681
783
|
registering = false,
|
|
@@ -684,11 +786,12 @@ local function registerProxy(player, rf)
|
|
|
684
786
|
}
|
|
685
787
|
local _player_1 = player
|
|
686
788
|
proxyByPlayer[_player_1] = entry
|
|
687
|
-
local
|
|
688
|
-
|
|
789
|
+
local _peerId_1 = entry.peerId
|
|
790
|
+
proxyByPeerId[_peerId_1] = entry
|
|
689
791
|
task.spawn(registerProxyEntry, entry)
|
|
792
|
+
return true
|
|
690
793
|
end
|
|
691
|
-
local function
|
|
794
|
+
local function refreshAllProxyRegistrations()
|
|
692
795
|
for _, entry in proxyByPlayer do
|
|
693
796
|
entry.generation += 1
|
|
694
797
|
entry.registered = false
|
|
@@ -697,9 +800,9 @@ local function refreshAllLogicalRegistrations()
|
|
|
697
800
|
task.spawn(registerProxyEntry, entry)
|
|
698
801
|
end
|
|
699
802
|
end
|
|
700
|
-
local function dispatchClientRequest(
|
|
701
|
-
local
|
|
702
|
-
local entry =
|
|
803
|
+
local function dispatchClientRequest(peerId, target, endpoint, data)
|
|
804
|
+
local _peerId = peerId
|
|
805
|
+
local entry = proxyByPeerId[_peerId]
|
|
703
806
|
local _condition = not entry
|
|
704
807
|
if not _condition then
|
|
705
808
|
local _player = entry.player
|
|
@@ -707,7 +810,7 @@ local function dispatchClientRequest(logicalSessionId, target, endpoint, data)
|
|
|
707
810
|
end
|
|
708
811
|
if _condition then
|
|
709
812
|
return {
|
|
710
|
-
error = `Client proxy {target} ({
|
|
813
|
+
error = `Client proxy {target} ({peerId}) is not registered.`,
|
|
711
814
|
}
|
|
712
815
|
end
|
|
713
816
|
if entry.role == "client" then
|
|
@@ -718,7 +821,7 @@ local function dispatchClientRequest(logicalSessionId, target, endpoint, data)
|
|
|
718
821
|
end
|
|
719
822
|
if entry.role ~= target then
|
|
720
823
|
return {
|
|
721
|
-
error = `Client proxy {
|
|
824
|
+
error = `Client proxy {peerId} is registered as {entry.role}, not {target}.`,
|
|
722
825
|
}
|
|
723
826
|
end
|
|
724
827
|
if entry.player.Parent == nil or not RunService:IsRunning() then
|
|
@@ -759,26 +862,46 @@ local function setupServerBroker()
|
|
|
759
862
|
if serverBrokerStarted then
|
|
760
863
|
return nil
|
|
761
864
|
end
|
|
762
|
-
local
|
|
763
|
-
|
|
865
|
+
local existing = ReplicatedStorage:FindFirstChild(BROKER_NAME)
|
|
866
|
+
local rf
|
|
867
|
+
if existing ~= nil then
|
|
868
|
+
if not existing:IsA("RemoteFunction") then
|
|
869
|
+
warn(`[robloxstudio-mcp] server: {BROKER_NAME} exists but is not a RemoteFunction`)
|
|
870
|
+
return nil
|
|
871
|
+
end
|
|
872
|
+
rf = existing
|
|
873
|
+
else
|
|
764
874
|
rf = Instance.new("RemoteFunction")
|
|
765
|
-
rf.Name = BROKER_NAME
|
|
766
|
-
rf.Parent = ReplicatedStorage
|
|
767
875
|
end
|
|
768
876
|
if rf:GetAttribute(BROKER_OWNER_ATTRIBUTE) ~= nil then
|
|
769
877
|
return nil
|
|
770
878
|
end
|
|
771
|
-
rf
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
879
|
+
rf.Name = BROKER_NAME
|
|
880
|
+
rf:SetAttribute(BROKER_OWNER_ATTRIBUTE, PluginSession.peerId)
|
|
881
|
+
rf.OnServerInvoke = function(player, payload)
|
|
882
|
+
local identity = parseClientIdentity(payload)
|
|
883
|
+
if identity == nil then
|
|
884
|
+
return {
|
|
885
|
+
success = false,
|
|
886
|
+
error = "Invalid client Peer identity handshake.",
|
|
887
|
+
}
|
|
888
|
+
end
|
|
889
|
+
if not registerProxy(player, rf, identity) then
|
|
890
|
+
return {
|
|
891
|
+
success = false,
|
|
892
|
+
error = "Client Peer identity is already registered by another player.",
|
|
893
|
+
}
|
|
894
|
+
end
|
|
895
|
+
return {
|
|
896
|
+
success = true,
|
|
897
|
+
}
|
|
779
898
|
end
|
|
780
|
-
|
|
781
|
-
|
|
899
|
+
if rf.Parent == nil then
|
|
900
|
+
rf.Parent = ReplicatedStorage
|
|
901
|
+
end
|
|
902
|
+
serverBrokerStarted = true
|
|
903
|
+
Players.PlayerRemoving:Connect(function(player)
|
|
904
|
+
unregisterProxy(player)
|
|
782
905
|
end)
|
|
783
906
|
game:BindToClose(function()
|
|
784
907
|
disconnectAllProxies()
|
|
@@ -788,7 +911,7 @@ return {
|
|
|
788
911
|
DEFAULT_MCP_URL = DEFAULT_MCP_URL,
|
|
789
912
|
setServerUrl = setServerUrl,
|
|
790
913
|
disconnectAllProxies = disconnectAllProxies,
|
|
791
|
-
|
|
914
|
+
refreshAllProxyRegistrations = refreshAllProxyRegistrations,
|
|
792
915
|
dispatchClientRequest = dispatchClientRequest,
|
|
793
916
|
forkRole = forkRole,
|
|
794
917
|
setupClientBroker = setupClientBroker,
|
|
@@ -831,7 +954,7 @@ local ServerUrlSettings = TS.import(script, script.Parent, "ServerUrlSettings")
|
|
|
831
954
|
local PluginSession = TS.import(script, script.Parent, "PluginSession")
|
|
832
955
|
local StudioEventStream = TS.import(script, script.Parent, "StudioEventStream")
|
|
833
956
|
local assignedRole
|
|
834
|
-
local
|
|
957
|
+
local lastReadyPlaceKey
|
|
835
958
|
local initialRole = PluginSession.getRole()
|
|
836
959
|
local routeMap = {
|
|
837
960
|
["/api/file-tree"] = QueryHandlers.getFileTree,
|
|
@@ -906,8 +1029,8 @@ local function getConnectionStatus()
|
|
|
906
1029
|
return "connecting"
|
|
907
1030
|
end
|
|
908
1031
|
local function dispatchStreamRequest(request, context)
|
|
909
|
-
if request.
|
|
910
|
-
return ClientBroker.dispatchClientRequest(request.
|
|
1032
|
+
if request.peerId ~= PluginSession.peerId then
|
|
1033
|
+
return ClientBroker.dispatchClientRequest(request.peerId, request.target, request.endpoint, request.data)
|
|
911
1034
|
end
|
|
912
1035
|
local _condition = assignedRole
|
|
913
1036
|
if _condition == nil then
|
|
@@ -916,7 +1039,7 @@ local function dispatchStreamRequest(request, context)
|
|
|
916
1039
|
local localRole = _condition
|
|
917
1040
|
if request.target ~= localRole then
|
|
918
1041
|
return {
|
|
919
|
-
error = `
|
|
1042
|
+
error = `Transport peer is registered as {localRole}, not {request.target}.`,
|
|
920
1043
|
}
|
|
921
1044
|
end
|
|
922
1045
|
return processRequest({
|
|
@@ -930,9 +1053,9 @@ local function handleReady(response)
|
|
|
930
1053
|
return nil
|
|
931
1054
|
end
|
|
932
1055
|
assignedRole = response.assignedRole
|
|
933
|
-
|
|
1056
|
+
lastReadyPlaceKey = PluginSession.getPlaceKey()
|
|
934
1057
|
ServerUrlSettings.rememberServerUrl(conn.serverUrl)
|
|
935
|
-
ClientBroker.
|
|
1058
|
+
ClientBroker.refreshAllProxyRegistrations()
|
|
936
1059
|
end
|
|
937
1060
|
local function handleStatus(status)
|
|
938
1061
|
local conn = State.getActiveConnection()
|
|
@@ -1011,7 +1134,7 @@ local function ensureIdentityWatchers()
|
|
|
1011
1134
|
if signalOk and signal then
|
|
1012
1135
|
placeIdChangeConn = signal:Connect(function()
|
|
1013
1136
|
PluginSession.invalidatePlaceName()
|
|
1014
|
-
|
|
1137
|
+
lastReadyPlaceKey = PluginSession.getPlaceKey()
|
|
1015
1138
|
StudioEventStream.refresh()
|
|
1016
1139
|
end)
|
|
1017
1140
|
end
|
|
@@ -1051,7 +1174,7 @@ local function activatePlugin()
|
|
|
1051
1174
|
conn.port = port
|
|
1052
1175
|
end
|
|
1053
1176
|
ClientBroker.setServerUrl(conn.serverUrl)
|
|
1054
|
-
|
|
1177
|
+
lastReadyPlaceKey = PluginSession.getPlaceKey()
|
|
1055
1178
|
UI.updateUIState()
|
|
1056
1179
|
StudioEventStream.start({
|
|
1057
1180
|
serverUrl = conn.serverUrl,
|
|
@@ -1068,15 +1191,15 @@ local function activatePlugin()
|
|
|
1068
1191
|
deactivatePlugin()
|
|
1069
1192
|
return nil
|
|
1070
1193
|
end
|
|
1071
|
-
local
|
|
1072
|
-
if
|
|
1073
|
-
|
|
1194
|
+
local currentPlaceKey = PluginSession.getPlaceKey()
|
|
1195
|
+
if lastReadyPlaceKey ~= nil and currentPlaceKey ~= lastReadyPlaceKey then
|
|
1196
|
+
lastReadyPlaceKey = currentPlaceKey
|
|
1074
1197
|
PluginSession.invalidatePlaceName()
|
|
1075
1198
|
StudioEventStream.refresh()
|
|
1076
1199
|
end
|
|
1077
1200
|
end)
|
|
1078
1201
|
end
|
|
1079
|
-
if
|
|
1202
|
+
if initialRole == "edit" then
|
|
1080
1203
|
task.spawn(cleanupEditBridgeArtifacts)
|
|
1081
1204
|
end
|
|
1082
1205
|
ensureIdentityWatchers()
|
|
@@ -1099,7 +1222,7 @@ function deactivatePlugin()
|
|
|
1099
1222
|
conn.heartbeatConnection:Disconnect()
|
|
1100
1223
|
conn.heartbeatConnection = nil
|
|
1101
1224
|
end
|
|
1102
|
-
|
|
1225
|
+
lastReadyPlaceKey = nil
|
|
1103
1226
|
assignedRole = nil
|
|
1104
1227
|
conn.consecutiveFailures = 0
|
|
1105
1228
|
conn.currentRetryDelay = 0.5
|
|
@@ -1267,6 +1390,7 @@ local ReplicatedStorage = _services.ReplicatedStorage
|
|
|
1267
1390
|
local RunService = _services.RunService
|
|
1268
1391
|
local ServerScriptService = _services.ServerScriptService
|
|
1269
1392
|
local StarterPlayer = _services.StarterPlayer
|
|
1393
|
+
local PeerRole = TS.import(script, script.Parent, "PeerRole")
|
|
1270
1394
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
1271
1395
|
local function getStarterPlayerScripts()
|
|
1272
1396
|
return StarterPlayer:FindFirstChild("StarterPlayerScripts")
|
|
@@ -1348,9 +1472,9 @@ local function computeBridgeStamp()
|
|
|
1348
1472
|
for i = 1, #combined do
|
|
1349
1473
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1350
1474
|
end
|
|
1351
|
-
-- "3.0
|
|
1475
|
+
-- "3.1.0" is replaced with the package version at package time
|
|
1352
1476
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1353
|
-
return `{tostring(h)}-3.0
|
|
1477
|
+
return `{tostring(h)}-3.1.0`
|
|
1354
1478
|
end
|
|
1355
1479
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1356
1480
|
local function setSource(scriptInst, source)
|
|
@@ -1484,10 +1608,10 @@ local function installClientRuntimeBridge()
|
|
|
1484
1608
|
}
|
|
1485
1609
|
end
|
|
1486
1610
|
local function ensureRuntimeBridgeInstalled()
|
|
1487
|
-
if
|
|
1611
|
+
if PeerRole.detect() == "edit" then
|
|
1488
1612
|
return {
|
|
1489
1613
|
installed = false,
|
|
1490
|
-
error = "Eval bridges are installed only in
|
|
1614
|
+
error = "Eval bridges are installed only in play DataModels",
|
|
1491
1615
|
}
|
|
1492
1616
|
end
|
|
1493
1617
|
if RunService:IsServer() then
|
|
@@ -1954,10 +2078,10 @@ return {
|
|
|
1954
2078
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
1955
2079
|
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
1956
2080
|
local Utils = TS.import(script, script.Parent.Parent, "Utils")
|
|
2081
|
+
local PeerRole = TS.import(script, script.Parent.Parent, "PeerRole")
|
|
1957
2082
|
local _binding = Utils
|
|
1958
2083
|
local getInstanceByPath = _binding.getInstanceByPath
|
|
1959
2084
|
local HttpService = game:GetService("HttpService")
|
|
1960
|
-
local RunService = game:GetService("RunService")
|
|
1961
2085
|
local ServerStorage = game:GetService("ServerStorage")
|
|
1962
2086
|
local LOG_PREFIX = "Breakpoint"
|
|
1963
2087
|
local REGISTRY_KEY_PREFIX = "MCP_BREAKPOINTS_V1_"
|
|
@@ -1972,7 +2096,7 @@ end
|
|
|
1972
2096
|
local function breakpointKey(scriptPath, line)
|
|
1973
2097
|
return `{scriptPath}:{line}`
|
|
1974
2098
|
end
|
|
1975
|
-
local function
|
|
2099
|
+
local function computePlaceKey()
|
|
1976
2100
|
if game.PlaceId ~= 0 then
|
|
1977
2101
|
return `place:{tostring(game.PlaceId)}`
|
|
1978
2102
|
end
|
|
@@ -1987,13 +2111,7 @@ local function computeInstanceId()
|
|
|
1987
2111
|
return `anon:{fresh}`
|
|
1988
2112
|
end
|
|
1989
2113
|
local function detectRole()
|
|
1990
|
-
|
|
1991
|
-
return "edit"
|
|
1992
|
-
end
|
|
1993
|
-
if RunService:IsServer() then
|
|
1994
|
-
return "server"
|
|
1995
|
-
end
|
|
1996
|
-
return "client"
|
|
2114
|
+
return PeerRole.detect()
|
|
1997
2115
|
end
|
|
1998
2116
|
local function requestedRole(requestData)
|
|
1999
2117
|
local ___mcp_target_role = requestData.__mcp_target_role
|
|
@@ -2004,15 +2122,10 @@ local function requestedRole(requestData)
|
|
|
2004
2122
|
return if _condition then requestData.__mcp_target_role else detectRole()
|
|
2005
2123
|
end
|
|
2006
2124
|
local function registryScope(requestData)
|
|
2007
|
-
local
|
|
2008
|
-
local _condition = type(___mcp_instance_id) == "string"
|
|
2009
|
-
if _condition then
|
|
2010
|
-
_condition = requestData.__mcp_instance_id ~= ""
|
|
2011
|
-
end
|
|
2012
|
-
local instanceId = if _condition then requestData.__mcp_instance_id else computeInstanceId()
|
|
2125
|
+
local placeKey = computePlaceKey()
|
|
2013
2126
|
local role = requestedRole(requestData)
|
|
2014
2127
|
return {
|
|
2015
|
-
key = `{REGISTRY_KEY_PREFIX}{
|
|
2128
|
+
key = `{REGISTRY_KEY_PREFIX}{placeKey}:{role}`,
|
|
2016
2129
|
}
|
|
2017
2130
|
end
|
|
2018
2131
|
local function readSetting(key)
|
|
@@ -3279,15 +3392,11 @@ local function getRuntimeLogs(requestData)
|
|
|
3279
3392
|
local since = requestData.since
|
|
3280
3393
|
local tail = requestData.tail
|
|
3281
3394
|
local filter = requestData.filter
|
|
3282
|
-
-- This is the buffer that captured the LogService event, not necessarily
|
|
3283
|
-
-- the script-origin peer. Ordinary playtests share/reflect logs across
|
|
3284
|
-
-- edit/server/client LogService buffers.
|
|
3285
|
-
local capturedBy = RuntimeLogBuffer.detectPeer()
|
|
3286
3395
|
return RuntimeLogBuffer.query({
|
|
3287
3396
|
since = since,
|
|
3288
3397
|
tail = tail,
|
|
3289
3398
|
filter = filter,
|
|
3290
|
-
}
|
|
3399
|
+
})
|
|
3291
3400
|
end
|
|
3292
3401
|
return {
|
|
3293
3402
|
getRuntimeLogs = getRuntimeLogs,
|
|
@@ -7576,7 +7685,7 @@ return {
|
|
|
7576
7685
|
<string name="Name">SerializationHandlers</string>
|
|
7577
7686
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
7578
7687
|
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
7579
|
-
local
|
|
7688
|
+
local PeerRole = TS.import(script, script.Parent.Parent, "PeerRole")
|
|
7580
7689
|
local Utils = TS.import(script, script.Parent.Parent, "Utils")
|
|
7581
7690
|
local Recording = TS.import(script, script.Parent.Parent, "Recording")
|
|
7582
7691
|
-- SerializationService:SerializeInstancesAsync / DeserializeInstancesAsync were
|
|
@@ -7696,7 +7805,7 @@ local function importRbxm(requestData)
|
|
|
7696
7805
|
-- All-or-nothing parenting. Track every instance we've attached and roll back
|
|
7697
7806
|
-- (unparent + Destroy) if any later one fails - partial imports leave the DM
|
|
7698
7807
|
-- in a worse state than failing cleanly.
|
|
7699
|
-
local isEdit =
|
|
7808
|
+
local isEdit = PeerRole.detect() == "edit"
|
|
7700
7809
|
local recordingId = if isEdit then beginRecording(`Import rbxm`) else nil
|
|
7701
7810
|
local attached = {}
|
|
7702
7811
|
local failureMessage
|
|
@@ -7767,19 +7876,15 @@ local HttpService = _services.HttpService
|
|
|
7767
7876
|
local Players = _services.Players
|
|
7768
7877
|
local RunService = _services.RunService
|
|
7769
7878
|
local StopPlayMonitor = TS.import(script, script.Parent.Parent, "StopPlayMonitor")
|
|
7879
|
+
local PluginSession = TS.import(script, script.Parent.Parent, "PluginSession")
|
|
7880
|
+
local PeerRole = TS.import(script, script.Parent.Parent, "PeerRole")
|
|
7770
7881
|
local StudioTestService = game:GetService("StudioTestService")
|
|
7771
7882
|
local testRunning = false
|
|
7772
7883
|
local multiplayerState = {
|
|
7773
7884
|
phase = "idle",
|
|
7774
7885
|
}
|
|
7775
7886
|
local function detectPeerRole()
|
|
7776
|
-
|
|
7777
|
-
return "edit"
|
|
7778
|
-
end
|
|
7779
|
-
if RunService:IsServer() then
|
|
7780
|
-
return "server"
|
|
7781
|
-
end
|
|
7782
|
-
return "client"
|
|
7887
|
+
return PeerRole.detect()
|
|
7783
7888
|
end
|
|
7784
7889
|
local function getPlayersSnapshot()
|
|
7785
7890
|
local _exp = Players:GetPlayers()
|
|
@@ -7854,6 +7959,7 @@ local function startPlaytest(requestData)
|
|
|
7854
7959
|
}
|
|
7855
7960
|
end
|
|
7856
7961
|
testRunning = true
|
|
7962
|
+
local topologyMarkerToken = PluginSession.prepareSharedTopology()
|
|
7857
7963
|
task.spawn(function()
|
|
7858
7964
|
local ok, result = pcall(function()
|
|
7859
7965
|
if mode == "play" then
|
|
@@ -7861,6 +7967,7 @@ local function startPlaytest(requestData)
|
|
|
7861
7967
|
end
|
|
7862
7968
|
return StudioTestService:ExecuteRunModeAsync({})
|
|
7863
7969
|
end)
|
|
7970
|
+
PluginSession.clearTopologyMarker(topologyMarkerToken)
|
|
7864
7971
|
if not ok then
|
|
7865
7972
|
warn(`[robloxstudio-mcp] Playtest ended with error: {result}`)
|
|
7866
7973
|
end
|
|
@@ -7966,11 +8073,13 @@ local function multiplayerTestStart(requestData)
|
|
|
7966
8073
|
testArgs = testArgs,
|
|
7967
8074
|
startedAt = tick(),
|
|
7968
8075
|
}
|
|
8076
|
+
local topologyMarkerToken = PluginSession.prepareMultiplayerTopology(testId)
|
|
7969
8077
|
task.spawn(function()
|
|
7970
8078
|
multiplayerState.phase = "running"
|
|
7971
8079
|
local ok, result = pcall(function()
|
|
7972
8080
|
return StudioTestService:ExecuteMultiplayerTestAsync(numPlayers, testArgs)
|
|
7973
8081
|
end)
|
|
8082
|
+
PluginSession.clearTopologyMarker(topologyMarkerToken)
|
|
7974
8083
|
multiplayerState.completedAt = tick()
|
|
7975
8084
|
multiplayerState.ok = ok
|
|
7976
8085
|
if ok then
|
|
@@ -8652,20 +8761,69 @@ return {
|
|
|
8652
8761
|
</Properties>
|
|
8653
8762
|
</Item>
|
|
8654
8763
|
<Item class="ModuleScript" referent="27">
|
|
8764
|
+
<Properties>
|
|
8765
|
+
<string name="Name">PeerRole</string>
|
|
8766
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
8767
|
+
local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
8768
|
+
local RunService = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services").RunService
|
|
8769
|
+
local function detect()
|
|
8770
|
+
if RunService:IsEdit() then
|
|
8771
|
+
return "edit"
|
|
8772
|
+
end
|
|
8773
|
+
if RunService:IsServer() then
|
|
8774
|
+
return "server"
|
|
8775
|
+
end
|
|
8776
|
+
return "client"
|
|
8777
|
+
end
|
|
8778
|
+
return {
|
|
8779
|
+
detect = detect,
|
|
8780
|
+
}
|
|
8781
|
+
]]></string>
|
|
8782
|
+
</Properties>
|
|
8783
|
+
</Item>
|
|
8784
|
+
<Item class="ModuleScript" referent="28">
|
|
8655
8785
|
<Properties>
|
|
8656
8786
|
<string name="Name">PluginSession</string>
|
|
8657
8787
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
8658
8788
|
local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
8659
8789
|
local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
8660
8790
|
local HttpService = _services.HttpService
|
|
8791
|
+
local ReplicatedStorage = _services.ReplicatedStorage
|
|
8661
8792
|
local RunService = _services.RunService
|
|
8662
8793
|
local ServerStorage = _services.ServerStorage
|
|
8663
8794
|
local State = TS.import(script, script.Parent, "State")
|
|
8795
|
+
local PeerRole = TS.import(script, script.Parent, "PeerRole")
|
|
8796
|
+
local TopologyId = TS.import(script, script.Parent, "TopologyId")
|
|
8664
8797
|
local MCP_PLACE_ID_ATTRIBUTE = "__MCPPlaceId"
|
|
8665
|
-
local
|
|
8798
|
+
local TOPOLOGY_MODE_ATTRIBUTE = "__MCPTopologyMode"
|
|
8799
|
+
local TOPOLOGY_INSTANCE_ID_ATTRIBUTE = "__MCPTopologyInstanceId"
|
|
8800
|
+
local TOPOLOGY_GROUP_ID_ATTRIBUTE = "__MCPTopologyGroupId"
|
|
8801
|
+
local TOPOLOGY_TOKEN_ATTRIBUTE = "__MCPTopologyToken"
|
|
8802
|
+
local peerId = TopologyId.createPeerId()
|
|
8803
|
+
local processInstanceId = TopologyId.currentProcessInstanceId()
|
|
8666
8804
|
local cachedPlaceName
|
|
8667
8805
|
local cachedPlaceNamePlaceId
|
|
8806
|
+
local function getMarkerMode()
|
|
8807
|
+
local mode = ReplicatedStorage:GetAttribute(TOPOLOGY_MODE_ATTRIBUTE)
|
|
8808
|
+
return if mode == "shared" or mode == "multiplayer" then mode else nil
|
|
8809
|
+
end
|
|
8668
8810
|
local function getInstanceId()
|
|
8811
|
+
if getMarkerMode() == "shared" then
|
|
8812
|
+
local sharedInstanceId = ReplicatedStorage:GetAttribute(TOPOLOGY_INSTANCE_ID_ATTRIBUTE)
|
|
8813
|
+
if type(sharedInstanceId) == "string" and sharedInstanceId ~= "" then
|
|
8814
|
+
return sharedInstanceId
|
|
8815
|
+
end
|
|
8816
|
+
end
|
|
8817
|
+
return processInstanceId
|
|
8818
|
+
end
|
|
8819
|
+
local function getMultiplayerGroupId()
|
|
8820
|
+
if getMarkerMode() ~= "multiplayer" then
|
|
8821
|
+
return nil
|
|
8822
|
+
end
|
|
8823
|
+
local groupId = ReplicatedStorage:GetAttribute(TOPOLOGY_GROUP_ID_ATTRIBUTE)
|
|
8824
|
+
return if type(groupId) == "string" and groupId ~= "" then groupId else nil
|
|
8825
|
+
end
|
|
8826
|
+
local function getPlaceKey()
|
|
8669
8827
|
if game.PlaceId ~= 0 then
|
|
8670
8828
|
return `place:{tostring(game.PlaceId)}`
|
|
8671
8829
|
end
|
|
@@ -8679,14 +8837,32 @@ local function getInstanceId()
|
|
|
8679
8837
|
end)
|
|
8680
8838
|
return `anon:{fresh}`
|
|
8681
8839
|
end
|
|
8682
|
-
local function
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8840
|
+
local function setTopologyMarker(mode, instanceId, groupId)
|
|
8841
|
+
local token = HttpService:GenerateGUID(false)
|
|
8842
|
+
ReplicatedStorage:SetAttribute(TOPOLOGY_MODE_ATTRIBUTE, nil)
|
|
8843
|
+
ReplicatedStorage:SetAttribute(TOPOLOGY_INSTANCE_ID_ATTRIBUTE, instanceId)
|
|
8844
|
+
ReplicatedStorage:SetAttribute(TOPOLOGY_GROUP_ID_ATTRIBUTE, groupId)
|
|
8845
|
+
ReplicatedStorage:SetAttribute(TOPOLOGY_TOKEN_ATTRIBUTE, token)
|
|
8846
|
+
ReplicatedStorage:SetAttribute(TOPOLOGY_MODE_ATTRIBUTE, mode)
|
|
8847
|
+
return token
|
|
8848
|
+
end
|
|
8849
|
+
local function prepareSharedTopology()
|
|
8850
|
+
return setTopologyMarker("shared", processInstanceId, nil)
|
|
8851
|
+
end
|
|
8852
|
+
local function prepareMultiplayerTopology(groupId)
|
|
8853
|
+
return setTopologyMarker("multiplayer", nil, groupId)
|
|
8854
|
+
end
|
|
8855
|
+
local function clearTopologyMarker(token)
|
|
8856
|
+
if ReplicatedStorage:GetAttribute(TOPOLOGY_TOKEN_ATTRIBUTE) ~= token then
|
|
8857
|
+
return nil
|
|
8688
8858
|
end
|
|
8689
|
-
|
|
8859
|
+
ReplicatedStorage:SetAttribute(TOPOLOGY_MODE_ATTRIBUTE, nil)
|
|
8860
|
+
ReplicatedStorage:SetAttribute(TOPOLOGY_INSTANCE_ID_ATTRIBUTE, nil)
|
|
8861
|
+
ReplicatedStorage:SetAttribute(TOPOLOGY_GROUP_ID_ATTRIBUTE, nil)
|
|
8862
|
+
ReplicatedStorage:SetAttribute(TOPOLOGY_TOKEN_ATTRIBUTE, nil)
|
|
8863
|
+
end
|
|
8864
|
+
local function getRole()
|
|
8865
|
+
return PeerRole.detect()
|
|
8690
8866
|
end
|
|
8691
8867
|
local function invalidatePlaceName()
|
|
8692
8868
|
cachedPlaceName = nil
|
|
@@ -8717,14 +8893,22 @@ local function getPlaceName()
|
|
|
8717
8893
|
end
|
|
8718
8894
|
return game.Name
|
|
8719
8895
|
end
|
|
8720
|
-
local function createReadyPayload(
|
|
8896
|
+
local function createReadyPayload(readyPeerId, role, instanceId, multiplayerGroupId)
|
|
8897
|
+
if instanceId == nil then
|
|
8898
|
+
instanceId = getInstanceId()
|
|
8899
|
+
end
|
|
8900
|
+
if multiplayerGroupId == nil then
|
|
8901
|
+
multiplayerGroupId = getMultiplayerGroupId()
|
|
8902
|
+
end
|
|
8721
8903
|
return {
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
instanceId =
|
|
8904
|
+
peerId = readyPeerId,
|
|
8905
|
+
transportPeerId = peerId,
|
|
8906
|
+
instanceId = instanceId,
|
|
8907
|
+
multiplayerGroupId = multiplayerGroupId,
|
|
8725
8908
|
role = role,
|
|
8726
8909
|
placeId = game.PlaceId,
|
|
8727
8910
|
placeName = getPlaceName(),
|
|
8911
|
+
placeKey = getPlaceKey(),
|
|
8728
8912
|
dataModelName = game.Name,
|
|
8729
8913
|
isRunning = RunService:IsRunning(),
|
|
8730
8914
|
pluginVersion = State.CURRENT_VERSION,
|
|
@@ -8733,17 +8917,22 @@ local function createReadyPayload(pluginSessionId, role)
|
|
|
8733
8917
|
}
|
|
8734
8918
|
end
|
|
8735
8919
|
return {
|
|
8736
|
-
|
|
8920
|
+
peerId = peerId,
|
|
8737
8921
|
getInstanceId = getInstanceId,
|
|
8922
|
+
getMultiplayerGroupId = getMultiplayerGroupId,
|
|
8923
|
+
getPlaceKey = getPlaceKey,
|
|
8738
8924
|
getRole = getRole,
|
|
8739
8925
|
getPlaceName = getPlaceName,
|
|
8740
8926
|
invalidatePlaceName = invalidatePlaceName,
|
|
8927
|
+
prepareSharedTopology = prepareSharedTopology,
|
|
8928
|
+
prepareMultiplayerTopology = prepareMultiplayerTopology,
|
|
8929
|
+
clearTopologyMarker = clearTopologyMarker,
|
|
8741
8930
|
createReadyPayload = createReadyPayload,
|
|
8742
8931
|
}
|
|
8743
8932
|
]]></string>
|
|
8744
8933
|
</Properties>
|
|
8745
8934
|
</Item>
|
|
8746
|
-
<Item class="ModuleScript" referent="
|
|
8935
|
+
<Item class="ModuleScript" referent="29">
|
|
8747
8936
|
<Properties>
|
|
8748
8937
|
<string name="Name">Recording</string>
|
|
8749
8938
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -8773,7 +8962,7 @@ return {
|
|
|
8773
8962
|
]]></string>
|
|
8774
8963
|
</Properties>
|
|
8775
8964
|
</Item>
|
|
8776
|
-
<Item class="ModuleScript" referent="
|
|
8965
|
+
<Item class="ModuleScript" referent="30">
|
|
8777
8966
|
<Properties>
|
|
8778
8967
|
<string name="Name">RenderMonitor</string>
|
|
8779
8968
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -8841,30 +9030,22 @@ return {
|
|
|
8841
9030
|
]]></string>
|
|
8842
9031
|
</Properties>
|
|
8843
9032
|
</Item>
|
|
8844
|
-
<Item class="ModuleScript" referent="
|
|
9033
|
+
<Item class="ModuleScript" referent="31">
|
|
8845
9034
|
<Properties>
|
|
8846
9035
|
<string name="Name">RuntimeLogBuffer</string>
|
|
8847
9036
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
8848
9037
|
local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
8849
|
-
--
|
|
8850
|
-
-- Powers
|
|
8851
|
-
-- primitives + StringValue approach from chrrxs/roblox-mcp-primitives.
|
|
8852
|
-
--
|
|
8853
|
-
-- Each peer's plugin attaches a MessageOut listener at plugin load (edit DM,
|
|
8854
|
-
-- play-server DM, play-client DM all run their own copy of this module).
|
|
8855
|
-
-- Captured entries live in plugin module-state; nothing is parented to the
|
|
8856
|
-
-- DataModel. The buffer is bounded by a message-byte budget; oldest entries
|
|
8857
|
-
-- drop when over budget.
|
|
9038
|
+
-- Bounded capture for one Peer VM's LogService callbacks.
|
|
9039
|
+
-- Powers get_runtime_logs without parenting state to the DataModel.
|
|
8858
9040
|
--
|
|
8859
|
-
--
|
|
8860
|
-
--
|
|
8861
|
-
--
|
|
8862
|
-
--
|
|
8863
|
-
-- exposes that as capturedBy, and only promotes it to origin peer in
|
|
8864
|
-
-- StudioTestService multiplayer sessions where peer attribution is reliable.
|
|
9041
|
+
-- A Studio process can host several Peer VMs, and its LogService callbacks are
|
|
9042
|
+
-- delivered in those VM contexts. MCP reads every Peer buffer in an Instance
|
|
9043
|
+
-- and merges them into that process's log stream. Multiplayer Group Instances
|
|
9044
|
+
-- remain isolated and are returned independently.
|
|
8865
9045
|
local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
8866
9046
|
local LogService = _services.LogService
|
|
8867
9047
|
local RunService = _services.RunService
|
|
9048
|
+
local PeerRole = TS.import(script, script.Parent, "PeerRole")
|
|
8868
9049
|
local MAX_BYTES = 64 * 1024
|
|
8869
9050
|
local HARD_ENTRY_CAP = 50_000
|
|
8870
9051
|
local entries = {}
|
|
@@ -8954,7 +9135,7 @@ local function seedRuntimeHistory()
|
|
|
8954
9135
|
if not ok then
|
|
8955
9136
|
return nil
|
|
8956
9137
|
end
|
|
8957
|
-
local isEdit =
|
|
9138
|
+
local isEdit = PeerRole.detect() == "edit"
|
|
8958
9139
|
-- GetLogHistory timestamps and DateTime.now() share Unix time, while
|
|
8959
9140
|
-- os.clock() is elapsed time for this Studio process. Their difference is
|
|
8960
9141
|
-- therefore the process launch boundary. Edit-mode history is filtered to
|
|
@@ -8990,16 +9171,7 @@ local function install()
|
|
|
8990
9171
|
pushEntry(msg, t, nil, context)
|
|
8991
9172
|
end)
|
|
8992
9173
|
end
|
|
8993
|
-
local function
|
|
8994
|
-
if not RunService:IsRunning() then
|
|
8995
|
-
return "edit"
|
|
8996
|
-
end
|
|
8997
|
-
if RunService:IsServer() then
|
|
8998
|
-
return "server"
|
|
8999
|
-
end
|
|
9000
|
-
return "client"
|
|
9001
|
-
end
|
|
9002
|
-
local function query(opts, capturedBy)
|
|
9174
|
+
local function query(opts)
|
|
9003
9175
|
local _result
|
|
9004
9176
|
if opts.since ~= nil then
|
|
9005
9177
|
-- ▼ ReadonlyArray.filter ▼
|
|
@@ -9070,7 +9242,6 @@ local function query(opts, capturedBy)
|
|
|
9070
9242
|
end
|
|
9071
9243
|
local last = if #entries > 0 then entries[#entries] else nil
|
|
9072
9244
|
local _object = {
|
|
9073
|
-
capturedBy = capturedBy,
|
|
9074
9245
|
entries = result,
|
|
9075
9246
|
totalDropped = totalDropped,
|
|
9076
9247
|
}
|
|
@@ -9090,13 +9261,12 @@ local function query(opts, capturedBy)
|
|
|
9090
9261
|
end
|
|
9091
9262
|
return {
|
|
9092
9263
|
install = install,
|
|
9093
|
-
detectPeer = detectPeer,
|
|
9094
9264
|
query = query,
|
|
9095
9265
|
}
|
|
9096
9266
|
]]></string>
|
|
9097
9267
|
</Properties>
|
|
9098
9268
|
</Item>
|
|
9099
|
-
<Item class="ModuleScript" referent="
|
|
9269
|
+
<Item class="ModuleScript" referent="32">
|
|
9100
9270
|
<Properties>
|
|
9101
9271
|
<string name="Name">ScriptSearch</string>
|
|
9102
9272
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -9423,7 +9593,7 @@ return {
|
|
|
9423
9593
|
]]></string>
|
|
9424
9594
|
</Properties>
|
|
9425
9595
|
</Item>
|
|
9426
|
-
<Item class="ModuleScript" referent="
|
|
9596
|
+
<Item class="ModuleScript" referent="33">
|
|
9427
9597
|
<Properties>
|
|
9428
9598
|
<string name="Name">ServerUrlSettings</string>
|
|
9429
9599
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -9470,14 +9640,14 @@ local function addUnique(values, value)
|
|
|
9470
9640
|
table.insert(_values_1, _value_1)
|
|
9471
9641
|
end
|
|
9472
9642
|
end
|
|
9473
|
-
local function
|
|
9474
|
-
local
|
|
9643
|
+
local function computePlaceKeys(options)
|
|
9644
|
+
local placeKeys = {}
|
|
9475
9645
|
if game.PlaceId ~= 0 then
|
|
9476
|
-
addUnique(
|
|
9646
|
+
addUnique(placeKeys, `place:{tostring(game.PlaceId)}`)
|
|
9477
9647
|
end
|
|
9478
9648
|
local existing = ServerStorage:GetAttribute("__MCPPlaceId")
|
|
9479
9649
|
if type(existing) == "string" and existing ~= "" then
|
|
9480
|
-
addUnique(
|
|
9650
|
+
addUnique(placeKeys, `anon:{existing}`)
|
|
9481
9651
|
else
|
|
9482
9652
|
local _condition = game.PlaceId == 0
|
|
9483
9653
|
if _condition then
|
|
@@ -9492,13 +9662,13 @@ local function computeInstanceIds(options)
|
|
|
9492
9662
|
pcall(function()
|
|
9493
9663
|
return ServerStorage:SetAttribute("__MCPPlaceId", fresh)
|
|
9494
9664
|
end)
|
|
9495
|
-
addUnique(
|
|
9665
|
+
addUnique(placeKeys, `anon:{fresh}`)
|
|
9496
9666
|
end
|
|
9497
9667
|
end
|
|
9498
|
-
return
|
|
9668
|
+
return placeKeys
|
|
9499
9669
|
end
|
|
9500
|
-
local function settingKey(
|
|
9501
|
-
return SETTING_KEY_PREFIX ..
|
|
9670
|
+
local function settingKey(placeKey)
|
|
9671
|
+
return SETTING_KEY_PREFIX .. placeKey
|
|
9502
9672
|
end
|
|
9503
9673
|
local function readSettingString(key)
|
|
9504
9674
|
if not pluginRef then
|
|
@@ -9527,21 +9697,21 @@ local function rememberServerUrl(serverUrl)
|
|
|
9527
9697
|
return nil
|
|
9528
9698
|
end
|
|
9529
9699
|
writeSettingString(GLOBAL_SETTING_KEY, normalized)
|
|
9530
|
-
for _,
|
|
9700
|
+
for _, placeKey in computePlaceKeys({
|
|
9531
9701
|
createAnonymous = true,
|
|
9532
9702
|
}) do
|
|
9533
|
-
writeSettingString(settingKey(
|
|
9703
|
+
writeSettingString(settingKey(placeKey), normalized)
|
|
9534
9704
|
end
|
|
9535
9705
|
end
|
|
9536
9706
|
local function readServerUrl()
|
|
9537
9707
|
if not pluginRef then
|
|
9538
9708
|
return nil
|
|
9539
9709
|
end
|
|
9540
|
-
-- Reading settings should not mint a place
|
|
9541
|
-
-- their own ServerStorage; creating
|
|
9542
|
-
--
|
|
9543
|
-
for _,
|
|
9544
|
-
local remembered = readSettingString(settingKey(
|
|
9710
|
+
-- Reading settings should not mint a place key. Client play DataModels have
|
|
9711
|
+
-- their own ServerStorage; creating a key there would not match the
|
|
9712
|
+
-- edit/server place-scoped setting.
|
|
9713
|
+
for _, placeKey in computePlaceKeys() do
|
|
9714
|
+
local remembered = readSettingString(settingKey(placeKey))
|
|
9545
9715
|
if remembered ~= nil then
|
|
9546
9716
|
return remembered
|
|
9547
9717
|
end
|
|
@@ -9562,11 +9732,11 @@ return {
|
|
|
9562
9732
|
]]></string>
|
|
9563
9733
|
</Properties>
|
|
9564
9734
|
</Item>
|
|
9565
|
-
<Item class="ModuleScript" referent="
|
|
9735
|
+
<Item class="ModuleScript" referent="34">
|
|
9566
9736
|
<Properties>
|
|
9567
9737
|
<string name="Name">State</string>
|
|
9568
9738
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
9569
|
-
local CURRENT_VERSION = "3.0
|
|
9739
|
+
local CURRENT_VERSION = "3.1.0"
|
|
9570
9740
|
local PLUGIN_VARIANT = "main"
|
|
9571
9741
|
local BASE_PORT = 58741
|
|
9572
9742
|
local function createConnection(port)
|
|
@@ -9596,40 +9766,18 @@ return {
|
|
|
9596
9766
|
]]></string>
|
|
9597
9767
|
</Properties>
|
|
9598
9768
|
</Item>
|
|
9599
|
-
<Item class="ModuleScript" referent="
|
|
9769
|
+
<Item class="ModuleScript" referent="35">
|
|
9600
9770
|
<Properties>
|
|
9601
9771
|
<string name="Name">StopPlayMonitor</string>
|
|
9602
9772
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
9603
9773
|
local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
9604
|
-
-- Cross-
|
|
9605
|
-
--
|
|
9606
|
-
--
|
|
9607
|
-
-- During publish-after-connect, both "anon:<uuid>" and "place:<PlaceId>"
|
|
9608
|
-
-- can refer to the same Studio place, so stop requests are mirrored across
|
|
9609
|
-
-- both keys while the monitor waits for a matching result on either key.
|
|
9610
|
-
--
|
|
9611
|
-
-- `plugin:SetSetting` / `plugin:GetSetting` is a per-plugin persistent store
|
|
9612
|
-
-- shared across every DataModel the plugin runs in (edit DMs, play-server
|
|
9613
|
-
-- DMs, play-client DMs). For each connected place we use a dedicated key
|
|
9614
|
-
-- "MCP_STOP_PLAY_<instanceId>" as a tiny request/result mailbox:
|
|
9615
|
-
--
|
|
9616
|
-
-- * The edit DM's handler writes a tokenized stop request into its own key
|
|
9617
|
-
-- (computed from its placeId / ServerStorage anon UUID).
|
|
9618
|
-
-- * Each play-server DM's monitor loop polls the key matching its own
|
|
9619
|
-
-- instanceId at 1Hz. On a fresh token, it calls StudioTestService:EndTest
|
|
9620
|
-
-- and writes a matching result token. Play-server DMs for other places
|
|
9621
|
-
-- never touch this key.
|
|
9622
|
-
-- * The edit DM waits up to ~8s for its result token, confirming a matching
|
|
9623
|
-
-- play-server actually consumed the request.
|
|
9624
|
-
--
|
|
9625
|
-
-- Earlier versions used a single shared boolean flag, which let any
|
|
9626
|
-
-- play-server DM in the same Studio process consume any place's stop
|
|
9627
|
-
-- request — silently yanking teammates' playtests. The per-key scoping
|
|
9628
|
-
-- below is the fix.
|
|
9774
|
+
-- Cross-DataModel stop_playtest signaling via plugin settings. Solo edit and
|
|
9775
|
+
-- runtime peers share one process instanceId through PluginSession's topology
|
|
9776
|
+
-- marker, so only the intended Studio process can consume the request.
|
|
9629
9777
|
local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
9630
9778
|
local HttpService = _services.HttpService
|
|
9631
9779
|
local RunService = _services.RunService
|
|
9632
|
-
local
|
|
9780
|
+
local PluginSession = TS.import(script, script.Parent, "PluginSession")
|
|
9633
9781
|
local StudioTestService = game:GetService("StudioTestService")
|
|
9634
9782
|
local SETTING_KEY_PREFIX = "MCP_STOP_PLAY_"
|
|
9635
9783
|
-- Keep this conservative. plugin:GetSetting is backed by Studio's plugin
|
|
@@ -9646,55 +9794,12 @@ local WAIT_POLL_SEC = 0.1
|
|
|
9646
9794
|
local REQUEST_TTL_SEC = 12.0
|
|
9647
9795
|
local pluginRef
|
|
9648
9796
|
local endTestIssued = false
|
|
9797
|
+
local transportLifecycle
|
|
9649
9798
|
local function init(p)
|
|
9650
9799
|
pluginRef = p
|
|
9651
9800
|
end
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
-- agree on the place identifier (published places: placeId; unpublished:
|
|
9655
|
-
-- UUID on ServerStorage's __MCPPlaceId attribute, travels with the .rbxl
|
|
9656
|
-
-- into the play DM).
|
|
9657
|
-
local function addUnique(values, value)
|
|
9658
|
-
local _values = values
|
|
9659
|
-
local _value = value
|
|
9660
|
-
if not (table.find(_values, _value) ~= nil) then
|
|
9661
|
-
local _values_1 = values
|
|
9662
|
-
local _value_1 = value
|
|
9663
|
-
table.insert(_values_1, _value_1)
|
|
9664
|
-
end
|
|
9665
|
-
end
|
|
9666
|
-
local function computeInstanceIds()
|
|
9667
|
-
local ids = {}
|
|
9668
|
-
if game.PlaceId ~= 0 then
|
|
9669
|
-
addUnique(ids, `place:{tostring(game.PlaceId)}`)
|
|
9670
|
-
end
|
|
9671
|
-
local existing = ServerStorage:GetAttribute("__MCPPlaceId")
|
|
9672
|
-
if type(existing) == "string" and existing ~= "" then
|
|
9673
|
-
addUnique(ids, `anon:{existing}`)
|
|
9674
|
-
elseif game.PlaceId == 0 then
|
|
9675
|
-
local fresh = HttpService:GenerateGUID(false)
|
|
9676
|
-
pcall(function()
|
|
9677
|
-
return ServerStorage:SetAttribute("__MCPPlaceId", fresh)
|
|
9678
|
-
end)
|
|
9679
|
-
addUnique(ids, `anon:{fresh}`)
|
|
9680
|
-
end
|
|
9681
|
-
return ids
|
|
9682
|
-
end
|
|
9683
|
-
local function settingKey(instanceId)
|
|
9684
|
-
return SETTING_KEY_PREFIX .. instanceId
|
|
9685
|
-
end
|
|
9686
|
-
local function settingKeys()
|
|
9687
|
-
local _exp = computeInstanceIds()
|
|
9688
|
-
-- ▼ ReadonlyArray.map ▼
|
|
9689
|
-
local _newValue = table.create(#_exp)
|
|
9690
|
-
local _callback = function(instanceId)
|
|
9691
|
-
return settingKey(instanceId)
|
|
9692
|
-
end
|
|
9693
|
-
for _k, _v in _exp do
|
|
9694
|
-
_newValue[_k] = _callback(_v, _k - 1, _exp)
|
|
9695
|
-
end
|
|
9696
|
-
-- ▲ ReadonlyArray.map ▲
|
|
9697
|
-
return _newValue
|
|
9801
|
+
local function settingKey()
|
|
9802
|
+
return SETTING_KEY_PREFIX .. PluginSession.getInstanceId()
|
|
9698
9803
|
end
|
|
9699
9804
|
local function readSetting(key)
|
|
9700
9805
|
if not pluginRef then
|
|
@@ -9789,26 +9894,39 @@ local function handleStopRequest(key, request)
|
|
|
9789
9894
|
return nil
|
|
9790
9895
|
end
|
|
9791
9896
|
endTestIssued = true
|
|
9897
|
+
local lifecycle = transportLifecycle
|
|
9898
|
+
if lifecycle ~= nil then
|
|
9899
|
+
local closeOk, closeError = pcall(lifecycle.beforeEndTest)
|
|
9900
|
+
if not closeOk then
|
|
9901
|
+
warn(`[robloxstudio-mcp] Failed to close the play-server transport before EndTest: {tostring(closeError)}`)
|
|
9902
|
+
end
|
|
9903
|
+
end
|
|
9792
9904
|
local endOk, endErr = pcall(function()
|
|
9793
9905
|
return StudioTestService:EndTest("stopped_by_mcp")
|
|
9794
9906
|
end)
|
|
9795
9907
|
writeResult(key, request, endOk, if endOk then nil else tostring(endErr))
|
|
9796
9908
|
if not endOk then
|
|
9797
9909
|
endTestIssued = false
|
|
9910
|
+
if lifecycle ~= nil then
|
|
9911
|
+
local restoreOk, restoreError = pcall(lifecycle.afterEndTestFailure)
|
|
9912
|
+
if not restoreOk then
|
|
9913
|
+
warn(`[robloxstudio-mcp] Failed to restore the play-server transport after EndTest failed: {tostring(restoreError)}`)
|
|
9914
|
+
end
|
|
9915
|
+
end
|
|
9798
9916
|
end
|
|
9799
9917
|
end
|
|
9800
|
-
local function startMonitor()
|
|
9918
|
+
local function startMonitor(lifecycle)
|
|
9919
|
+
transportLifecycle = lifecycle
|
|
9801
9920
|
if not pluginRef then
|
|
9802
9921
|
warn("[robloxstudio-mcp] StopPlayMonitor.startMonitor called before init; skipping")
|
|
9803
9922
|
return nil
|
|
9804
9923
|
end
|
|
9805
9924
|
task.spawn(function()
|
|
9806
9925
|
while true do
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
end
|
|
9926
|
+
local myKey = settingKey()
|
|
9927
|
+
local payload = decodePayload(readSetting(myKey))
|
|
9928
|
+
if payload then
|
|
9929
|
+
handleStopRequest(myKey, payload)
|
|
9812
9930
|
end
|
|
9813
9931
|
task.wait(POLL_INTERVAL_SEC)
|
|
9814
9932
|
end
|
|
@@ -9826,10 +9944,7 @@ local function requestStop()
|
|
|
9826
9944
|
id = requestId,
|
|
9827
9945
|
requestedAt = tick(),
|
|
9828
9946
|
}
|
|
9829
|
-
local ok =
|
|
9830
|
-
for _, myKey in settingKeys() do
|
|
9831
|
-
ok = writePayload(myKey, payload) or ok
|
|
9832
|
-
end
|
|
9947
|
+
local ok = writePayload(settingKey(), payload)
|
|
9833
9948
|
return {
|
|
9834
9949
|
ok = ok,
|
|
9835
9950
|
requestId = if ok then requestId else nil,
|
|
@@ -9845,15 +9960,13 @@ local function waitForConsumption(requestId)
|
|
|
9845
9960
|
end
|
|
9846
9961
|
local start = tick()
|
|
9847
9962
|
while tick() - start < WAIT_FOR_CONSUMPTION_TIMEOUT_SEC do
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
}
|
|
9856
|
-
end
|
|
9963
|
+
local payload = decodePayload(readSetting(settingKey()))
|
|
9964
|
+
if payload and payload.kind == "result" and payload.id == requestId then
|
|
9965
|
+
return {
|
|
9966
|
+
ok = payload.ok == true,
|
|
9967
|
+
consumed = true,
|
|
9968
|
+
error = payload.error,
|
|
9969
|
+
}
|
|
9857
9970
|
end
|
|
9858
9971
|
task.wait(WAIT_POLL_SEC)
|
|
9859
9972
|
end
|
|
@@ -9867,15 +9980,14 @@ local function clearPending(requestId)
|
|
|
9867
9980
|
if not pluginRef then
|
|
9868
9981
|
return nil
|
|
9869
9982
|
end
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
end
|
|
9983
|
+
local myKey = settingKey()
|
|
9984
|
+
if requestId ~= nil then
|
|
9985
|
+
local payload = decodePayload(readSetting(myKey))
|
|
9986
|
+
if payload and payload.id ~= requestId then
|
|
9987
|
+
return nil
|
|
9876
9988
|
end
|
|
9877
|
-
writeSetting(myKey, false)
|
|
9878
9989
|
end
|
|
9990
|
+
writeSetting(myKey, false)
|
|
9879
9991
|
end
|
|
9880
9992
|
return {
|
|
9881
9993
|
init = init,
|
|
@@ -9887,7 +9999,7 @@ return {
|
|
|
9887
9999
|
]]></string>
|
|
9888
10000
|
</Properties>
|
|
9889
10001
|
</Item>
|
|
9890
|
-
<Item class="ModuleScript" referent="
|
|
10002
|
+
<Item class="ModuleScript" referent="36">
|
|
9891
10003
|
<Properties>
|
|
9892
10004
|
<string name="Name">StudioEventStream</string>
|
|
9893
10005
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -9903,6 +10015,7 @@ local MAX_TERMINAL_RESPONSES = 256
|
|
|
9903
10015
|
local STREAM_SILENCE_TIMEOUT_SECONDS = 20
|
|
9904
10016
|
local options
|
|
9905
10017
|
local active = false
|
|
10018
|
+
local shutdownSuspended = false
|
|
9906
10019
|
local generation = 0
|
|
9907
10020
|
local reconnectAttempt = 0
|
|
9908
10021
|
local streamClient
|
|
@@ -9940,8 +10053,8 @@ local function decodeMessage(message)
|
|
|
9940
10053
|
}
|
|
9941
10054
|
end
|
|
9942
10055
|
if envelope.kind == "status" then
|
|
9943
|
-
local
|
|
9944
|
-
local _condition = not (type(
|
|
10056
|
+
local _knownPeer = envelope.knownPeer
|
|
10057
|
+
local _condition = not (type(_knownPeer) == "boolean")
|
|
9945
10058
|
if not _condition then
|
|
9946
10059
|
local _mcpConnected = envelope.mcpConnected
|
|
9947
10060
|
_condition = not (type(_mcpConnected) == "boolean")
|
|
@@ -9951,7 +10064,7 @@ local function decodeMessage(message)
|
|
|
9951
10064
|
end
|
|
9952
10065
|
local _object = {
|
|
9953
10066
|
kind = "status",
|
|
9954
|
-
|
|
10067
|
+
knownPeer = envelope.knownPeer,
|
|
9955
10068
|
mcpConnected = envelope.mcpConnected,
|
|
9956
10069
|
}
|
|
9957
10070
|
local _left = "serverVersion"
|
|
@@ -9984,8 +10097,8 @@ local function decodeMessage(message)
|
|
|
9984
10097
|
local _requestId = envelope.requestId
|
|
9985
10098
|
local _condition = not (type(_requestId) == "string")
|
|
9986
10099
|
if not _condition then
|
|
9987
|
-
local
|
|
9988
|
-
_condition = not (type(
|
|
10100
|
+
local _peerId = envelope.peerId
|
|
10101
|
+
_condition = not (type(_peerId) == "string")
|
|
9989
10102
|
if not _condition then
|
|
9990
10103
|
local _target = envelope.target
|
|
9991
10104
|
_condition = not (type(_target) == "string")
|
|
@@ -10013,7 +10126,7 @@ local function decodeMessage(message)
|
|
|
10013
10126
|
return {
|
|
10014
10127
|
kind = "request",
|
|
10015
10128
|
requestId = envelope.requestId,
|
|
10016
|
-
|
|
10129
|
+
peerId = envelope.peerId,
|
|
10017
10130
|
target = envelope.target,
|
|
10018
10131
|
endpoint = envelope.endpoint,
|
|
10019
10132
|
data = data,
|
|
@@ -10035,6 +10148,20 @@ local function closeCurrentStream()
|
|
|
10035
10148
|
end)
|
|
10036
10149
|
end
|
|
10037
10150
|
end
|
|
10151
|
+
local function disconnectSession(currentOptions)
|
|
10152
|
+
pcall(function()
|
|
10153
|
+
return HttpService:RequestAsync({
|
|
10154
|
+
Url = `{currentOptions.serverUrl}/disconnect`,
|
|
10155
|
+
Method = "POST",
|
|
10156
|
+
Headers = {
|
|
10157
|
+
["Content-Type"] = "application/json",
|
|
10158
|
+
},
|
|
10159
|
+
Body = HttpService:JSONEncode({
|
|
10160
|
+
peerId = PluginSession.peerId,
|
|
10161
|
+
}),
|
|
10162
|
+
})
|
|
10163
|
+
end)
|
|
10164
|
+
end
|
|
10038
10165
|
local function responseRetryDelay(attempt)
|
|
10039
10166
|
return math.min(INITIAL_RESPONSE_RETRY_DELAY_SECONDS * math.pow(2, math.max(attempt - 1, 0)), MAX_RESPONSE_RETRY_DELAY_SECONDS)
|
|
10040
10167
|
end
|
|
@@ -10351,15 +10478,23 @@ local function parseReadyResponse(body)
|
|
|
10351
10478
|
if not _condition then
|
|
10352
10479
|
_condition = value.assignedRole == ""
|
|
10353
10480
|
if not _condition then
|
|
10354
|
-
local
|
|
10355
|
-
_condition = not (type(
|
|
10481
|
+
local _peerId = value.peerId
|
|
10482
|
+
_condition = not (type(_peerId) == "string")
|
|
10356
10483
|
if not _condition then
|
|
10357
|
-
_condition = value.
|
|
10484
|
+
_condition = value.peerId == ""
|
|
10358
10485
|
if not _condition then
|
|
10359
|
-
local
|
|
10360
|
-
_condition = not (type(
|
|
10486
|
+
local _instanceId = value.instanceId
|
|
10487
|
+
_condition = not (type(_instanceId) == "string")
|
|
10361
10488
|
if not _condition then
|
|
10362
|
-
_condition = value.
|
|
10489
|
+
_condition = value.instanceId == ""
|
|
10490
|
+
if not _condition then
|
|
10491
|
+
local _condition_1 = value.multiplayerGroupId ~= nil
|
|
10492
|
+
if _condition_1 then
|
|
10493
|
+
local _multiplayerGroupId = value.multiplayerGroupId
|
|
10494
|
+
_condition_1 = not (type(_multiplayerGroupId) == "string")
|
|
10495
|
+
end
|
|
10496
|
+
_condition = _condition_1
|
|
10497
|
+
end
|
|
10363
10498
|
end
|
|
10364
10499
|
end
|
|
10365
10500
|
end
|
|
@@ -10372,8 +10507,9 @@ local function parseReadyResponse(body)
|
|
|
10372
10507
|
return {
|
|
10373
10508
|
success = true,
|
|
10374
10509
|
assignedRole = value.assignedRole,
|
|
10510
|
+
peerId = value.peerId,
|
|
10375
10511
|
instanceId = value.instanceId,
|
|
10376
|
-
|
|
10512
|
+
multiplayerGroupId = value.multiplayerGroupId,
|
|
10377
10513
|
}
|
|
10378
10514
|
end
|
|
10379
10515
|
local refresh
|
|
@@ -10389,10 +10525,10 @@ function connect(expectedGeneration)
|
|
|
10389
10525
|
})
|
|
10390
10526
|
task.spawn(function()
|
|
10391
10527
|
local instanceId = PluginSession.getInstanceId()
|
|
10528
|
+
local multiplayerGroupId = PluginSession.getMultiplayerGroupId()
|
|
10392
10529
|
local readyUrl = `{currentOptions.serverUrl}/ready`
|
|
10393
|
-
local
|
|
10394
|
-
local readyPayload = PluginSession.createReadyPayload(PluginSession.
|
|
10395
|
-
readyPayload.pluginReady = true
|
|
10530
|
+
local transportRole = PluginSession.getRole()
|
|
10531
|
+
local readyPayload = PluginSession.createReadyPayload(PluginSession.peerId, transportRole, instanceId, multiplayerGroupId)
|
|
10396
10532
|
if not active or generation ~= expectedGeneration or options ~= currentOptions then
|
|
10397
10533
|
return nil
|
|
10398
10534
|
end
|
|
@@ -10409,12 +10545,12 @@ function connect(expectedGeneration)
|
|
|
10409
10545
|
if not active or generation ~= expectedGeneration or options ~= currentOptions then
|
|
10410
10546
|
return nil
|
|
10411
10547
|
end
|
|
10412
|
-
local readyLogKey = `{currentOptions.serverUrl}|{
|
|
10548
|
+
local readyLogKey = `{currentOptions.serverUrl}|{PluginSession.peerId}`
|
|
10413
10549
|
if not readyOk then
|
|
10414
10550
|
local detail = HttpDiagnostics.formatRequestFailure(readyUrl, false, readyResult)
|
|
10415
10551
|
if not (readyFailureLogKeys[readyLogKey] ~= nil) then
|
|
10416
10552
|
readyFailureLogKeys[readyLogKey] = true
|
|
10417
|
-
warn(`[robloxstudio-mcp] /ready failed for {instanceId}/{
|
|
10553
|
+
warn(`[robloxstudio-mcp] /ready failed for {instanceId}/{transportRole}: {detail}`)
|
|
10418
10554
|
end
|
|
10419
10555
|
scheduleReconnect(expectedGeneration, detail)
|
|
10420
10556
|
return nil
|
|
@@ -10423,14 +10559,14 @@ function connect(expectedGeneration)
|
|
|
10423
10559
|
local detail = HttpDiagnostics.formatRequestFailure(readyUrl, true, readyResult)
|
|
10424
10560
|
if not (readyFailureLogKeys[readyLogKey] ~= nil) then
|
|
10425
10561
|
readyFailureLogKeys[readyLogKey] = true
|
|
10426
|
-
warn(`[robloxstudio-mcp] /ready rejected for {instanceId}/{
|
|
10562
|
+
warn(`[robloxstudio-mcp] /ready rejected for {instanceId}/{transportRole}: {detail}`)
|
|
10427
10563
|
end
|
|
10428
10564
|
scheduleReconnect(expectedGeneration, detail, readyResult.StatusCode == 409)
|
|
10429
10565
|
return nil
|
|
10430
10566
|
end
|
|
10431
10567
|
local readyData = parseReadyResponse(readyResult.Body)
|
|
10432
|
-
if readyData == nil then
|
|
10433
|
-
scheduleReconnect(expectedGeneration, "Invalid /ready response: expected the
|
|
10568
|
+
if readyData == nil or readyData.peerId ~= PluginSession.peerId or readyData.instanceId ~= instanceId or readyData.multiplayerGroupId ~= multiplayerGroupId then
|
|
10569
|
+
scheduleReconnect(expectedGeneration, "Invalid /ready response: expected the registered Peer topology")
|
|
10434
10570
|
return nil
|
|
10435
10571
|
end
|
|
10436
10572
|
if readyFailureLogKeys[readyLogKey] ~= nil then
|
|
@@ -10442,7 +10578,7 @@ function connect(expectedGeneration)
|
|
|
10442
10578
|
end)
|
|
10443
10579
|
local createOk, createdClient = pcall(function()
|
|
10444
10580
|
return HttpService:CreateWebStreamClient(Enum.WebStreamClientType.SSE, {
|
|
10445
|
-
Url = `{currentOptions.serverUrl}/events?
|
|
10581
|
+
Url = `{currentOptions.serverUrl}/events?peerId={PluginSession.peerId}`,
|
|
10446
10582
|
Method = "GET",
|
|
10447
10583
|
Headers = {
|
|
10448
10584
|
Accept = "text/event-stream",
|
|
@@ -10502,7 +10638,7 @@ function connect(expectedGeneration)
|
|
|
10502
10638
|
invokeCallback("event stream status", function()
|
|
10503
10639
|
return currentOptions.onStatus(event)
|
|
10504
10640
|
end)
|
|
10505
|
-
if not event.
|
|
10641
|
+
if not event.knownPeer then
|
|
10506
10642
|
refresh()
|
|
10507
10643
|
end
|
|
10508
10644
|
end), createdClient.Error:Connect(function(statusCode, message)
|
|
@@ -10523,11 +10659,12 @@ function connect(expectedGeneration)
|
|
|
10523
10659
|
end
|
|
10524
10660
|
local stop
|
|
10525
10661
|
local function start(newOptions)
|
|
10526
|
-
if active then
|
|
10662
|
+
if active or shutdownSuspended then
|
|
10527
10663
|
stop()
|
|
10528
10664
|
end
|
|
10529
10665
|
options = newOptions
|
|
10530
10666
|
active = true
|
|
10667
|
+
shutdownSuspended = false
|
|
10531
10668
|
reconnectAttempt = 0
|
|
10532
10669
|
generation += 1
|
|
10533
10670
|
connect(generation)
|
|
@@ -10541,12 +10678,39 @@ function refresh()
|
|
|
10541
10678
|
reconnectAttempt = 0
|
|
10542
10679
|
connect(generation)
|
|
10543
10680
|
end
|
|
10681
|
+
-- EndTest can tear down a play DataModel without giving Plugin.Unloading a
|
|
10682
|
+
-- usable execution window. Release the native WebStreamClient before the
|
|
10683
|
+
-- yielding unregister request, but retain local request state and connection
|
|
10684
|
+
-- options so a failed EndTest can register again and reconnect.
|
|
10685
|
+
local function suspendForShutdown()
|
|
10686
|
+
local currentOptions = options
|
|
10687
|
+
if not active or currentOptions == nil then
|
|
10688
|
+
return nil
|
|
10689
|
+
end
|
|
10690
|
+
active = false
|
|
10691
|
+
shutdownSuspended = true
|
|
10692
|
+
generation += 1
|
|
10693
|
+
reconnectAttempt = 0
|
|
10694
|
+
closeCurrentStream()
|
|
10695
|
+
disconnectSession(currentOptions)
|
|
10696
|
+
end
|
|
10697
|
+
local function resumeAfterShutdownFailure()
|
|
10698
|
+
if not shutdownSuspended or options == nil then
|
|
10699
|
+
return nil
|
|
10700
|
+
end
|
|
10701
|
+
shutdownSuspended = false
|
|
10702
|
+
active = true
|
|
10703
|
+
generation += 1
|
|
10704
|
+
reconnectAttempt = 0
|
|
10705
|
+
connect(generation)
|
|
10706
|
+
end
|
|
10544
10707
|
function stop()
|
|
10545
|
-
if not active then
|
|
10708
|
+
if not active and not shutdownSuspended then
|
|
10546
10709
|
return nil
|
|
10547
10710
|
end
|
|
10548
10711
|
local currentOptions = options
|
|
10549
10712
|
active = false
|
|
10713
|
+
shutdownSuspended = false
|
|
10550
10714
|
generation += 1
|
|
10551
10715
|
for _, inFlight in inFlightRequests do
|
|
10552
10716
|
inFlight.cancelled = true
|
|
@@ -10558,30 +10722,72 @@ function stop()
|
|
|
10558
10722
|
options = nil
|
|
10559
10723
|
reconnectAttempt = 0
|
|
10560
10724
|
if currentOptions ~= nil then
|
|
10561
|
-
|
|
10562
|
-
return HttpService:RequestAsync({
|
|
10563
|
-
Url = `{currentOptions.serverUrl}/disconnect`,
|
|
10564
|
-
Method = "POST",
|
|
10565
|
-
Headers = {
|
|
10566
|
-
["Content-Type"] = "application/json",
|
|
10567
|
-
},
|
|
10568
|
-
Body = HttpService:JSONEncode({
|
|
10569
|
-
pluginSessionId = PluginSession.id,
|
|
10570
|
-
timestamp = tick(),
|
|
10571
|
-
}),
|
|
10572
|
-
})
|
|
10573
|
-
end)
|
|
10725
|
+
disconnectSession(currentOptions)
|
|
10574
10726
|
end
|
|
10575
10727
|
end
|
|
10576
10728
|
return {
|
|
10577
10729
|
start = start,
|
|
10578
10730
|
refresh = refresh,
|
|
10731
|
+
suspendForShutdown = suspendForShutdown,
|
|
10732
|
+
resumeAfterShutdownFailure = resumeAfterShutdownFailure,
|
|
10579
10733
|
stop = stop,
|
|
10580
10734
|
}
|
|
10581
10735
|
]]></string>
|
|
10582
10736
|
</Properties>
|
|
10583
10737
|
</Item>
|
|
10584
|
-
<Item class="ModuleScript" referent="
|
|
10738
|
+
<Item class="ModuleScript" referent="37">
|
|
10739
|
+
<Properties>
|
|
10740
|
+
<string name="Name">TopologyId</string>
|
|
10741
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
10742
|
+
local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
10743
|
+
local HttpService = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services").HttpService
|
|
10744
|
+
local BASE36_DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
10745
|
+
local TOKEN_LENGTH = 6
|
|
10746
|
+
local TOKEN_MODULUS = 2_176_782_336
|
|
10747
|
+
local function tokenFromNumber(value)
|
|
10748
|
+
local remaining = math.floor(math.abs(value)) % TOKEN_MODULUS
|
|
10749
|
+
local token = ""
|
|
10750
|
+
for index = 0, TOKEN_LENGTH - 1 do
|
|
10751
|
+
local digit = remaining % 36
|
|
10752
|
+
local _arg0 = digit + 1
|
|
10753
|
+
local _arg1 = digit + 1
|
|
10754
|
+
token = string.sub(BASE36_DIGITS, _arg0, _arg1) .. token
|
|
10755
|
+
remaining = math.floor(remaining / 36)
|
|
10756
|
+
end
|
|
10757
|
+
return `{string.sub(token, 1, 3)}-{string.sub(token, 4, 6)}`
|
|
10758
|
+
end
|
|
10759
|
+
local function formatId(kind, value)
|
|
10760
|
+
return `{kind}:{tokenFromNumber(value)}`
|
|
10761
|
+
end
|
|
10762
|
+
local function randomValue()
|
|
10763
|
+
local guidPrefix = string.sub(HttpService:GenerateGUID(false), 1, 8)
|
|
10764
|
+
local _condition = tonumber(guidPrefix, 16)
|
|
10765
|
+
if _condition == nil then
|
|
10766
|
+
_condition = 0
|
|
10767
|
+
end
|
|
10768
|
+
return _condition
|
|
10769
|
+
end
|
|
10770
|
+
local function createPeerId()
|
|
10771
|
+
return formatId("peer", randomValue())
|
|
10772
|
+
end
|
|
10773
|
+
local function createInstanceId()
|
|
10774
|
+
return formatId("instance", randomValue())
|
|
10775
|
+
end
|
|
10776
|
+
local function currentProcessInstanceId()
|
|
10777
|
+
-- Roblox exposes process uptime through os.clock(). Quantizing the recovered
|
|
10778
|
+
-- launch wall-clock to 10 ms gives every VM in one Studio process the same ID.
|
|
10779
|
+
local launchTick = math.round((DateTime.now().UnixTimestampMillis - os.clock() * 1000) / 10)
|
|
10780
|
+
return formatId("instance", launchTick)
|
|
10781
|
+
end
|
|
10782
|
+
return {
|
|
10783
|
+
createPeerId = createPeerId,
|
|
10784
|
+
createInstanceId = createInstanceId,
|
|
10785
|
+
currentProcessInstanceId = currentProcessInstanceId,
|
|
10786
|
+
}
|
|
10787
|
+
]]></string>
|
|
10788
|
+
</Properties>
|
|
10789
|
+
</Item>
|
|
10790
|
+
<Item class="ModuleScript" referent="38">
|
|
10585
10791
|
<Properties>
|
|
10586
10792
|
<string name="Name">UI</string>
|
|
10587
10793
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -11133,7 +11339,7 @@ return {
|
|
|
11133
11339
|
]]></string>
|
|
11134
11340
|
</Properties>
|
|
11135
11341
|
</Item>
|
|
11136
|
-
<Item class="ModuleScript" referent="
|
|
11342
|
+
<Item class="ModuleScript" referent="39">
|
|
11137
11343
|
<Properties>
|
|
11138
11344
|
<string name="Name">Utils</string>
|
|
11139
11345
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -11974,11 +12180,11 @@ return {
|
|
|
11974
12180
|
</Properties>
|
|
11975
12181
|
</Item>
|
|
11976
12182
|
</Item>
|
|
11977
|
-
<Item class="Folder" referent="
|
|
12183
|
+
<Item class="Folder" referent="44">
|
|
11978
12184
|
<Properties>
|
|
11979
12185
|
<string name="Name">include</string>
|
|
11980
12186
|
</Properties>
|
|
11981
|
-
<Item class="ModuleScript" referent="
|
|
12187
|
+
<Item class="ModuleScript" referent="40">
|
|
11982
12188
|
<Properties>
|
|
11983
12189
|
<string name="Name">LibMP</string>
|
|
11984
12190
|
<string name="Source"><![CDATA[-- =============================================================================
|
|
@@ -168362,7 +168568,7 @@ return LibMP
|
|
|
168362
168568
|
]]></string>
|
|
168363
168569
|
</Properties>
|
|
168364
168570
|
</Item>
|
|
168365
|
-
<Item class="ModuleScript" referent="
|
|
168571
|
+
<Item class="ModuleScript" referent="41">
|
|
168366
168572
|
<Properties>
|
|
168367
168573
|
<string name="Name">Promise</string>
|
|
168368
168574
|
<string name="Source"><![CDATA[--[[
|
|
@@ -170436,7 +170642,7 @@ return Promise
|
|
|
170436
170642
|
]]></string>
|
|
170437
170643
|
</Properties>
|
|
170438
170644
|
</Item>
|
|
170439
|
-
<Item class="ModuleScript" referent="
|
|
170645
|
+
<Item class="ModuleScript" referent="42">
|
|
170440
170646
|
<Properties>
|
|
170441
170647
|
<string name="Name">RuntimeLib</string>
|
|
170442
170648
|
<string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
|
|
@@ -170703,15 +170909,15 @@ return TS
|
|
|
170703
170909
|
</Properties>
|
|
170704
170910
|
</Item>
|
|
170705
170911
|
</Item>
|
|
170706
|
-
<Item class="Folder" referent="
|
|
170912
|
+
<Item class="Folder" referent="45">
|
|
170707
170913
|
<Properties>
|
|
170708
170914
|
<string name="Name">node_modules</string>
|
|
170709
170915
|
</Properties>
|
|
170710
|
-
<Item class="Folder" referent="
|
|
170916
|
+
<Item class="Folder" referent="46">
|
|
170711
170917
|
<Properties>
|
|
170712
170918
|
<string name="Name">@rbxts</string>
|
|
170713
170919
|
</Properties>
|
|
170714
|
-
<Item class="ModuleScript" referent="
|
|
170920
|
+
<Item class="ModuleScript" referent="43">
|
|
170715
170921
|
<Properties>
|
|
170716
170922
|
<string name="Name">services</string>
|
|
170717
170923
|
<string name="Source"><![CDATA[return setmetatable({}, {
|