@chrrxs/robloxstudio-mcp 3.0.0 → 3.0.2
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 +9401 -9047
- package/package.json +2 -2
- package/studio-plugin/MCPPlugin.rbxmx +400 -97
- package/studio-plugin/.Carbon.rbxm.lock +0 -0
- package/studio-plugin/Carbon.rbxm +0 -0
- package/studio-plugin/INSTALLATION.md +0 -170
- package/studio-plugin/MCPInspectorPlugin.rbxmx +0 -169759
- package/studio-plugin/default.project.json +0 -19
- package/studio-plugin/dev.project.json +0 -23
- package/studio-plugin/include/LibMP.lua +0 -156378
- package/studio-plugin/inspector-icon.png +0 -0
- package/studio-plugin/package-lock.json +0 -706
- package/studio-plugin/package.json +0 -19
- package/studio-plugin/plugin.json +0 -10
- package/studio-plugin/src/modules/AssetSanitizationPolicy.ts +0 -127
- package/studio-plugin/src/modules/ClientBroker.ts +0 -450
- package/studio-plugin/src/modules/Communication.ts +0 -601
- package/studio-plugin/src/modules/EvalBridges.ts +0 -255
- package/studio-plugin/src/modules/HttpDiagnostics.ts +0 -50
- package/studio-plugin/src/modules/LuauExec.ts +0 -403
- package/studio-plugin/src/modules/Recording.ts +0 -28
- package/studio-plugin/src/modules/RenderMonitor.ts +0 -60
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +0 -210
- package/studio-plugin/src/modules/ServerUrlSettings.ts +0 -117
- package/studio-plugin/src/modules/State.ts +0 -39
- package/studio-plugin/src/modules/StopPlayMonitor.ts +0 -267
- package/studio-plugin/src/modules/UI.ts +0 -597
- package/studio-plugin/src/modules/Utils.ts +0 -527
- package/studio-plugin/src/modules/handlers/AssetHandlers.ts +0 -391
- package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +0 -460
- package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +0 -170
- package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +0 -149
- package/studio-plugin/src/modules/handlers/GenerateModelHandlers.ts +0 -168
- package/studio-plugin/src/modules/handlers/InputHandlers.ts +0 -163
- package/studio-plugin/src/modules/handlers/LogHandlers.ts +0 -14
- package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +0 -44
- package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +0 -96
- package/studio-plugin/src/modules/handlers/MicroProfilerHandlers.ts +0 -1263
- package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +0 -62
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +0 -716
- package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +0 -216
- package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +0 -531
- package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +0 -386
- package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +0 -172
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +0 -350
- package/studio-plugin/src/server/index.server.ts +0 -135
- package/studio-plugin/src/types/index.d.ts +0 -57
- package/studio-plugin/tsconfig.json +0 -20
|
@@ -271,6 +271,7 @@ local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandl
|
|
|
271
271
|
local SceneAnalysisHandlers = TS.import(script, script.Parent, "handlers", "SceneAnalysisHandlers")
|
|
272
272
|
local CaptureHandlers = TS.import(script, script.Parent, "handlers", "CaptureHandlers")
|
|
273
273
|
local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandlers")
|
|
274
|
+
local MetadataHandlers = TS.import(script, script.Parent, "handlers", "MetadataHandlers")
|
|
274
275
|
local EvalRuntimeHandlers = TS.import(script, script.Parent, "handlers", "EvalRuntimeHandlers")
|
|
275
276
|
local BreakpointHandlers = TS.import(script, script.Parent, "handlers", "BreakpointHandlers")
|
|
276
277
|
local ScriptProfilerHandlers = TS.import(script, script.Parent, "handlers", "ScriptProfilerHandlers")
|
|
@@ -351,6 +352,7 @@ local CLIENT_BROKER_ALLOWED_ENDPOINTS = {
|
|
|
351
352
|
["/api/capture-begin"] = true,
|
|
352
353
|
["/api/simulate-mouse-input"] = true,
|
|
353
354
|
["/api/simulate-keyboard-input"] = true,
|
|
355
|
+
["/api/focus-viewport"] = true,
|
|
354
356
|
}
|
|
355
357
|
-- Throttle re-ready calls per proxyId so a brief window of unknownInstance
|
|
356
358
|
-- polls doesn't cause a re-register stampede.
|
|
@@ -559,6 +561,9 @@ local function setupClientBroker()
|
|
|
559
561
|
if payload and payload.endpoint == "/api/simulate-keyboard-input" then
|
|
560
562
|
return InputHandlers.simulateKeyboardInput(payload.data or {})
|
|
561
563
|
end
|
|
564
|
+
if payload and payload.endpoint == "/api/focus-viewport" then
|
|
565
|
+
return MetadataHandlers.focusViewport(payload.data or {})
|
|
566
|
+
end
|
|
562
567
|
if payload and payload.endpoint == "/api/execute-luau" then
|
|
563
568
|
return handleExecuteLuau(payload.data)
|
|
564
569
|
end
|
|
@@ -611,15 +616,33 @@ local function pollProxy(proxyId, player, rf)
|
|
|
611
616
|
unregisterProxy(player)
|
|
612
617
|
break
|
|
613
618
|
end
|
|
619
|
+
local nextPollDelay = 0.5
|
|
614
620
|
local ok, res = pcall(function()
|
|
615
|
-
|
|
616
|
-
Url = `{mcpUrl}/poll?pluginSessionId={proxyId}`,
|
|
621
|
+
local requestOptions = {
|
|
622
|
+
Url = `{mcpUrl}/poll?pluginSessionId={proxyId}&pollMode=long`,
|
|
617
623
|
Method = "GET",
|
|
618
624
|
Headers = {
|
|
619
625
|
["Content-Type"] = "application/json",
|
|
620
626
|
},
|
|
621
|
-
|
|
627
|
+
Timeout = State.POLL_REQUEST_TIMEOUT_SECONDS,
|
|
628
|
+
}
|
|
629
|
+
return HttpService:RequestAsync(requestOptions)
|
|
622
630
|
end)
|
|
631
|
+
-- RequestAsync may yield for the full long-poll window. Never apply a
|
|
632
|
+
-- stale response to a player whose proxy was removed or replaced.
|
|
633
|
+
local _player = player
|
|
634
|
+
local currentProxy = proxyByPlayer[_player]
|
|
635
|
+
local _condition_1 = player.Parent == nil
|
|
636
|
+
if not _condition_1 then
|
|
637
|
+
local _result = currentProxy
|
|
638
|
+
if _result ~= nil then
|
|
639
|
+
_result = _result.pluginSessionId
|
|
640
|
+
end
|
|
641
|
+
_condition_1 = _result ~= proxyId
|
|
642
|
+
end
|
|
643
|
+
if _condition_1 then
|
|
644
|
+
break
|
|
645
|
+
end
|
|
623
646
|
if ok and res and (res.Success or res.StatusCode == 503) then
|
|
624
647
|
local okJson, body = pcall(function()
|
|
625
648
|
return HttpService:JSONDecode(res.Body)
|
|
@@ -669,9 +692,17 @@ local function pollProxy(proxyId, player, rf)
|
|
|
669
692
|
response = response,
|
|
670
693
|
})
|
|
671
694
|
end
|
|
695
|
+
if res.Success and body.pollMode == "long" and body.knownInstance ~= false and body.request == nil then
|
|
696
|
+
nextPollDelay = 0
|
|
697
|
+
end
|
|
672
698
|
end
|
|
673
699
|
end
|
|
674
|
-
|
|
700
|
+
if nextPollDelay > 0 then
|
|
701
|
+
task.wait(nextPollDelay)
|
|
702
|
+
else
|
|
703
|
+
-- Yield one scheduler turn before replacing a completed long poll.
|
|
704
|
+
task.wait()
|
|
705
|
+
end
|
|
675
706
|
end
|
|
676
707
|
end
|
|
677
708
|
local function registerProxy(player, rf)
|
|
@@ -826,6 +857,7 @@ local lastVersionMismatchWarningKey
|
|
|
826
857
|
local lastReadyInstanceId
|
|
827
858
|
local readyFailureLogKeys = {}
|
|
828
859
|
local retryingDuplicateReady = false
|
|
860
|
+
local pollEpoch = 0
|
|
829
861
|
-- Cache the published place name from MarketplaceService:GetProductInfo so
|
|
830
862
|
-- /ready can carry a friendly identifier (e.g. "Natural Disasters") distinct
|
|
831
863
|
-- from game.Name (the DataModel name, often "Place1" in edit). We only fetch
|
|
@@ -885,6 +917,8 @@ local routeMap = {
|
|
|
885
917
|
["/api/delete-script-lines"] = ScriptHandlers.deleteScriptLines,
|
|
886
918
|
["/api/get-attributes"] = MetadataHandlers.getAttributes,
|
|
887
919
|
["/api/get-selection"] = MetadataHandlers.getSelection,
|
|
920
|
+
["/api/set-selection"] = MetadataHandlers.setSelection,
|
|
921
|
+
["/api/focus-viewport"] = MetadataHandlers.focusViewport,
|
|
888
922
|
["/api/execute-luau"] = MetadataHandlers.executeLuau,
|
|
889
923
|
["/api/eval-runtime"] = EvalRuntimeHandlers.evalRuntime,
|
|
890
924
|
["/api/start-playtest"] = TestHandlers.startPlaytest,
|
|
@@ -1108,16 +1142,24 @@ local function pollForRequests()
|
|
|
1108
1142
|
if conn.isPolling then
|
|
1109
1143
|
return nil
|
|
1110
1144
|
end
|
|
1145
|
+
local epoch = pollEpoch
|
|
1111
1146
|
conn.isPolling = true
|
|
1112
1147
|
local success, result = pcall(function()
|
|
1113
|
-
|
|
1114
|
-
Url = `{conn.serverUrl}/poll?pluginSessionId={pluginSessionId}`,
|
|
1148
|
+
local requestOptions = {
|
|
1149
|
+
Url = `{conn.serverUrl}/poll?pluginSessionId={pluginSessionId}&pollMode=long`,
|
|
1115
1150
|
Method = "GET",
|
|
1116
1151
|
Headers = {
|
|
1117
1152
|
["Content-Type"] = "application/json",
|
|
1118
1153
|
},
|
|
1119
|
-
|
|
1154
|
+
Timeout = State.POLL_REQUEST_TIMEOUT_SECONDS,
|
|
1155
|
+
}
|
|
1156
|
+
return HttpService:RequestAsync(requestOptions)
|
|
1120
1157
|
end)
|
|
1158
|
+
-- A held request can finish after deactivate/reactivate. Only the current
|
|
1159
|
+
-- lifecycle epoch may mutate connection state or clear a newer poll's lock.
|
|
1160
|
+
if epoch ~= pollEpoch or not conn.isActive then
|
|
1161
|
+
return nil
|
|
1162
|
+
end
|
|
1121
1163
|
conn.isPolling = false
|
|
1122
1164
|
local ui = UI.getElements()
|
|
1123
1165
|
UI.updateToolbarIcon()
|
|
@@ -1126,6 +1168,12 @@ local function pollForRequests()
|
|
|
1126
1168
|
conn.currentRetryDelay = 0.5
|
|
1127
1169
|
conn.lastSuccessfulConnection = tick()
|
|
1128
1170
|
local data = HttpService:JSONDecode(result.Body)
|
|
1171
|
+
conn.lastPoll = tick()
|
|
1172
|
+
if result.Success and data.pollMode == "long" and data.knownInstance ~= false and data.request == nil then
|
|
1173
|
+
-- The server held this request until work or timeout, so immediately
|
|
1174
|
+
-- replace it instead of reintroducing the legacy 500 ms idle gap.
|
|
1175
|
+
conn.lastPoll = 0
|
|
1176
|
+
end
|
|
1129
1177
|
local mcpConnected = data.mcpConnected == true
|
|
1130
1178
|
conn.lastHttpOk = true
|
|
1131
1179
|
conn.lastMcpOk = mcpConnected
|
|
@@ -1216,6 +1264,7 @@ local function pollForRequests()
|
|
|
1216
1264
|
end)
|
|
1217
1265
|
end
|
|
1218
1266
|
elseif conn.isActive then
|
|
1267
|
+
conn.lastPoll = tick()
|
|
1219
1268
|
conn.consecutiveFailures += 1
|
|
1220
1269
|
if conn.consecutiveFailures > 1 then
|
|
1221
1270
|
conn.currentRetryDelay = math.min(conn.currentRetryDelay * conn.retryBackoffMultiplier, conn.maxRetryDelay)
|
|
@@ -1280,6 +1329,9 @@ local deactivatePlugin
|
|
|
1280
1329
|
local function activatePlugin()
|
|
1281
1330
|
local conn = State.getActiveConnection()
|
|
1282
1331
|
local ui = UI.getElements()
|
|
1332
|
+
pollEpoch += 1
|
|
1333
|
+
conn.isPolling = false
|
|
1334
|
+
conn.lastPoll = 0
|
|
1283
1335
|
conn.isActive = true
|
|
1284
1336
|
conn.consecutiveFailures = 0
|
|
1285
1337
|
conn.currentRetryDelay = 0.5
|
|
@@ -1328,6 +1380,8 @@ local function activatePlugin()
|
|
|
1328
1380
|
end
|
|
1329
1381
|
function deactivatePlugin()
|
|
1330
1382
|
local conn = State.getActiveConnection()
|
|
1383
|
+
pollEpoch += 1
|
|
1384
|
+
conn.isPolling = false
|
|
1331
1385
|
conn.isActive = false
|
|
1332
1386
|
conn.lastMcpOk = false
|
|
1333
1387
|
UI.updateUIState()
|
|
@@ -1521,9 +1575,9 @@ local function computeBridgeStamp()
|
|
|
1521
1575
|
for i = 1, #combined do
|
|
1522
1576
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1523
1577
|
end
|
|
1524
|
-
-- "3.0.
|
|
1578
|
+
-- "3.0.2" is replaced with the package version at package time
|
|
1525
1579
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1526
|
-
return `{tostring(h)}-3.0.
|
|
1580
|
+
return `{tostring(h)}-3.0.2`
|
|
1527
1581
|
end
|
|
1528
1582
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1529
1583
|
local function setSource(scriptInst, source)
|
|
@@ -3593,11 +3647,14 @@ local function getAttributes(requestData)
|
|
|
3593
3647
|
}
|
|
3594
3648
|
count += 1
|
|
3595
3649
|
end
|
|
3596
|
-
|
|
3650
|
+
local response = {
|
|
3597
3651
|
instancePath = instancePath,
|
|
3598
|
-
attributes = serializedAttributes,
|
|
3599
3652
|
count = count,
|
|
3600
3653
|
}
|
|
3654
|
+
if count > 0 then
|
|
3655
|
+
response.attributes = serializedAttributes
|
|
3656
|
+
end
|
|
3657
|
+
return response
|
|
3601
3658
|
end)
|
|
3602
3659
|
if success then
|
|
3603
3660
|
return result
|
|
@@ -3638,6 +3695,180 @@ local function getSelection(_requestData)
|
|
|
3638
3695
|
message = `{#selection} object(s) selected`,
|
|
3639
3696
|
}
|
|
3640
3697
|
end
|
|
3698
|
+
-- The counterpart to getSelection: this sets it. Selecting what the agent just
|
|
3699
|
+
-- built shows the user the result and puts the instance under Studio's own
|
|
3700
|
+
-- move/scale handles, which is why it is a tool and not left to execute_luau.
|
|
3701
|
+
local function setSelection(requestData)
|
|
3702
|
+
local rawPaths = requestData.paths
|
|
3703
|
+
if not (type(rawPaths) == "table") then
|
|
3704
|
+
return {
|
|
3705
|
+
error = "paths is required (an empty array clears in set mode)",
|
|
3706
|
+
}
|
|
3707
|
+
end
|
|
3708
|
+
local _condition = (requestData.mode)
|
|
3709
|
+
if _condition == nil then
|
|
3710
|
+
_condition = "set"
|
|
3711
|
+
end
|
|
3712
|
+
local mode = _condition
|
|
3713
|
+
if mode ~= "set" and mode ~= "add" and mode ~= "remove" then
|
|
3714
|
+
return {
|
|
3715
|
+
error = `mode must be "set", "add" or "remove" (got: {mode})`,
|
|
3716
|
+
}
|
|
3717
|
+
end
|
|
3718
|
+
local paths = rawPaths
|
|
3719
|
+
if #paths == 0 and mode ~= "set" then
|
|
3720
|
+
return {
|
|
3721
|
+
error = `paths cannot be empty in {mode} mode`,
|
|
3722
|
+
}
|
|
3723
|
+
end
|
|
3724
|
+
local targets = {}
|
|
3725
|
+
local missing = {}
|
|
3726
|
+
for _, entry in paths do
|
|
3727
|
+
if not (type(entry) == "string") or entry == "" then
|
|
3728
|
+
return {
|
|
3729
|
+
error = "paths must contain non-empty instance path strings",
|
|
3730
|
+
}
|
|
3731
|
+
end
|
|
3732
|
+
local instance = getInstanceByPath(entry)
|
|
3733
|
+
if instance then
|
|
3734
|
+
table.insert(targets, instance)
|
|
3735
|
+
else
|
|
3736
|
+
table.insert(missing, entry)
|
|
3737
|
+
end
|
|
3738
|
+
end
|
|
3739
|
+
local clearsSelection = mode == "set" and #paths == 0
|
|
3740
|
+
if #targets == 0 and not clearsSelection then
|
|
3741
|
+
return {
|
|
3742
|
+
error = "No matching instances found for any path",
|
|
3743
|
+
missingPaths = missing,
|
|
3744
|
+
}
|
|
3745
|
+
end
|
|
3746
|
+
local ok, err = pcall(function()
|
|
3747
|
+
if mode == "add" then
|
|
3748
|
+
Selection:Add(targets)
|
|
3749
|
+
elseif mode == "remove" then
|
|
3750
|
+
Selection:Remove(targets)
|
|
3751
|
+
else
|
|
3752
|
+
Selection:Set(targets)
|
|
3753
|
+
end
|
|
3754
|
+
end)
|
|
3755
|
+
if not ok then
|
|
3756
|
+
return {
|
|
3757
|
+
error = `Selection.{mode} failed: {tostring(err)}`,
|
|
3758
|
+
}
|
|
3759
|
+
end
|
|
3760
|
+
return {
|
|
3761
|
+
success = true,
|
|
3762
|
+
mode = mode,
|
|
3763
|
+
selected = #targets,
|
|
3764
|
+
missingPaths = missing,
|
|
3765
|
+
message = if clearsSelection then "Selection cleared" elseif mode == "remove" then `Deselected {#targets} object(s)` else `{#targets} object(s) {if mode == "add" then "added to" else "in"} the selection`,
|
|
3766
|
+
}
|
|
3767
|
+
end
|
|
3768
|
+
-- Aims the Studio camera at an instance and frames it from a sensible angle.
|
|
3769
|
+
-- This completes the screenshot loop capture_screenshot documents: build,
|
|
3770
|
+
-- focus, screenshot. Framing distance comes from the bounding box and the
|
|
3771
|
+
-- camera's own field of view so any subject fills a similar share of frame.
|
|
3772
|
+
local function focusViewport(requestData)
|
|
3773
|
+
local instancePath = requestData.path
|
|
3774
|
+
if not (instancePath ~= "" and instancePath) then
|
|
3775
|
+
return {
|
|
3776
|
+
error = "path is required (the instance to frame)",
|
|
3777
|
+
}
|
|
3778
|
+
end
|
|
3779
|
+
local instance = getInstanceByPath(instancePath)
|
|
3780
|
+
if not instance then
|
|
3781
|
+
return {
|
|
3782
|
+
error = `Instance not found: {instancePath}`,
|
|
3783
|
+
}
|
|
3784
|
+
end
|
|
3785
|
+
local workspace = game:GetService("Workspace")
|
|
3786
|
+
local camera = workspace.CurrentCamera
|
|
3787
|
+
if not camera then
|
|
3788
|
+
return {
|
|
3789
|
+
error = "Workspace.CurrentCamera is unavailable right now",
|
|
3790
|
+
}
|
|
3791
|
+
end
|
|
3792
|
+
local _condition = tonumber(requestData.padding)
|
|
3793
|
+
if _condition == nil then
|
|
3794
|
+
_condition = 1
|
|
3795
|
+
end
|
|
3796
|
+
local padding = _condition
|
|
3797
|
+
if padding <= 0 or padding > 10 then
|
|
3798
|
+
return {
|
|
3799
|
+
error = "padding must be greater than 0 and at most 10",
|
|
3800
|
+
}
|
|
3801
|
+
end
|
|
3802
|
+
local compassOverride = if requestData.from == nil then nil else tonumber(requestData.from)
|
|
3803
|
+
if requestData.from ~= nil and compassOverride == nil then
|
|
3804
|
+
return {
|
|
3805
|
+
error = "from must be a compass angle in degrees",
|
|
3806
|
+
}
|
|
3807
|
+
end
|
|
3808
|
+
local elevationOverride = if requestData.angleY == nil then nil else tonumber(requestData.angleY)
|
|
3809
|
+
if requestData.angleY ~= nil and (elevationOverride == nil or elevationOverride < -89 or elevationOverride > 89) then
|
|
3810
|
+
return {
|
|
3811
|
+
error = "angleY must be between -89 and 89",
|
|
3812
|
+
}
|
|
3813
|
+
end
|
|
3814
|
+
local ok, err = pcall(function()
|
|
3815
|
+
local boundsCF
|
|
3816
|
+
local boundsSize
|
|
3817
|
+
if instance:IsA("Model") then
|
|
3818
|
+
boundsCF, boundsSize = instance:GetBoundingBox()
|
|
3819
|
+
elseif instance:IsA("BasePart") then
|
|
3820
|
+
boundsCF = instance.CFrame
|
|
3821
|
+
boundsSize = instance.Size
|
|
3822
|
+
else
|
|
3823
|
+
return error(`Cannot frame a {instance.ClassName}: it has no 3D bounding box. Frame a part or model instead.`)
|
|
3824
|
+
end
|
|
3825
|
+
local center = boundsCF.Position
|
|
3826
|
+
local direction = camera.CFrame.LookVector * (-1)
|
|
3827
|
+
local currentHorizontal = Vector3.new(direction.X, 0, direction.Z)
|
|
3828
|
+
local horizontalDirection = if currentHorizontal.Magnitude < 0.001 then Vector3.new(1, 0, 0) else currentHorizontal.Unit
|
|
3829
|
+
if compassOverride ~= nil then
|
|
3830
|
+
local yaw = math.rad(compassOverride)
|
|
3831
|
+
horizontalDirection = Vector3.new(math.cos(yaw), 0, math.sin(yaw))
|
|
3832
|
+
end
|
|
3833
|
+
local vertical = direction.Y
|
|
3834
|
+
local horizontalMagnitude = math.sqrt(math.max(0, 1 - vertical * vertical))
|
|
3835
|
+
if elevationOverride ~= nil then
|
|
3836
|
+
local pitch = math.rad(elevationOverride)
|
|
3837
|
+
vertical = math.sin(pitch)
|
|
3838
|
+
horizontalMagnitude = math.cos(pitch)
|
|
3839
|
+
end
|
|
3840
|
+
local _horizontalDirection = horizontalDirection
|
|
3841
|
+
local _horizontalMagnitude = horizontalMagnitude
|
|
3842
|
+
local _vector3 = Vector3.new(0, vertical, 0)
|
|
3843
|
+
direction = (_horizontalDirection * _horizontalMagnitude + _vector3).Unit
|
|
3844
|
+
camera.CameraType = Enum.CameraType.Scriptable
|
|
3845
|
+
local _direction = direction
|
|
3846
|
+
local _arg0 = math.max(boundsSize.Magnitude, 1)
|
|
3847
|
+
camera.CFrame = CFrame.lookAt(center + (_direction * _arg0), center)
|
|
3848
|
+
camera.Focus = CFrame.new(center)
|
|
3849
|
+
camera:ZoomToExtents(boundsCF, boundsSize)
|
|
3850
|
+
if padding ~= 1 then
|
|
3851
|
+
local fittedOffset = camera.CFrame.Position - center
|
|
3852
|
+
local _arg0_1 = fittedOffset * padding
|
|
3853
|
+
camera.CFrame = CFrame.lookAt(center + _arg0_1, center)
|
|
3854
|
+
end
|
|
3855
|
+
camera.Focus = CFrame.new(center)
|
|
3856
|
+
end)
|
|
3857
|
+
if not ok then
|
|
3858
|
+
return {
|
|
3859
|
+
error = `focus failed: {tostring(err)}`,
|
|
3860
|
+
}
|
|
3861
|
+
end
|
|
3862
|
+
return {
|
|
3863
|
+
success = true,
|
|
3864
|
+
path = getInstancePath(instance),
|
|
3865
|
+
cameraPosition = {
|
|
3866
|
+
X = camera.CFrame.Position.X,
|
|
3867
|
+
Y = camera.CFrame.Position.Y,
|
|
3868
|
+
Z = camera.CFrame.Position.Z,
|
|
3869
|
+
},
|
|
3870
|
+
}
|
|
3871
|
+
end
|
|
3641
3872
|
local function executeLuau(requestData)
|
|
3642
3873
|
local code = requestData.code
|
|
3643
3874
|
if not (code ~= "" and code) or code == "" then
|
|
@@ -3654,6 +3885,8 @@ end
|
|
|
3654
3885
|
return {
|
|
3655
3886
|
getAttributes = getAttributes,
|
|
3656
3887
|
getSelection = getSelection,
|
|
3888
|
+
setSelection = setSelection,
|
|
3889
|
+
focusViewport = focusViewport,
|
|
3657
3890
|
executeLuau = executeLuau,
|
|
3658
3891
|
}
|
|
3659
3892
|
]]></string>
|
|
@@ -3674,6 +3907,8 @@ local DEFAULT_MAX_TIMERS_PER_GROUP = 5
|
|
|
3674
3907
|
local DEFAULT_MAX_RELATED_TIMERS = 3
|
|
3675
3908
|
local DEFAULT_MAX_EVENTS = 250000
|
|
3676
3909
|
local MAX_EVENTS = 1000000
|
|
3910
|
+
local COOPERATIVE_CLOCK_CHECK_INTERVAL = 128
|
|
3911
|
+
local MAX_COOPERATIVE_SLICE_SECONDS = 0.008
|
|
3677
3912
|
local DEFAULT_FRAME_WINDOW = 240
|
|
3678
3913
|
local BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
3679
3914
|
local PAD_BYTE = (string.byte("="))
|
|
@@ -3690,6 +3925,87 @@ local FOCUS_GROUP_MASKS = {
|
|
|
3690
3925
|
jobs = { "Jobs" },
|
|
3691
3926
|
}
|
|
3692
3927
|
local cachedLibMP
|
|
3928
|
+
-- Sampling the clock avoids a timing call for every event while still bounding
|
|
3929
|
+
-- each VM resumption. Sorting uses the same checkpoint so no full sort is atomic.
|
|
3930
|
+
local function createCooperativeCheckpoint()
|
|
3931
|
+
local operationsSinceClockCheck = 0
|
|
3932
|
+
local sliceStartedAt = os.clock()
|
|
3933
|
+
return function()
|
|
3934
|
+
operationsSinceClockCheck += 1
|
|
3935
|
+
if operationsSinceClockCheck < COOPERATIVE_CLOCK_CHECK_INTERVAL then
|
|
3936
|
+
return nil
|
|
3937
|
+
end
|
|
3938
|
+
operationsSinceClockCheck = 0
|
|
3939
|
+
if os.clock() - sliceStartedAt < MAX_COOPERATIVE_SLICE_SECONDS then
|
|
3940
|
+
return nil
|
|
3941
|
+
end
|
|
3942
|
+
task.wait()
|
|
3943
|
+
sliceStartedAt = os.clock()
|
|
3944
|
+
end
|
|
3945
|
+
end
|
|
3946
|
+
local function cooperativeSort(values, comesBefore, checkpoint)
|
|
3947
|
+
local count = #values
|
|
3948
|
+
if count < 2 then
|
|
3949
|
+
return nil
|
|
3950
|
+
end
|
|
3951
|
+
local function siftDown(start, lastIndex)
|
|
3952
|
+
local root = start
|
|
3953
|
+
while root * 2 + 1 <= lastIndex do
|
|
3954
|
+
checkpoint()
|
|
3955
|
+
local candidate = root
|
|
3956
|
+
local left = root * 2 + 1
|
|
3957
|
+
if comesBefore(values[candidate + 1], values[left + 1]) then
|
|
3958
|
+
candidate = left
|
|
3959
|
+
end
|
|
3960
|
+
local right = left + 1
|
|
3961
|
+
if right <= lastIndex and comesBefore(values[candidate + 1], values[right + 1]) then
|
|
3962
|
+
candidate = right
|
|
3963
|
+
end
|
|
3964
|
+
if candidate == root then
|
|
3965
|
+
return nil
|
|
3966
|
+
end
|
|
3967
|
+
local value = values[root + 1]
|
|
3968
|
+
values[root + 1] = values[candidate + 1]
|
|
3969
|
+
values[candidate + 1] = value
|
|
3970
|
+
root = candidate
|
|
3971
|
+
end
|
|
3972
|
+
end
|
|
3973
|
+
do
|
|
3974
|
+
local start = math.floor((count - 2) / 2)
|
|
3975
|
+
local _shouldIncrement = false
|
|
3976
|
+
while true do
|
|
3977
|
+
if _shouldIncrement then
|
|
3978
|
+
start -= 1
|
|
3979
|
+
else
|
|
3980
|
+
_shouldIncrement = true
|
|
3981
|
+
end
|
|
3982
|
+
if not (start >= 0) then
|
|
3983
|
+
break
|
|
3984
|
+
end
|
|
3985
|
+
siftDown(start, count - 1)
|
|
3986
|
+
checkpoint()
|
|
3987
|
+
end
|
|
3988
|
+
end
|
|
3989
|
+
do
|
|
3990
|
+
local lastIndex = count - 1
|
|
3991
|
+
local _shouldIncrement = false
|
|
3992
|
+
while true do
|
|
3993
|
+
if _shouldIncrement then
|
|
3994
|
+
lastIndex -= 1
|
|
3995
|
+
else
|
|
3996
|
+
_shouldIncrement = true
|
|
3997
|
+
end
|
|
3998
|
+
if not (lastIndex > 0) then
|
|
3999
|
+
break
|
|
4000
|
+
end
|
|
4001
|
+
local value = values[1]
|
|
4002
|
+
values[1] = values[lastIndex + 1]
|
|
4003
|
+
values[lastIndex + 1] = value
|
|
4004
|
+
checkpoint()
|
|
4005
|
+
siftDown(0, lastIndex - 1)
|
|
4006
|
+
end
|
|
4007
|
+
end
|
|
4008
|
+
end
|
|
3693
4009
|
local function normalizeDurationMs(value)
|
|
3694
4010
|
local _value = value
|
|
3695
4011
|
if not (type(_value) == "number") then
|
|
@@ -3817,7 +4133,7 @@ local function addFrameRaw(map, frameId, raw)
|
|
|
3817
4133
|
local _arg1 = _condition + raw
|
|
3818
4134
|
_map[_exp] = _arg1
|
|
3819
4135
|
end
|
|
3820
|
-
local function summarizeFrameImpact(frameRawById, analyzedFrames)
|
|
4136
|
+
local function summarizeFrameImpact(frameRawById, analyzedFrames, checkpoint)
|
|
3821
4137
|
local _condition = frameRawById == nil
|
|
3822
4138
|
if not _condition then
|
|
3823
4139
|
-- ▼ ReadonlyMap.size ▼
|
|
@@ -3842,6 +4158,7 @@ local function summarizeFrameImpact(frameRawById, analyzedFrames)
|
|
|
3842
4158
|
local maxUs = 0
|
|
3843
4159
|
local maxFrameId = 0
|
|
3844
4160
|
for frameId, raw in frameRawById do
|
|
4161
|
+
checkpoint()
|
|
3845
4162
|
local us = rawToUs(raw)
|
|
3846
4163
|
table.insert(values, us)
|
|
3847
4164
|
totalUs += us
|
|
@@ -3850,9 +4167,9 @@ local function summarizeFrameImpact(frameRawById, analyzedFrames)
|
|
|
3850
4167
|
maxFrameId = frameId
|
|
3851
4168
|
end
|
|
3852
4169
|
end
|
|
3853
|
-
|
|
4170
|
+
cooperativeSort(values, function(a, b)
|
|
3854
4171
|
return a < b
|
|
3855
|
-
end)
|
|
4172
|
+
end, checkpoint)
|
|
3856
4173
|
return {
|
|
3857
4174
|
active_frame_count = #values,
|
|
3858
4175
|
active_frame_pct = percent(#values, analyzedFrames),
|
|
@@ -3863,7 +4180,7 @@ local function summarizeFrameImpact(frameRawById, analyzedFrames)
|
|
|
3863
4180
|
max_frame_id = maxFrameId,
|
|
3864
4181
|
}
|
|
3865
4182
|
end
|
|
3866
|
-
local function encodeBase64(buf)
|
|
4183
|
+
local function encodeBase64(buf, checkpoint)
|
|
3867
4184
|
local len = buffer.len(buf)
|
|
3868
4185
|
local fullTriples = math.floor(len / 3)
|
|
3869
4186
|
local remaining = len - fullTriples * 3
|
|
@@ -3892,6 +4209,7 @@ local function encodeBase64(buf)
|
|
|
3892
4209
|
buffer.writeu8(out, di + 3, B64[bit32.band(b2, 63) + 1])
|
|
3893
4210
|
si += 3
|
|
3894
4211
|
di += 4
|
|
4212
|
+
checkpoint()
|
|
3895
4213
|
end
|
|
3896
4214
|
end
|
|
3897
4215
|
if remaining == 2 then
|
|
@@ -4002,7 +4320,7 @@ local function recommendedToolsForGroups(groups, targetRole)
|
|
|
4002
4320
|
end
|
|
4003
4321
|
return tools
|
|
4004
4322
|
end
|
|
4005
|
-
local function collectFrameSummary(session, startFrame, frameMax)
|
|
4323
|
+
local function collectFrameSummary(session, startFrame, frameMax, checkpoint)
|
|
4006
4324
|
local frameRows = {}
|
|
4007
4325
|
local durations = {}
|
|
4008
4326
|
local incompleteFrames = 0
|
|
@@ -4020,6 +4338,7 @@ local function collectFrameSummary(session, startFrame, frameMax)
|
|
|
4020
4338
|
if not (frameId <= frameMax) then
|
|
4021
4339
|
break
|
|
4022
4340
|
end
|
|
4341
|
+
checkpoint()
|
|
4023
4342
|
local ok, descOrErr = safeCall(function()
|
|
4024
4343
|
return session:GetFrameDesc(frameId)
|
|
4025
4344
|
end)
|
|
@@ -4059,15 +4378,16 @@ local function collectFrameSummary(session, startFrame, frameMax)
|
|
|
4059
4378
|
_frameId = frameId
|
|
4060
4379
|
end
|
|
4061
4380
|
end
|
|
4062
|
-
|
|
4381
|
+
cooperativeSort(durations, function(a, b)
|
|
4063
4382
|
return a < b
|
|
4064
|
-
end)
|
|
4065
|
-
|
|
4383
|
+
end, checkpoint)
|
|
4384
|
+
cooperativeSort(frameRows, function(a, b)
|
|
4066
4385
|
return (a.duration_us) > (b.duration_us)
|
|
4067
|
-
end)
|
|
4386
|
+
end, checkpoint)
|
|
4068
4387
|
local totalUs = 0
|
|
4069
4388
|
for _, duration in durations do
|
|
4070
4389
|
totalUs += duration
|
|
4390
|
+
checkpoint()
|
|
4071
4391
|
end
|
|
4072
4392
|
local topFrames = {}
|
|
4073
4393
|
do
|
|
@@ -4381,6 +4701,7 @@ local function captureMicroProfiler(requestData)
|
|
|
4381
4701
|
backend = backend,
|
|
4382
4702
|
}
|
|
4383
4703
|
end
|
|
4704
|
+
local checkpoint = createCooperativeCheckpoint()
|
|
4384
4705
|
local stacks = {}
|
|
4385
4706
|
local aggregates = {}
|
|
4386
4707
|
local timerThreadAggregates = {}
|
|
@@ -4396,7 +4717,11 @@ local function captureMicroProfiler(requestData)
|
|
|
4396
4717
|
local sampledFrameMax
|
|
4397
4718
|
local lastProcessedFrameId
|
|
4398
4719
|
local lastProcessedTimestampRaw
|
|
4399
|
-
while eventsSampled < maxEvents
|
|
4720
|
+
while eventsSampled < maxEvents do
|
|
4721
|
+
checkpoint()
|
|
4722
|
+
if not iterator:Step() then
|
|
4723
|
+
break
|
|
4724
|
+
end
|
|
4400
4725
|
eventsSampled += 1
|
|
4401
4726
|
local state = iterator:GetState()
|
|
4402
4727
|
if not state then
|
|
@@ -4437,6 +4762,7 @@ local function captureMicroProfiler(requestData)
|
|
|
4437
4762
|
exitEvents += 1
|
|
4438
4763
|
local entry
|
|
4439
4764
|
while #stack > 0 do
|
|
4765
|
+
checkpoint()
|
|
4440
4766
|
-- ▼ Array.pop ▼
|
|
4441
4767
|
local _length = #stack
|
|
4442
4768
|
local _result = stack[_length]
|
|
@@ -4514,7 +4840,7 @@ local function captureMicroProfiler(requestData)
|
|
|
4514
4840
|
_condition_2 = frameMax
|
|
4515
4841
|
end
|
|
4516
4842
|
local analysisFrameMax = _condition_2
|
|
4517
|
-
local frameSummary = collectFrameSummary(session, analysisFrameMin, analysisFrameMax)
|
|
4843
|
+
local frameSummary = collectFrameSummary(session, analysisFrameMin, analysisFrameMax, checkpoint)
|
|
4518
4844
|
local _frames = frameSummary.frames
|
|
4519
4845
|
local _condition_3 = type(_frames) == "number"
|
|
4520
4846
|
if _condition_3 then
|
|
@@ -4563,6 +4889,7 @@ local function captureMicroProfiler(requestData)
|
|
|
4563
4889
|
local omittedBelowThreshold = 0
|
|
4564
4890
|
local omittedByFilter = 0
|
|
4565
4891
|
for timerId, aggregate in aggregates do
|
|
4892
|
+
checkpoint()
|
|
4566
4893
|
local info = getTimerInfo(timerId)
|
|
4567
4894
|
local totalUs = rawToUs(aggregate.inclusive_raw)
|
|
4568
4895
|
if not includeIdle and isIdleTimer(info) then
|
|
@@ -4627,7 +4954,7 @@ local function captureMicroProfiler(requestData)
|
|
|
4627
4954
|
if maxToAvg ~= nil then
|
|
4628
4955
|
row.max_to_avg = maxToAvg
|
|
4629
4956
|
end
|
|
4630
|
-
local timerFrameImpact = summarizeFrameImpact(timerFrameAggregates[timerId], analysisFrameCount)
|
|
4957
|
+
local timerFrameImpact = summarizeFrameImpact(timerFrameAggregates[timerId], analysisFrameCount, checkpoint)
|
|
4631
4958
|
for key, value in pairs(timerFrameImpact) do
|
|
4632
4959
|
row[key] = value
|
|
4633
4960
|
end
|
|
@@ -4653,12 +4980,14 @@ local function captureMicroProfiler(requestData)
|
|
|
4653
4980
|
end
|
|
4654
4981
|
for frameId, raw in timerFrames do
|
|
4655
4982
|
addFrameRaw(groupFrames, frameId, raw)
|
|
4983
|
+
checkpoint()
|
|
4656
4984
|
end
|
|
4657
4985
|
end
|
|
4658
4986
|
local perThread = timerThreadAggregates[timerId]
|
|
4659
4987
|
if perThread ~= nil then
|
|
4660
4988
|
local timerThreadSummaryRows = {}
|
|
4661
4989
|
for threadId, threadAggregate in perThread do
|
|
4990
|
+
checkpoint()
|
|
4662
4991
|
local threadAggregateRow = threadAggregates[threadId]
|
|
4663
4992
|
if threadAggregateRow == nil then
|
|
4664
4993
|
threadAggregateRow = {
|
|
@@ -4717,9 +5046,9 @@ local function captureMicroProfiler(requestData)
|
|
|
4717
5046
|
}
|
|
4718
5047
|
table.insert(timerThreadSummaryRows, _arg0)
|
|
4719
5048
|
end
|
|
4720
|
-
|
|
5049
|
+
cooperativeSort(timerThreadSummaryRows, function(a, b)
|
|
4721
5050
|
return (a.inclusive_us) > (b.inclusive_us)
|
|
4722
|
-
end)
|
|
5051
|
+
end, checkpoint)
|
|
4723
5052
|
if maxRelatedTimers > 0 and #timerThreadSummaryRows > 0 then
|
|
4724
5053
|
local topTimerThreads = {}
|
|
4725
5054
|
do
|
|
@@ -4742,13 +5071,14 @@ local function captureMicroProfiler(requestData)
|
|
|
4742
5071
|
end
|
|
4743
5072
|
end
|
|
4744
5073
|
end
|
|
4745
|
-
|
|
5074
|
+
cooperativeSort(rows, function(a, b)
|
|
4746
5075
|
return (a.inclusive_us) > (b.inclusive_us)
|
|
4747
|
-
end)
|
|
5076
|
+
end, checkpoint)
|
|
4748
5077
|
local edgeRows = {}
|
|
4749
5078
|
local parentRelationsByChild = {}
|
|
4750
5079
|
local childRelationsByParent = {}
|
|
4751
5080
|
for _, edge in edgeAggregates do
|
|
5081
|
+
checkpoint()
|
|
4752
5082
|
local _parent_timer_id = edge.parent_timer_id
|
|
4753
5083
|
local parentRow = rowsByTimerId[_parent_timer_id]
|
|
4754
5084
|
local _child_timer_id = edge.child_timer_id
|
|
@@ -4819,21 +5149,24 @@ local function captureMicroProfiler(requestData)
|
|
|
4819
5149
|
}
|
|
4820
5150
|
table.insert(_childRelations, _arg0_1)
|
|
4821
5151
|
end
|
|
4822
|
-
|
|
5152
|
+
cooperativeSort(edgeRows, function(a, b)
|
|
4823
5153
|
return (a.inclusive_us) > (b.inclusive_us)
|
|
4824
|
-
end)
|
|
5154
|
+
end, checkpoint)
|
|
4825
5155
|
if maxRelatedTimers > 0 then
|
|
4826
5156
|
for _, relationRows in parentRelationsByChild do
|
|
4827
|
-
|
|
5157
|
+
checkpoint()
|
|
5158
|
+
cooperativeSort(relationRows, function(a, b)
|
|
4828
5159
|
return (a.inclusive_us) > (b.inclusive_us)
|
|
4829
|
-
end)
|
|
5160
|
+
end, checkpoint)
|
|
4830
5161
|
end
|
|
4831
5162
|
for _, relationRows in childRelationsByParent do
|
|
4832
|
-
|
|
5163
|
+
checkpoint()
|
|
5164
|
+
cooperativeSort(relationRows, function(a, b)
|
|
4833
5165
|
return (a.inclusive_us) > (b.inclusive_us)
|
|
4834
|
-
end)
|
|
5166
|
+
end, checkpoint)
|
|
4835
5167
|
end
|
|
4836
5168
|
for _, row in rows do
|
|
5169
|
+
checkpoint()
|
|
4837
5170
|
local timerId = row.timer_id
|
|
4838
5171
|
local parents = parentRelationsByChild[timerId]
|
|
4839
5172
|
local children = childRelationsByParent[timerId]
|
|
@@ -4900,10 +5233,11 @@ local function captureMicroProfiler(requestData)
|
|
|
4900
5233
|
local rowsByExclusive = {}
|
|
4901
5234
|
for _, row in rows do
|
|
4902
5235
|
table.insert(rowsByExclusive, row)
|
|
5236
|
+
checkpoint()
|
|
4903
5237
|
end
|
|
4904
|
-
|
|
5238
|
+
cooperativeSort(rowsByExclusive, function(a, b)
|
|
4905
5239
|
return (a.exclusive_us) > (b.exclusive_us)
|
|
4906
|
-
end)
|
|
5240
|
+
end, checkpoint)
|
|
4907
5241
|
local topTimersByExclusive = {}
|
|
4908
5242
|
do
|
|
4909
5243
|
local i = 0
|
|
@@ -4942,6 +5276,7 @@ local function captureMicroProfiler(requestData)
|
|
|
4942
5276
|
end
|
|
4943
5277
|
local threadRows = {}
|
|
4944
5278
|
for threadId, aggregate in threadAggregates do
|
|
5279
|
+
checkpoint()
|
|
4945
5280
|
local info = getThreadInfo(threadId)
|
|
4946
5281
|
local inclusiveUs = rawToUs(aggregate.inclusive_raw)
|
|
4947
5282
|
local exclusiveUs = rawToUs(aggregate.exclusive_raw)
|
|
@@ -4959,9 +5294,9 @@ local function captureMicroProfiler(requestData)
|
|
|
4959
5294
|
pct_of_analyzed_wall = percent(inclusiveUs, analysisDurationUs),
|
|
4960
5295
|
}
|
|
4961
5296
|
local timerRows = threadTimerRows[threadId] or {}
|
|
4962
|
-
|
|
5297
|
+
cooperativeSort(timerRows, function(a, b)
|
|
4963
5298
|
return (a.inclusive_us) > (b.inclusive_us)
|
|
4964
|
-
end)
|
|
5299
|
+
end, checkpoint)
|
|
4965
5300
|
local topThreadTimers = {}
|
|
4966
5301
|
do
|
|
4967
5302
|
local i = 0
|
|
@@ -4984,9 +5319,9 @@ local function captureMicroProfiler(requestData)
|
|
|
4984
5319
|
end
|
|
4985
5320
|
table.insert(threadRows, row)
|
|
4986
5321
|
end
|
|
4987
|
-
|
|
5322
|
+
cooperativeSort(threadRows, function(a, b)
|
|
4988
5323
|
return (a.inclusive_us) > (b.inclusive_us)
|
|
4989
|
-
end)
|
|
5324
|
+
end, checkpoint)
|
|
4990
5325
|
local topThreads = {}
|
|
4991
5326
|
do
|
|
4992
5327
|
local i = 0
|
|
@@ -5007,15 +5342,16 @@ local function captureMicroProfiler(requestData)
|
|
|
5007
5342
|
end
|
|
5008
5343
|
local groupRows = {}
|
|
5009
5344
|
for _, aggregate in groupAggregates do
|
|
5345
|
+
checkpoint()
|
|
5010
5346
|
local _group = aggregate.group
|
|
5011
5347
|
local _condition_5 = groupTimerRows[_group]
|
|
5012
5348
|
if _condition_5 == nil then
|
|
5013
5349
|
_condition_5 = {}
|
|
5014
5350
|
end
|
|
5015
5351
|
local timerRows = _condition_5
|
|
5016
|
-
|
|
5352
|
+
cooperativeSort(timerRows, function(a, b)
|
|
5017
5353
|
return (a.inclusive_us) > (b.inclusive_us)
|
|
5018
|
-
end)
|
|
5354
|
+
end, checkpoint)
|
|
5019
5355
|
local topGroupTimers = {}
|
|
5020
5356
|
do
|
|
5021
5357
|
local i = 0
|
|
@@ -5058,7 +5394,7 @@ local function captureMicroProfiler(requestData)
|
|
|
5058
5394
|
pct_of_analyzed_wall = percent(inclusiveUs, analysisDurationUs),
|
|
5059
5395
|
}
|
|
5060
5396
|
local _group_1 = aggregate.group
|
|
5061
|
-
local groupFrameImpact = summarizeFrameImpact(groupFrameAggregates[_group_1], analysisFrameCount)
|
|
5397
|
+
local groupFrameImpact = summarizeFrameImpact(groupFrameAggregates[_group_1], analysisFrameCount, checkpoint)
|
|
5062
5398
|
for key, value in pairs(groupFrameImpact) do
|
|
5063
5399
|
groupRow[key] = value
|
|
5064
5400
|
end
|
|
@@ -5067,9 +5403,9 @@ local function captureMicroProfiler(requestData)
|
|
|
5067
5403
|
end
|
|
5068
5404
|
table.insert(groupRows, groupRow)
|
|
5069
5405
|
end
|
|
5070
|
-
|
|
5406
|
+
cooperativeSort(groupRows, function(a, b)
|
|
5071
5407
|
return (a.inclusive_us) > (b.inclusive_us)
|
|
5072
|
-
end)
|
|
5408
|
+
end, checkpoint)
|
|
5073
5409
|
local topGroups = {}
|
|
5074
5410
|
do
|
|
5075
5411
|
local i = 0
|
|
@@ -5091,10 +5427,11 @@ local function captureMicroProfiler(requestData)
|
|
|
5091
5427
|
local groupRowsByExclusive = {}
|
|
5092
5428
|
for _, row in groupRows do
|
|
5093
5429
|
table.insert(groupRowsByExclusive, row)
|
|
5430
|
+
checkpoint()
|
|
5094
5431
|
end
|
|
5095
|
-
|
|
5432
|
+
cooperativeSort(groupRowsByExclusive, function(a, b)
|
|
5096
5433
|
return (a.exclusive_us) > (b.exclusive_us)
|
|
5097
|
-
end)
|
|
5434
|
+
end, checkpoint)
|
|
5098
5435
|
local topGroupsByExclusive = {}
|
|
5099
5436
|
do
|
|
5100
5437
|
local i = 0
|
|
@@ -5126,6 +5463,7 @@ local function captureMicroProfiler(requestData)
|
|
|
5126
5463
|
local openStackEntriesAtEnd = 0
|
|
5127
5464
|
for _, stack in stacks do
|
|
5128
5465
|
openStackEntriesAtEnd += #stack
|
|
5466
|
+
checkpoint()
|
|
5129
5467
|
end
|
|
5130
5468
|
local iteratorFinished = eventsSampled < maxEvents
|
|
5131
5469
|
local partialReasons = {}
|
|
@@ -5148,21 +5486,25 @@ local function captureMicroProfiler(requestData)
|
|
|
5148
5486
|
for _, row in rows do
|
|
5149
5487
|
local _arg0 = pickFields(row, timerFields)
|
|
5150
5488
|
table.insert(timerIndex, _arg0)
|
|
5489
|
+
checkpoint()
|
|
5151
5490
|
end
|
|
5152
5491
|
local groupIndex = {}
|
|
5153
5492
|
for _, row in groupRows do
|
|
5154
5493
|
local _arg0 = pickFields(row, groupFields)
|
|
5155
5494
|
table.insert(groupIndex, _arg0)
|
|
5495
|
+
checkpoint()
|
|
5156
5496
|
end
|
|
5157
5497
|
local threadIndex = {}
|
|
5158
5498
|
for _, row in threadRows do
|
|
5159
5499
|
local _arg0 = pickFields(row, threadFields)
|
|
5160
5500
|
table.insert(threadIndex, _arg0)
|
|
5501
|
+
checkpoint()
|
|
5161
5502
|
end
|
|
5162
5503
|
local edgeIndex = {}
|
|
5163
5504
|
for _, row in edgeRows do
|
|
5164
5505
|
local _arg0 = pickFields(row, edgeFields)
|
|
5165
5506
|
table.insert(edgeIndex, _arg0)
|
|
5507
|
+
checkpoint()
|
|
5166
5508
|
end
|
|
5167
5509
|
comparisonIndex = {
|
|
5168
5510
|
timers = timerIndex,
|
|
@@ -5291,7 +5633,7 @@ local function captureMicroProfiler(requestData)
|
|
|
5291
5633
|
result.snapshot_object_size_bytes = session:GetObjSize()
|
|
5292
5634
|
end
|
|
5293
5635
|
if includeRawBuffer then
|
|
5294
|
-
result.raw_snapshot_base64 = encodeBase64(snapshot)
|
|
5636
|
+
result.raw_snapshot_base64 = encodeBase64(snapshot, checkpoint)
|
|
5295
5637
|
end
|
|
5296
5638
|
iterator:Dispose()
|
|
5297
5639
|
session:Dispose()
|
|
@@ -5860,7 +6202,6 @@ local function getProjectStructure(requestData)
|
|
|
5860
6202
|
name = instance.Name,
|
|
5861
6203
|
className = instance.ClassName,
|
|
5862
6204
|
path = getInstancePath(instance),
|
|
5863
|
-
children = {},
|
|
5864
6205
|
}
|
|
5865
6206
|
if instance:IsA("LuaSourceContainer") then
|
|
5866
6207
|
node.hasSource = true
|
|
@@ -5900,7 +6241,7 @@ local function getProjectStructure(requestData)
|
|
|
5900
6241
|
-- ▲ ReadonlyArray.filter ▲
|
|
5901
6242
|
children = _newValue
|
|
5902
6243
|
end
|
|
5903
|
-
local nodeChildren =
|
|
6244
|
+
local nodeChildren = {}
|
|
5904
6245
|
local childCount = #children
|
|
5905
6246
|
if childCount > 20 and depth < maxDepth then
|
|
5906
6247
|
local classGroups = {}
|
|
@@ -5975,6 +6316,9 @@ local function getProjectStructure(requestData)
|
|
|
5975
6316
|
table.insert(nodeChildren, _arg0)
|
|
5976
6317
|
end
|
|
5977
6318
|
end
|
|
6319
|
+
if #nodeChildren > 0 then
|
|
6320
|
+
node.children = nodeChildren
|
|
6321
|
+
end
|
|
5978
6322
|
return node
|
|
5979
6323
|
end
|
|
5980
6324
|
local result = getStructure(startInstance, 0)
|
|
@@ -6527,16 +6871,6 @@ local finishRecording = _binding_1.finishRecording
|
|
|
6527
6871
|
local SOURCE_TRUNCATE_CHAR_BUDGET = 25000
|
|
6528
6872
|
local SOURCE_TRUNCATE_LINE_BUDGET = 400
|
|
6529
6873
|
local SOURCE_TRUNCATE_TO_LINES = 300
|
|
6530
|
-
local function normalizeEscapes(s)
|
|
6531
|
-
local result = s
|
|
6532
|
-
result = (string.gsub(result, "\\\\", "\x01"))
|
|
6533
|
-
result = (string.gsub(result, "\\n", "\n"))
|
|
6534
|
-
result = (string.gsub(result, "\\t", "\t"))
|
|
6535
|
-
result = (string.gsub(result, "\\r", "\r"))
|
|
6536
|
-
result = (string.gsub(result, '\\"', '"'))
|
|
6537
|
-
result = (string.gsub(result, "\x01", "\\"))
|
|
6538
|
-
return result
|
|
6539
|
-
end
|
|
6540
6874
|
local function getTopServiceName(instance)
|
|
6541
6875
|
local topServiceInst = instance
|
|
6542
6876
|
while topServiceInst.Parent and topServiceInst.Parent ~= game do
|
|
@@ -6664,7 +6998,7 @@ end
|
|
|
6664
6998
|
local function setScriptSource(requestData)
|
|
6665
6999
|
local instancePath = requestData.instancePath
|
|
6666
7000
|
local newSource = requestData.source
|
|
6667
|
-
if not (instancePath ~= "" and instancePath) or not (newSource
|
|
7001
|
+
if not (instancePath ~= "" and instancePath) or not (type(newSource) == "string") then
|
|
6668
7002
|
return {
|
|
6669
7003
|
error = "Instance path and source are required",
|
|
6670
7004
|
}
|
|
@@ -6680,7 +7014,8 @@ local function setScriptSource(requestData)
|
|
|
6680
7014
|
error = `Instance is not a script-like object: {instance.ClassName}`,
|
|
6681
7015
|
}
|
|
6682
7016
|
end
|
|
6683
|
-
|
|
7017
|
+
-- Communication has already JSON-decoded the poll payload; source text is exact at this boundary.
|
|
7018
|
+
local sourceToSet = newSource
|
|
6684
7019
|
local recordingId = beginRecording(`Set script source: {instance.Name}`)
|
|
6685
7020
|
local readSuccess, readResult = pcall(function()
|
|
6686
7021
|
return #readScriptSource(instance)
|
|
@@ -6704,40 +7039,9 @@ local function setScriptSource(requestData)
|
|
|
6704
7039
|
message = `Script source updated successfully ({if applyResult.method == "UpdateSourceAsync" then "editor-safe" else "direct assignment"})`,
|
|
6705
7040
|
}
|
|
6706
7041
|
end
|
|
6707
|
-
local replaceSuccess, replaceResult = pcall(function()
|
|
6708
|
-
local parent = instance.Parent
|
|
6709
|
-
local name = instance.Name
|
|
6710
|
-
local className = instance.ClassName
|
|
6711
|
-
local wasBaseScript = instance:IsA("BaseScript")
|
|
6712
|
-
local enabled = if wasBaseScript then instance.Enabled else nil
|
|
6713
|
-
local newScript = Instance.new(className)
|
|
6714
|
-
newScript.Name = name
|
|
6715
|
-
-- @rbxts/types does not expose PluginSecurity Source writes.
|
|
6716
|
-
local writableNewScript = newScript
|
|
6717
|
-
writableNewScript.Source = sourceToSet
|
|
6718
|
-
if readScriptSource(newScript) ~= sourceToSet then
|
|
6719
|
-
error("Replacement script source did not match the requested source")
|
|
6720
|
-
end
|
|
6721
|
-
if wasBaseScript and enabled ~= nil then
|
|
6722
|
-
local newBaseScript = newScript
|
|
6723
|
-
newBaseScript.Enabled = enabled
|
|
6724
|
-
end
|
|
6725
|
-
newScript.Parent = parent
|
|
6726
|
-
instance:Destroy()
|
|
6727
|
-
return {
|
|
6728
|
-
success = true,
|
|
6729
|
-
instancePath = getInstancePath(newScript),
|
|
6730
|
-
method = "replace",
|
|
6731
|
-
message = "Script replaced successfully with new source",
|
|
6732
|
-
}
|
|
6733
|
-
end)
|
|
6734
|
-
if replaceSuccess then
|
|
6735
|
-
finishRecording(recordingId, true)
|
|
6736
|
-
return replaceResult
|
|
6737
|
-
end
|
|
6738
7042
|
finishRecording(recordingId, false)
|
|
6739
7043
|
return {
|
|
6740
|
-
error = `Failed to set script source
|
|
7044
|
+
error = `Failed to set script source: {applyResult.error}`,
|
|
6741
7045
|
}
|
|
6742
7046
|
end
|
|
6743
7047
|
local function editScriptLines(requestData)
|
|
@@ -6750,8 +7054,6 @@ local function editScriptLines(requestData)
|
|
|
6750
7054
|
error = "Instance path, old_string, and new_string are required",
|
|
6751
7055
|
}
|
|
6752
7056
|
end
|
|
6753
|
-
oldString = normalizeEscapes(oldString)
|
|
6754
|
-
newString = normalizeEscapes(newString)
|
|
6755
7057
|
local instance = getInstanceByPath(instancePath)
|
|
6756
7058
|
if not instance then
|
|
6757
7059
|
return {
|
|
@@ -6848,7 +7150,6 @@ local function insertScriptLines(requestData)
|
|
|
6848
7150
|
error = "Instance path and newContent are required",
|
|
6849
7151
|
}
|
|
6850
7152
|
end
|
|
6851
|
-
newContent = normalizeEscapes(newContent)
|
|
6852
7153
|
local instance = getInstanceByPath(instancePath)
|
|
6853
7154
|
if not instance then
|
|
6854
7155
|
return {
|
|
@@ -9272,9 +9573,10 @@ return {
|
|
|
9272
9573
|
<Properties>
|
|
9273
9574
|
<string name="Name">State</string>
|
|
9274
9575
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
9275
|
-
local CURRENT_VERSION = "3.0.
|
|
9576
|
+
local CURRENT_VERSION = "3.0.2"
|
|
9276
9577
|
local PLUGIN_VARIANT = "main"
|
|
9277
9578
|
local BASE_PORT = 58741
|
|
9579
|
+
local POLL_REQUEST_TIMEOUT_SECONDS = 20
|
|
9278
9580
|
local function createConnection(port)
|
|
9279
9581
|
return {
|
|
9280
9582
|
port = port,
|
|
@@ -9303,6 +9605,7 @@ return {
|
|
|
9303
9605
|
CURRENT_VERSION = CURRENT_VERSION,
|
|
9304
9606
|
PLUGIN_VARIANT = PLUGIN_VARIANT,
|
|
9305
9607
|
BASE_PORT = BASE_PORT,
|
|
9608
|
+
POLL_REQUEST_TIMEOUT_SECONDS = POLL_REQUEST_TIMEOUT_SECONDS,
|
|
9306
9609
|
getActiveConnection = getActiveConnection,
|
|
9307
9610
|
}
|
|
9308
9611
|
]]></string>
|