@chrrxs/robloxstudio-mcp-inspector 2.16.3 → 2.17.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 +232 -65
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +1100 -168
- package/studio-plugin/MCPPlugin.rbxmx +1100 -168
- package/studio-plugin/src/modules/ClientBroker.ts +10 -0
- package/studio-plugin/src/modules/Communication.ts +4 -2
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +36 -10
- package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +460 -0
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +0 -33
- package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +386 -0
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +14 -44
- package/studio-plugin/src/server/index.server.ts +2 -0
|
@@ -16,6 +16,7 @@ local cleanupLegacyEditBridges = _EvalBridges.cleanupLegacyEditBridges
|
|
|
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 BreakpointHandlers = TS.import(script, script, "modules", "handlers", "BreakpointHandlers")
|
|
19
20
|
local RenderMonitor = TS.import(script, script, "modules", "RenderMonitor")
|
|
20
21
|
-- Track render-loop liveness so input/screenshot tools can report "window
|
|
21
22
|
-- minimized / not rendering" instead of silently no-op'ing. No-op in the
|
|
@@ -29,6 +30,7 @@ RuntimeLogBuffer.install()
|
|
|
29
30
|
-- edit DM (write the flag) and the play-server DM (read+act on the flag) can
|
|
30
31
|
-- access plugin:SetSetting/GetSetting.
|
|
31
32
|
StopPlayMonitor.init(plugin)
|
|
33
|
+
BreakpointHandlers.init(plugin)
|
|
32
34
|
ServerUrlSettings.init(plugin)
|
|
33
35
|
UI.init(plugin)
|
|
34
36
|
local elements = UI.getElements()
|
|
@@ -147,6 +149,8 @@ local SceneAnalysisHandlers = TS.import(script, script.Parent, "handlers", "Scen
|
|
|
147
149
|
local CaptureHandlers = TS.import(script, script.Parent, "handlers", "CaptureHandlers")
|
|
148
150
|
local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandlers")
|
|
149
151
|
local EvalRuntimeHandlers = TS.import(script, script.Parent, "handlers", "EvalRuntimeHandlers")
|
|
152
|
+
local BreakpointHandlers = TS.import(script, script.Parent, "handlers", "BreakpointHandlers")
|
|
153
|
+
local ScriptProfilerHandlers = TS.import(script, script.Parent, "handlers", "ScriptProfilerHandlers")
|
|
150
154
|
local LuauExec = TS.import(script, script.Parent, "LuauExec")
|
|
151
155
|
local State = TS.import(script, script.Parent, "State")
|
|
152
156
|
local HttpDiagnostics = TS.import(script, script.Parent, "HttpDiagnostics")
|
|
@@ -215,6 +219,8 @@ local CLIENT_BROKER_ALLOWED_ENDPOINTS = {
|
|
|
215
219
|
["/api/get-runtime-logs"] = true,
|
|
216
220
|
["/api/get-memory-breakdown"] = true,
|
|
217
221
|
["/api/get-scene-analysis"] = true,
|
|
222
|
+
["/api/breakpoints"] = true,
|
|
223
|
+
["/api/capture-script-profiler"] = true,
|
|
218
224
|
["/api/multiplayer-test-state"] = true,
|
|
219
225
|
["/api/multiplayer-test-leave-client"] = true,
|
|
220
226
|
["/api/capture-begin"] = true,
|
|
@@ -404,6 +410,12 @@ local function setupClientBroker()
|
|
|
404
410
|
if payload and payload.endpoint == "/api/get-scene-analysis" then
|
|
405
411
|
return SceneAnalysisHandlers.getSceneAnalysis(payload.data or {})
|
|
406
412
|
end
|
|
413
|
+
if payload and payload.endpoint == "/api/breakpoints" then
|
|
414
|
+
return BreakpointHandlers.breakpoints(payload.data or {})
|
|
415
|
+
end
|
|
416
|
+
if payload and payload.endpoint == "/api/capture-script-profiler" then
|
|
417
|
+
return ScriptProfilerHandlers.captureScriptProfiler(payload.data or {})
|
|
418
|
+
end
|
|
407
419
|
if payload and payload.endpoint == "/api/multiplayer-test-state" then
|
|
408
420
|
return handleMultiplayerTestState()
|
|
409
421
|
end
|
|
@@ -650,6 +662,8 @@ local LogHandlers = TS.import(script, script.Parent, "handlers", "LogHandlers")
|
|
|
650
662
|
local SerializationHandlers = TS.import(script, script.Parent, "handlers", "SerializationHandlers")
|
|
651
663
|
local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandlers")
|
|
652
664
|
local SceneAnalysisHandlers = TS.import(script, script.Parent, "handlers", "SceneAnalysisHandlers")
|
|
665
|
+
local BreakpointHandlers = TS.import(script, script.Parent, "handlers", "BreakpointHandlers")
|
|
666
|
+
local ScriptProfilerHandlers = TS.import(script, script.Parent, "handlers", "ScriptProfilerHandlers")
|
|
653
667
|
local EvalRuntimeHandlers = TS.import(script, script.Parent, "handlers", "EvalRuntimeHandlers")
|
|
654
668
|
local ClientBroker = TS.import(script, script.Parent, "ClientBroker")
|
|
655
669
|
local ServerUrlSettings = TS.import(script, script.Parent, "ServerUrlSettings")
|
|
@@ -739,7 +753,6 @@ local routeMap = {
|
|
|
739
753
|
["/api/grep-scripts"] = QueryHandlers.grepScripts,
|
|
740
754
|
["/api/get-descendants"] = QueryHandlers.getDescendants,
|
|
741
755
|
["/api/compare-instances"] = QueryHandlers.compareInstances,
|
|
742
|
-
["/api/get-output-log"] = QueryHandlers.getOutputLog,
|
|
743
756
|
["/api/set-property"] = PropertyHandlers.setProperty,
|
|
744
757
|
["/api/set-properties"] = PropertyHandlers.setProperties,
|
|
745
758
|
["/api/mass-set-property"] = PropertyHandlers.massSetProperty,
|
|
@@ -771,7 +784,6 @@ local routeMap = {
|
|
|
771
784
|
["/api/bulk-set-attributes"] = MetadataHandlers.bulkSetAttributes,
|
|
772
785
|
["/api/start-playtest"] = TestHandlers.startPlaytest,
|
|
773
786
|
["/api/stop-playtest"] = TestHandlers.stopPlaytest,
|
|
774
|
-
["/api/get-playtest-output"] = TestHandlers.getPlaytestOutput,
|
|
775
787
|
["/api/multiplayer-test-start"] = TestHandlers.multiplayerTestStart,
|
|
776
788
|
["/api/multiplayer-test-state"] = TestHandlers.multiplayerTestState,
|
|
777
789
|
["/api/multiplayer-test-add-players"] = TestHandlers.multiplayerTestAddPlayers,
|
|
@@ -791,6 +803,8 @@ local routeMap = {
|
|
|
791
803
|
["/api/simulate-keyboard-input"] = InputHandlers.simulateKeyboardInput,
|
|
792
804
|
["/api/find-and-replace-in-scripts"] = ScriptHandlers.findAndReplaceInScripts,
|
|
793
805
|
["/api/get-runtime-logs"] = LogHandlers.getRuntimeLogs,
|
|
806
|
+
["/api/breakpoints"] = BreakpointHandlers.breakpoints,
|
|
807
|
+
["/api/capture-script-profiler"] = ScriptProfilerHandlers.captureScriptProfiler,
|
|
794
808
|
["/api/export-rbxm"] = SerializationHandlers.exportRbxm,
|
|
795
809
|
["/api/import-rbxm"] = SerializationHandlers.importRbxm,
|
|
796
810
|
["/api/get-memory-breakdown"] = MemoryHandlers.getMemoryBreakdown,
|
|
@@ -1411,9 +1425,9 @@ local function computeBridgeStamp()
|
|
|
1411
1425
|
for i = 1, #combined do
|
|
1412
1426
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1413
1427
|
end
|
|
1414
|
-
-- "2.
|
|
1428
|
+
-- "2.17.0" is replaced with the package version at package time
|
|
1415
1429
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1416
|
-
return `{tostring(h)}-2.
|
|
1430
|
+
return `{tostring(h)}-2.17.0`
|
|
1417
1431
|
end
|
|
1418
1432
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1419
1433
|
local function setSource(scriptInst, source)
|
|
@@ -1857,6 +1871,524 @@ return {
|
|
|
1857
1871
|
</Properties>
|
|
1858
1872
|
</Item>
|
|
1859
1873
|
<Item class="ModuleScript" referent="7">
|
|
1874
|
+
<Properties>
|
|
1875
|
+
<string name="Name">BreakpointHandlers</string>
|
|
1876
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
1877
|
+
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
1878
|
+
local Utils = TS.import(script, script.Parent.Parent, "Utils")
|
|
1879
|
+
local _binding = Utils
|
|
1880
|
+
local getInstanceByPath = _binding.getInstanceByPath
|
|
1881
|
+
local HttpService = game:GetService("HttpService")
|
|
1882
|
+
local RunService = game:GetService("RunService")
|
|
1883
|
+
local ServerStorage = game:GetService("ServerStorage")
|
|
1884
|
+
local LOG_PREFIX = "Breakpoint"
|
|
1885
|
+
local REGISTRY_KEY_PREFIX = "MCP_BREAKPOINTS_V1_"
|
|
1886
|
+
local MCP_PLACE_ID_ATTRIBUTE = "__MCPPlaceId"
|
|
1887
|
+
local pluginRef
|
|
1888
|
+
local loadedRegistryKey
|
|
1889
|
+
local loadedRegistryFromSettings = false
|
|
1890
|
+
local breakpoints = {}
|
|
1891
|
+
local function init(p)
|
|
1892
|
+
pluginRef = p
|
|
1893
|
+
end
|
|
1894
|
+
local function breakpointKey(scriptPath, line)
|
|
1895
|
+
return `{scriptPath}:{line}`
|
|
1896
|
+
end
|
|
1897
|
+
local function computeInstanceId()
|
|
1898
|
+
if game.PlaceId ~= 0 then
|
|
1899
|
+
return `place:{tostring(game.PlaceId)}`
|
|
1900
|
+
end
|
|
1901
|
+
local existing = ServerStorage:GetAttribute(MCP_PLACE_ID_ATTRIBUTE)
|
|
1902
|
+
if type(existing) == "string" and existing ~= "" then
|
|
1903
|
+
return `anon:{existing}`
|
|
1904
|
+
end
|
|
1905
|
+
local fresh = HttpService:GenerateGUID(false)
|
|
1906
|
+
pcall(function()
|
|
1907
|
+
return ServerStorage:SetAttribute(MCP_PLACE_ID_ATTRIBUTE, fresh)
|
|
1908
|
+
end)
|
|
1909
|
+
return `anon:{fresh}`
|
|
1910
|
+
end
|
|
1911
|
+
local function detectRole()
|
|
1912
|
+
if not RunService:IsRunning() then
|
|
1913
|
+
return "edit"
|
|
1914
|
+
end
|
|
1915
|
+
if RunService:IsServer() then
|
|
1916
|
+
return "server"
|
|
1917
|
+
end
|
|
1918
|
+
return "client"
|
|
1919
|
+
end
|
|
1920
|
+
local function requestedRole(requestData)
|
|
1921
|
+
local ___mcp_target_role = requestData.__mcp_target_role
|
|
1922
|
+
local _condition = type(___mcp_target_role) == "string"
|
|
1923
|
+
if _condition then
|
|
1924
|
+
_condition = requestData.__mcp_target_role ~= ""
|
|
1925
|
+
end
|
|
1926
|
+
return if _condition then requestData.__mcp_target_role else detectRole()
|
|
1927
|
+
end
|
|
1928
|
+
local function registryScope(requestData)
|
|
1929
|
+
local ___mcp_instance_id = requestData.__mcp_instance_id
|
|
1930
|
+
local _condition = type(___mcp_instance_id) == "string"
|
|
1931
|
+
if _condition then
|
|
1932
|
+
_condition = requestData.__mcp_instance_id ~= ""
|
|
1933
|
+
end
|
|
1934
|
+
local instanceId = if _condition then requestData.__mcp_instance_id else computeInstanceId()
|
|
1935
|
+
local role = requestedRole(requestData)
|
|
1936
|
+
return {
|
|
1937
|
+
key = `{REGISTRY_KEY_PREFIX}{instanceId}:{role}`,
|
|
1938
|
+
}
|
|
1939
|
+
end
|
|
1940
|
+
local function readSetting(key)
|
|
1941
|
+
if not pluginRef then
|
|
1942
|
+
return nil
|
|
1943
|
+
end
|
|
1944
|
+
local ok, value = pcall(function()
|
|
1945
|
+
return pluginRef:GetSetting(key)
|
|
1946
|
+
end)
|
|
1947
|
+
return if ok then value else nil
|
|
1948
|
+
end
|
|
1949
|
+
local function writeSetting(key, value)
|
|
1950
|
+
if not pluginRef then
|
|
1951
|
+
return false
|
|
1952
|
+
end
|
|
1953
|
+
local ok = pcall(function()
|
|
1954
|
+
return pluginRef:SetSetting(key, value)
|
|
1955
|
+
end)
|
|
1956
|
+
return ok
|
|
1957
|
+
end
|
|
1958
|
+
local function decodePersistedBreakpointEntry(value)
|
|
1959
|
+
local _value = value
|
|
1960
|
+
if not (type(_value) == "table") then
|
|
1961
|
+
return nil
|
|
1962
|
+
end
|
|
1963
|
+
local data = value
|
|
1964
|
+
local _script_path = data.script_path
|
|
1965
|
+
local _condition = not (type(_script_path) == "string")
|
|
1966
|
+
if not _condition then
|
|
1967
|
+
_condition = data.script_path == ""
|
|
1968
|
+
end
|
|
1969
|
+
if _condition then
|
|
1970
|
+
return nil
|
|
1971
|
+
end
|
|
1972
|
+
local _line = data.line
|
|
1973
|
+
local _condition_1 = not (type(_line) == "number")
|
|
1974
|
+
if not _condition_1 then
|
|
1975
|
+
_condition_1 = data.line < 1
|
|
1976
|
+
end
|
|
1977
|
+
if _condition_1 then
|
|
1978
|
+
return nil
|
|
1979
|
+
end
|
|
1980
|
+
return {
|
|
1981
|
+
script_path = data.script_path,
|
|
1982
|
+
line = math.floor(data.line),
|
|
1983
|
+
}
|
|
1984
|
+
end
|
|
1985
|
+
local function loadRegistry(requestData)
|
|
1986
|
+
local scope = registryScope(requestData)
|
|
1987
|
+
if loadedRegistryKey ~= scope.key then
|
|
1988
|
+
table.clear(breakpoints)
|
|
1989
|
+
loadedRegistryKey = scope.key
|
|
1990
|
+
loadedRegistryFromSettings = false
|
|
1991
|
+
end
|
|
1992
|
+
if loadedRegistryFromSettings then
|
|
1993
|
+
return scope
|
|
1994
|
+
end
|
|
1995
|
+
loadedRegistryFromSettings = true
|
|
1996
|
+
local stored = readSetting(scope.key)
|
|
1997
|
+
if stored == nil then
|
|
1998
|
+
return scope
|
|
1999
|
+
end
|
|
2000
|
+
local decoded = stored
|
|
2001
|
+
if type(stored) == "string" then
|
|
2002
|
+
local ok, result = pcall(function()
|
|
2003
|
+
return HttpService:JSONDecode(stored)
|
|
2004
|
+
end)
|
|
2005
|
+
if not ok then
|
|
2006
|
+
return scope
|
|
2007
|
+
end
|
|
2008
|
+
decoded = result
|
|
2009
|
+
end
|
|
2010
|
+
local _decoded = decoded
|
|
2011
|
+
if not (type(_decoded) == "table") then
|
|
2012
|
+
return scope
|
|
2013
|
+
end
|
|
2014
|
+
table.clear(breakpoints)
|
|
2015
|
+
for _, item in decoded do
|
|
2016
|
+
local entry = decodePersistedBreakpointEntry(item)
|
|
2017
|
+
if entry then
|
|
2018
|
+
local _arg0 = breakpointKey(entry.script_path, entry.line)
|
|
2019
|
+
breakpoints[_arg0] = entry
|
|
2020
|
+
end
|
|
2021
|
+
end
|
|
2022
|
+
return scope
|
|
2023
|
+
end
|
|
2024
|
+
local function persistRegistry(scope)
|
|
2025
|
+
if not pluginRef then
|
|
2026
|
+
return {
|
|
2027
|
+
ok = false,
|
|
2028
|
+
error = "Plugin settings are unavailable; managed breakpoint registry is memory-only.",
|
|
2029
|
+
}
|
|
2030
|
+
end
|
|
2031
|
+
local out = {}
|
|
2032
|
+
for _, entry in breakpoints do
|
|
2033
|
+
local _arg0 = {
|
|
2034
|
+
script_path = entry.script_path,
|
|
2035
|
+
line = entry.line,
|
|
2036
|
+
}
|
|
2037
|
+
table.insert(out, _arg0)
|
|
2038
|
+
end
|
|
2039
|
+
local encodedOk, encoded = pcall(function()
|
|
2040
|
+
return HttpService:JSONEncode(out)
|
|
2041
|
+
end)
|
|
2042
|
+
if not encodedOk or not (type(encoded) == "string") then
|
|
2043
|
+
return {
|
|
2044
|
+
ok = false,
|
|
2045
|
+
error = `Failed to encode managed breakpoint registry: {tostring(encoded)}`,
|
|
2046
|
+
}
|
|
2047
|
+
end
|
|
2048
|
+
if not writeSetting(scope.key, encoded) then
|
|
2049
|
+
return {
|
|
2050
|
+
ok = false,
|
|
2051
|
+
error = "Failed to persist managed breakpoint registry with plugin:SetSetting.",
|
|
2052
|
+
}
|
|
2053
|
+
end
|
|
2054
|
+
local stored = readSetting(scope.key)
|
|
2055
|
+
if stored ~= encoded then
|
|
2056
|
+
return {
|
|
2057
|
+
ok = false,
|
|
2058
|
+
error = "Failed to verify managed breakpoint registry persistence after plugin:SetSetting.",
|
|
2059
|
+
}
|
|
2060
|
+
end
|
|
2061
|
+
return {
|
|
2062
|
+
ok = true,
|
|
2063
|
+
}
|
|
2064
|
+
end
|
|
2065
|
+
local function attachPersistenceWarning(response, persist)
|
|
2066
|
+
if not persist.ok then
|
|
2067
|
+
response.managed_registry_persisted = false
|
|
2068
|
+
response.registry_error = persist.error
|
|
2069
|
+
end
|
|
2070
|
+
return response
|
|
2071
|
+
end
|
|
2072
|
+
local function serviceError(message)
|
|
2073
|
+
local _object = {
|
|
2074
|
+
error = "script_debugger_unavailable",
|
|
2075
|
+
}
|
|
2076
|
+
local _left = "message"
|
|
2077
|
+
local _condition = message
|
|
2078
|
+
if _condition == nil then
|
|
2079
|
+
_condition = "ScriptDebuggerService is unavailable. Enable the Studio Debugger Luau API beta feature and restart Studio."
|
|
2080
|
+
end
|
|
2081
|
+
_object[_left] = _condition
|
|
2082
|
+
_object.betaFeatureRequired = true
|
|
2083
|
+
return _object
|
|
2084
|
+
end
|
|
2085
|
+
local function operationError(errorCode, operation, raw)
|
|
2086
|
+
return {
|
|
2087
|
+
error = errorCode,
|
|
2088
|
+
message = `{operation} failed. The breakpoints tool requires the Studio Debugger Luau API beta feature. ` .. "Enable it in Studio Beta Features and restart/reload Studio, then retry.",
|
|
2089
|
+
rawMessage = tostring(raw),
|
|
2090
|
+
betaFeatureRequired = true,
|
|
2091
|
+
}
|
|
2092
|
+
end
|
|
2093
|
+
local function getService()
|
|
2094
|
+
local provider = game
|
|
2095
|
+
local ok, service = pcall(function()
|
|
2096
|
+
return provider:GetService("ScriptDebuggerService")
|
|
2097
|
+
end)
|
|
2098
|
+
if not ok or not service then
|
|
2099
|
+
return serviceError(`ScriptDebuggerService unavailable: {tostring(service)}`)
|
|
2100
|
+
end
|
|
2101
|
+
return service
|
|
2102
|
+
end
|
|
2103
|
+
local function luauStringLiteral(value)
|
|
2104
|
+
local escaped = (string.gsub(value, "\\", "\\\\"))
|
|
2105
|
+
escaped = (string.gsub(escaped, "\n", "\\n"))
|
|
2106
|
+
escaped = (string.gsub(escaped, "\r", "\\r"))
|
|
2107
|
+
escaped = (string.gsub(escaped, "\t", "\\t"))
|
|
2108
|
+
escaped = (string.gsub(escaped, '"', '\\"'))
|
|
2109
|
+
return `"{escaped}"`
|
|
2110
|
+
end
|
|
2111
|
+
local function buildLogMessage(scriptPath, line, logMessage)
|
|
2112
|
+
local prefix = { luauStringLiteral(LOG_PREFIX), luauStringLiteral(`{scriptPath}:{line}`) }
|
|
2113
|
+
local _logMessage = logMessage
|
|
2114
|
+
local _condition = type(_logMessage) == "string"
|
|
2115
|
+
if _condition then
|
|
2116
|
+
_condition = logMessage ~= ""
|
|
2117
|
+
end
|
|
2118
|
+
if _condition then
|
|
2119
|
+
local _logMessage_1 = logMessage
|
|
2120
|
+
table.insert(prefix, _logMessage_1)
|
|
2121
|
+
end
|
|
2122
|
+
return table.concat(prefix, ", ")
|
|
2123
|
+
end
|
|
2124
|
+
local function listBreakpoints(requestData)
|
|
2125
|
+
loadRegistry(requestData)
|
|
2126
|
+
local out = {}
|
|
2127
|
+
for _, entry in breakpoints do
|
|
2128
|
+
table.insert(out, entry)
|
|
2129
|
+
end
|
|
2130
|
+
return {
|
|
2131
|
+
breakpoints = out,
|
|
2132
|
+
count = #out,
|
|
2133
|
+
}
|
|
2134
|
+
end
|
|
2135
|
+
local function setBreakpoint(requestData)
|
|
2136
|
+
local scope = loadRegistry(requestData)
|
|
2137
|
+
local serviceOrError = getService()
|
|
2138
|
+
local _value = serviceOrError.IsA
|
|
2139
|
+
if not (_value ~= 0 and _value == _value and _value ~= "" and _value) then
|
|
2140
|
+
return serviceOrError
|
|
2141
|
+
end
|
|
2142
|
+
local service = serviceOrError
|
|
2143
|
+
local scriptPath = requestData.script_path
|
|
2144
|
+
local lineRaw = requestData.line
|
|
2145
|
+
if not (type(scriptPath) == "string") or scriptPath == "" or not (type(lineRaw) == "number") then
|
|
2146
|
+
return {
|
|
2147
|
+
error = "invalid_args",
|
|
2148
|
+
message = "breakpoints action=set requires script_path and line",
|
|
2149
|
+
}
|
|
2150
|
+
end
|
|
2151
|
+
local requestedLine = math.floor(lineRaw)
|
|
2152
|
+
if requestedLine < 1 then
|
|
2153
|
+
return {
|
|
2154
|
+
error = "invalid_line",
|
|
2155
|
+
message = "line must be a 1-based positive number",
|
|
2156
|
+
}
|
|
2157
|
+
end
|
|
2158
|
+
local instance = getInstanceByPath(scriptPath)
|
|
2159
|
+
if not instance then
|
|
2160
|
+
return {
|
|
2161
|
+
error = "script_not_found",
|
|
2162
|
+
script_path = scriptPath,
|
|
2163
|
+
}
|
|
2164
|
+
end
|
|
2165
|
+
if not instance:IsA("LuaSourceContainer") then
|
|
2166
|
+
return {
|
|
2167
|
+
error = "not_a_script",
|
|
2168
|
+
message = `{scriptPath} is {instance.ClassName}, not a LuaSourceContainer`,
|
|
2169
|
+
script_path = scriptPath,
|
|
2170
|
+
}
|
|
2171
|
+
end
|
|
2172
|
+
local _log_message = requestData.log_message
|
|
2173
|
+
local rawLogMessage = if type(_log_message) == "string" then requestData.log_message else nil
|
|
2174
|
+
local hasLogMessage = rawLogMessage ~= nil and rawLogMessage ~= ""
|
|
2175
|
+
local _continue_execution = requestData.continue_execution
|
|
2176
|
+
local continueExecution = if type(_continue_execution) == "boolean" then requestData.continue_execution else hasLogMessage
|
|
2177
|
+
local _enabled = requestData.enabled
|
|
2178
|
+
local enabled = if type(_enabled) == "boolean" then requestData.enabled else true
|
|
2179
|
+
local effectiveLogMessage = if hasLogMessage or continueExecution then buildLogMessage(scriptPath, requestedLine, rawLogMessage) else nil
|
|
2180
|
+
local spec = {
|
|
2181
|
+
Line = requestedLine,
|
|
2182
|
+
Enabled = enabled,
|
|
2183
|
+
ContinueExecution = continueExecution,
|
|
2184
|
+
}
|
|
2185
|
+
local _condition = requestData.condition
|
|
2186
|
+
local _condition_1 = type(_condition) == "string"
|
|
2187
|
+
if _condition_1 then
|
|
2188
|
+
_condition_1 = requestData.condition ~= ""
|
|
2189
|
+
end
|
|
2190
|
+
if _condition_1 then
|
|
2191
|
+
spec.Condition = requestData.condition
|
|
2192
|
+
end
|
|
2193
|
+
if effectiveLogMessage ~= nil then
|
|
2194
|
+
spec.LogMessage = effectiveLogMessage
|
|
2195
|
+
end
|
|
2196
|
+
local ok, result = pcall(function()
|
|
2197
|
+
return service:AddBreakpoint(instance, spec)
|
|
2198
|
+
end)
|
|
2199
|
+
if not ok then
|
|
2200
|
+
return operationError("add_breakpoint_failed", "ScriptDebuggerService:AddBreakpoint", result)
|
|
2201
|
+
end
|
|
2202
|
+
local breakpointResult = result
|
|
2203
|
+
local _line = breakpointResult.Line
|
|
2204
|
+
local actualLine = if type(_line) == "number" then breakpointResult.Line else requestedLine
|
|
2205
|
+
local _verified = breakpointResult.Verified
|
|
2206
|
+
local verified = if type(_verified) == "boolean" then breakpointResult.Verified else nil
|
|
2207
|
+
local _message = breakpointResult.Message
|
|
2208
|
+
local message = if type(_message) == "string" then breakpointResult.Message else nil
|
|
2209
|
+
local entry = {
|
|
2210
|
+
script_path = scriptPath,
|
|
2211
|
+
line = actualLine,
|
|
2212
|
+
requested_line = if actualLine ~= requestedLine then requestedLine else nil,
|
|
2213
|
+
enabled = enabled,
|
|
2214
|
+
condition = spec.Condition,
|
|
2215
|
+
log_message = rawLogMessage,
|
|
2216
|
+
continue_execution = continueExecution,
|
|
2217
|
+
verified = if verified == false then false else nil,
|
|
2218
|
+
message = message,
|
|
2219
|
+
created_at = DateTime.now().UnixTimestampMillis,
|
|
2220
|
+
}
|
|
2221
|
+
local _arg0 = breakpointKey(scriptPath, actualLine)
|
|
2222
|
+
breakpoints[_arg0] = entry
|
|
2223
|
+
return attachPersistenceWarning({
|
|
2224
|
+
ok = true,
|
|
2225
|
+
breakpoint = entry,
|
|
2226
|
+
}, persistRegistry(scope))
|
|
2227
|
+
end
|
|
2228
|
+
local function removeBreakpoint(requestData)
|
|
2229
|
+
local scope = loadRegistry(requestData)
|
|
2230
|
+
local serviceOrError = getService()
|
|
2231
|
+
local _value = serviceOrError.IsA
|
|
2232
|
+
if not (_value ~= 0 and _value == _value and _value ~= "" and _value) then
|
|
2233
|
+
return serviceOrError
|
|
2234
|
+
end
|
|
2235
|
+
local service = serviceOrError
|
|
2236
|
+
local scriptPath = requestData.script_path
|
|
2237
|
+
local lineRaw = requestData.line
|
|
2238
|
+
if not (type(scriptPath) == "string") or scriptPath == "" or not (type(lineRaw) == "number") then
|
|
2239
|
+
return {
|
|
2240
|
+
error = "invalid_args",
|
|
2241
|
+
message = "breakpoints action=remove requires script_path and line",
|
|
2242
|
+
}
|
|
2243
|
+
end
|
|
2244
|
+
local line = math.floor(lineRaw)
|
|
2245
|
+
if line < 1 then
|
|
2246
|
+
return {
|
|
2247
|
+
error = "invalid_line",
|
|
2248
|
+
message = "line must be a 1-based positive number",
|
|
2249
|
+
}
|
|
2250
|
+
end
|
|
2251
|
+
local instance = getInstanceByPath(scriptPath)
|
|
2252
|
+
if not instance then
|
|
2253
|
+
return {
|
|
2254
|
+
error = "script_not_found",
|
|
2255
|
+
script_path = scriptPath,
|
|
2256
|
+
}
|
|
2257
|
+
end
|
|
2258
|
+
if not instance:IsA("LuaSourceContainer") then
|
|
2259
|
+
return {
|
|
2260
|
+
error = "not_a_script",
|
|
2261
|
+
message = `{scriptPath} is {instance.ClassName}, not a LuaSourceContainer`,
|
|
2262
|
+
script_path = scriptPath,
|
|
2263
|
+
}
|
|
2264
|
+
end
|
|
2265
|
+
local ok, removed = pcall(function()
|
|
2266
|
+
return service:RemoveBreakpoint(instance, line)
|
|
2267
|
+
end)
|
|
2268
|
+
if not ok then
|
|
2269
|
+
return operationError("remove_breakpoint_failed", "ScriptDebuggerService:RemoveBreakpoint", removed)
|
|
2270
|
+
end
|
|
2271
|
+
local _arg0 = breakpointKey(scriptPath, line)
|
|
2272
|
+
breakpoints[_arg0] = nil
|
|
2273
|
+
return attachPersistenceWarning({
|
|
2274
|
+
ok = true,
|
|
2275
|
+
removed = removed,
|
|
2276
|
+
script_path = scriptPath,
|
|
2277
|
+
line = line,
|
|
2278
|
+
}, persistRegistry(scope))
|
|
2279
|
+
end
|
|
2280
|
+
local function clearManagedBreakpoints(requestData)
|
|
2281
|
+
local scope = loadRegistry(requestData)
|
|
2282
|
+
local serviceOrError = getService()
|
|
2283
|
+
local _value = serviceOrError.IsA
|
|
2284
|
+
if not (_value ~= 0 and _value == _value and _value ~= "" and _value) then
|
|
2285
|
+
return serviceOrError
|
|
2286
|
+
end
|
|
2287
|
+
local service = serviceOrError
|
|
2288
|
+
local cleared = 0
|
|
2289
|
+
local errors = {}
|
|
2290
|
+
for key, entry in breakpoints do
|
|
2291
|
+
local instance = getInstanceByPath(entry.script_path)
|
|
2292
|
+
if not instance or not instance:IsA("LuaSourceContainer") then
|
|
2293
|
+
breakpoints[key] = nil
|
|
2294
|
+
cleared += 1
|
|
2295
|
+
continue
|
|
2296
|
+
end
|
|
2297
|
+
local ok, removedOrError = pcall(function()
|
|
2298
|
+
return service:RemoveBreakpoint(instance, entry.line)
|
|
2299
|
+
end)
|
|
2300
|
+
if ok then
|
|
2301
|
+
breakpoints[key] = nil
|
|
2302
|
+
cleared += 1
|
|
2303
|
+
else
|
|
2304
|
+
local _arg0 = {
|
|
2305
|
+
script_path = entry.script_path,
|
|
2306
|
+
line = entry.line,
|
|
2307
|
+
error = tostring(removedOrError),
|
|
2308
|
+
}
|
|
2309
|
+
table.insert(errors, _arg0)
|
|
2310
|
+
end
|
|
2311
|
+
end
|
|
2312
|
+
if #errors > 0 then
|
|
2313
|
+
return {
|
|
2314
|
+
ok = false,
|
|
2315
|
+
cleared = cleared,
|
|
2316
|
+
errors = errors,
|
|
2317
|
+
}
|
|
2318
|
+
end
|
|
2319
|
+
return attachPersistenceWarning({
|
|
2320
|
+
ok = true,
|
|
2321
|
+
cleared = cleared,
|
|
2322
|
+
}, persistRegistry(scope))
|
|
2323
|
+
end
|
|
2324
|
+
local function clearAllBreakpoints(requestData)
|
|
2325
|
+
local scope = loadRegistry(requestData)
|
|
2326
|
+
local serviceOrError = getService()
|
|
2327
|
+
local _value = serviceOrError.IsA
|
|
2328
|
+
if not (_value ~= 0 and _value == _value and _value ~= "" and _value) then
|
|
2329
|
+
return serviceOrError
|
|
2330
|
+
end
|
|
2331
|
+
local service = serviceOrError
|
|
2332
|
+
-- ▼ ReadonlyMap.size ▼
|
|
2333
|
+
local _size = 0
|
|
2334
|
+
for _ in breakpoints do
|
|
2335
|
+
_size += 1
|
|
2336
|
+
end
|
|
2337
|
+
-- ▲ ReadonlyMap.size ▲
|
|
2338
|
+
local managedCount = _size
|
|
2339
|
+
local ok, err = pcall(function()
|
|
2340
|
+
return service:ClearBreakpoints()
|
|
2341
|
+
end)
|
|
2342
|
+
if not ok then
|
|
2343
|
+
return operationError("clear_breakpoints_failed", "ScriptDebuggerService:ClearBreakpoints", err)
|
|
2344
|
+
end
|
|
2345
|
+
table.clear(breakpoints)
|
|
2346
|
+
return attachPersistenceWarning({
|
|
2347
|
+
ok = true,
|
|
2348
|
+
cleared_managed = managedCount,
|
|
2349
|
+
}, persistRegistry(scope))
|
|
2350
|
+
end
|
|
2351
|
+
local function clearBreakpoints(requestData)
|
|
2352
|
+
if requestData.clear_all == true then
|
|
2353
|
+
return clearAllBreakpoints(requestData)
|
|
2354
|
+
end
|
|
2355
|
+
return clearManagedBreakpoints(requestData)
|
|
2356
|
+
end
|
|
2357
|
+
local function breakpointsTool(requestData)
|
|
2358
|
+
local action = requestData.action
|
|
2359
|
+
if not (type(action) == "string") or action == "" then
|
|
2360
|
+
return {
|
|
2361
|
+
error = "invalid_args",
|
|
2362
|
+
message = "breakpoints requires action=set|remove|clear|list",
|
|
2363
|
+
}
|
|
2364
|
+
end
|
|
2365
|
+
repeat
|
|
2366
|
+
if action == "set" then
|
|
2367
|
+
return setBreakpoint(requestData)
|
|
2368
|
+
end
|
|
2369
|
+
if action == "remove" then
|
|
2370
|
+
return removeBreakpoint(requestData)
|
|
2371
|
+
end
|
|
2372
|
+
if action == "clear" then
|
|
2373
|
+
return clearBreakpoints(requestData)
|
|
2374
|
+
end
|
|
2375
|
+
if action == "list" then
|
|
2376
|
+
return listBreakpoints(requestData)
|
|
2377
|
+
end
|
|
2378
|
+
return {
|
|
2379
|
+
error = "unknown_action",
|
|
2380
|
+
message = `breakpoints action must be one of: set, remove, clear, list (got {action})`,
|
|
2381
|
+
}
|
|
2382
|
+
until true
|
|
2383
|
+
end
|
|
2384
|
+
return {
|
|
2385
|
+
breakpoints = breakpointsTool,
|
|
2386
|
+
init = init,
|
|
2387
|
+
}
|
|
2388
|
+
]]></string>
|
|
2389
|
+
</Properties>
|
|
2390
|
+
</Item>
|
|
2391
|
+
<Item class="ModuleScript" referent="8">
|
|
1860
2392
|
<Properties>
|
|
1861
2393
|
<string name="Name">BuildHandlers</string>
|
|
1862
2394
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -2421,7 +2953,7 @@ return {
|
|
|
2421
2953
|
]]></string>
|
|
2422
2954
|
</Properties>
|
|
2423
2955
|
</Item>
|
|
2424
|
-
<Item class="ModuleScript" referent="
|
|
2956
|
+
<Item class="ModuleScript" referent="9">
|
|
2425
2957
|
<Properties>
|
|
2426
2958
|
<string name="Name">CaptureHandlers</string>
|
|
2427
2959
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -2636,7 +3168,7 @@ return {
|
|
|
2636
3168
|
]]></string>
|
|
2637
3169
|
</Properties>
|
|
2638
3170
|
</Item>
|
|
2639
|
-
<Item class="ModuleScript" referent="
|
|
3171
|
+
<Item class="ModuleScript" referent="10">
|
|
2640
3172
|
<Properties>
|
|
2641
3173
|
<string name="Name">EvalRuntimeHandlers</string>
|
|
2642
3174
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -2790,7 +3322,7 @@ return {
|
|
|
2790
3322
|
]]></string>
|
|
2791
3323
|
</Properties>
|
|
2792
3324
|
</Item>
|
|
2793
|
-
<Item class="ModuleScript" referent="
|
|
3325
|
+
<Item class="ModuleScript" referent="11">
|
|
2794
3326
|
<Properties>
|
|
2795
3327
|
<string name="Name">InputHandlers</string>
|
|
2796
3328
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -2990,7 +3522,7 @@ return {
|
|
|
2990
3522
|
]]></string>
|
|
2991
3523
|
</Properties>
|
|
2992
3524
|
</Item>
|
|
2993
|
-
<Item class="ModuleScript" referent="
|
|
3525
|
+
<Item class="ModuleScript" referent="12">
|
|
2994
3526
|
<Properties>
|
|
2995
3527
|
<string name="Name">InstanceHandlers</string>
|
|
2996
3528
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3497,7 +4029,7 @@ return {
|
|
|
3497
4029
|
]]></string>
|
|
3498
4030
|
</Properties>
|
|
3499
4031
|
</Item>
|
|
3500
|
-
<Item class="ModuleScript" referent="
|
|
4032
|
+
<Item class="ModuleScript" referent="13">
|
|
3501
4033
|
<Properties>
|
|
3502
4034
|
<string name="Name">LogHandlers</string>
|
|
3503
4035
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3523,7 +4055,7 @@ return {
|
|
|
3523
4055
|
]]></string>
|
|
3524
4056
|
</Properties>
|
|
3525
4057
|
</Item>
|
|
3526
|
-
<Item class="ModuleScript" referent="
|
|
4058
|
+
<Item class="ModuleScript" referent="14">
|
|
3527
4059
|
<Properties>
|
|
3528
4060
|
<string name="Name">MemoryHandlers</string>
|
|
3529
4061
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3591,7 +4123,7 @@ return {
|
|
|
3591
4123
|
]]></string>
|
|
3592
4124
|
</Properties>
|
|
3593
4125
|
</Item>
|
|
3594
|
-
<Item class="ModuleScript" referent="
|
|
4126
|
+
<Item class="ModuleScript" referent="15">
|
|
3595
4127
|
<Properties>
|
|
3596
4128
|
<string name="Name">MetadataHandlers</string>
|
|
3597
4129
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -4130,7 +4662,7 @@ return {
|
|
|
4130
4662
|
]]></string>
|
|
4131
4663
|
</Properties>
|
|
4132
4664
|
</Item>
|
|
4133
|
-
<Item class="ModuleScript" referent="
|
|
4665
|
+
<Item class="ModuleScript" referent="16">
|
|
4134
4666
|
<Properties>
|
|
4135
4667
|
<string name="Name">PropertyHandlers</string>
|
|
4136
4668
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -4382,7 +4914,7 @@ return {
|
|
|
4382
4914
|
]]></string>
|
|
4383
4915
|
</Properties>
|
|
4384
4916
|
</Item>
|
|
4385
|
-
<Item class="ModuleScript" referent="
|
|
4917
|
+
<Item class="ModuleScript" referent="17">
|
|
4386
4918
|
<Properties>
|
|
4387
4919
|
<string name="Name">QueryHandlers</string>
|
|
4388
4920
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5330,79 +5862,21 @@ local function compareInstances(requestData)
|
|
|
5330
5862
|
b = valB,
|
|
5331
5863
|
}
|
|
5332
5864
|
end
|
|
5333
|
-
elseif okA then
|
|
5334
|
-
table.insert(onlyA, prop)
|
|
5335
|
-
elseif okB then
|
|
5336
|
-
table.insert(onlyB, prop)
|
|
5337
|
-
end
|
|
5338
|
-
end
|
|
5339
|
-
return {
|
|
5340
|
-
instancePathA = instancePathA,
|
|
5341
|
-
instancePathB = instancePathB,
|
|
5342
|
-
classNameA = instA.ClassName,
|
|
5343
|
-
classNameB = instB.ClassName,
|
|
5344
|
-
matching = matching,
|
|
5345
|
-
differing = differing,
|
|
5346
|
-
onlyA = onlyA,
|
|
5347
|
-
onlyB = onlyB,
|
|
5348
|
-
}
|
|
5349
|
-
end
|
|
5350
|
-
local function getOutputLog(requestData)
|
|
5351
|
-
local _condition = (requestData.maxEntries)
|
|
5352
|
-
if _condition == nil then
|
|
5353
|
-
_condition = 100
|
|
5354
|
-
end
|
|
5355
|
-
local maxEntries = _condition
|
|
5356
|
-
local messageTypeFilter = requestData.messageType
|
|
5357
|
-
local success, result = pcall(function()
|
|
5358
|
-
local LogService = game:GetService("LogService")
|
|
5359
|
-
local history = LogService:GetLogHistory()
|
|
5360
|
-
local allEntries = {}
|
|
5361
|
-
for _, entry in history do
|
|
5362
|
-
local msgType = tostring(entry.messageType)
|
|
5363
|
-
local _condition_1 = messageTypeFilter
|
|
5364
|
-
if _condition_1 ~= "" and _condition_1 then
|
|
5365
|
-
_condition_1 = msgType ~= messageTypeFilter
|
|
5366
|
-
end
|
|
5367
|
-
if _condition_1 ~= "" and _condition_1 then
|
|
5368
|
-
continue
|
|
5369
|
-
end
|
|
5370
|
-
local _arg0 = {
|
|
5371
|
-
message = entry.message,
|
|
5372
|
-
messageType = msgType,
|
|
5373
|
-
timestamp = entry.timestamp,
|
|
5374
|
-
}
|
|
5375
|
-
table.insert(allEntries, _arg0)
|
|
5376
|
-
end
|
|
5377
|
-
local startIdx = math.max(0, #allEntries - maxEntries)
|
|
5378
|
-
local finalEntries = {}
|
|
5379
|
-
do
|
|
5380
|
-
local i = startIdx
|
|
5381
|
-
local _shouldIncrement = false
|
|
5382
|
-
while true do
|
|
5383
|
-
if _shouldIncrement then
|
|
5384
|
-
i += 1
|
|
5385
|
-
else
|
|
5386
|
-
_shouldIncrement = true
|
|
5387
|
-
end
|
|
5388
|
-
if not (i < #allEntries) then
|
|
5389
|
-
break
|
|
5390
|
-
end
|
|
5391
|
-
local _arg0 = allEntries[i + 1]
|
|
5392
|
-
table.insert(finalEntries, _arg0)
|
|
5393
|
-
end
|
|
5865
|
+
elseif okA then
|
|
5866
|
+
table.insert(onlyA, prop)
|
|
5867
|
+
elseif okB then
|
|
5868
|
+
table.insert(onlyB, prop)
|
|
5394
5869
|
end
|
|
5395
|
-
return {
|
|
5396
|
-
entries = finalEntries,
|
|
5397
|
-
count = #finalEntries,
|
|
5398
|
-
totalAvailable = #allEntries,
|
|
5399
|
-
}
|
|
5400
|
-
end)
|
|
5401
|
-
if success then
|
|
5402
|
-
return result
|
|
5403
5870
|
end
|
|
5404
5871
|
return {
|
|
5405
|
-
|
|
5872
|
+
instancePathA = instancePathA,
|
|
5873
|
+
instancePathB = instancePathB,
|
|
5874
|
+
classNameA = instA.ClassName,
|
|
5875
|
+
classNameB = instB.ClassName,
|
|
5876
|
+
matching = matching,
|
|
5877
|
+
differing = differing,
|
|
5878
|
+
onlyA = onlyA,
|
|
5879
|
+
onlyB = onlyB,
|
|
5406
5880
|
}
|
|
5407
5881
|
end
|
|
5408
5882
|
return {
|
|
@@ -5419,12 +5893,11 @@ return {
|
|
|
5419
5893
|
grepScripts = grepScripts,
|
|
5420
5894
|
getDescendants = getDescendants,
|
|
5421
5895
|
compareInstances = compareInstances,
|
|
5422
|
-
getOutputLog = getOutputLog,
|
|
5423
5896
|
}
|
|
5424
5897
|
]]></string>
|
|
5425
5898
|
</Properties>
|
|
5426
5899
|
</Item>
|
|
5427
|
-
<Item class="ModuleScript" referent="
|
|
5900
|
+
<Item class="ModuleScript" referent="18">
|
|
5428
5901
|
<Properties>
|
|
5429
5902
|
<string name="Name">SceneAnalysisHandlers</string>
|
|
5430
5903
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5673,7 +6146,7 @@ return {
|
|
|
5673
6146
|
]]></string>
|
|
5674
6147
|
</Properties>
|
|
5675
6148
|
</Item>
|
|
5676
|
-
<Item class="ModuleScript" referent="
|
|
6149
|
+
<Item class="ModuleScript" referent="19">
|
|
5677
6150
|
<Properties>
|
|
5678
6151
|
<string name="Name">ScriptHandlers</string>
|
|
5679
6152
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -6369,7 +6842,470 @@ return {
|
|
|
6369
6842
|
]]></string>
|
|
6370
6843
|
</Properties>
|
|
6371
6844
|
</Item>
|
|
6372
|
-
<Item class="ModuleScript" referent="
|
|
6845
|
+
<Item class="ModuleScript" referent="20">
|
|
6846
|
+
<Properties>
|
|
6847
|
+
<string name="Name">ScriptProfilerHandlers</string>
|
|
6848
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
6849
|
+
local HttpService = game:GetService("HttpService")
|
|
6850
|
+
local Players = game:GetService("Players")
|
|
6851
|
+
local RunService = game:GetService("RunService")
|
|
6852
|
+
local DEFAULT_DURATION_MS = 1000
|
|
6853
|
+
local MIN_DURATION_MS = 100
|
|
6854
|
+
local MAX_DURATION_MS = 15000
|
|
6855
|
+
local DEFAULT_FREQUENCY = 1000
|
|
6856
|
+
local DEFAULT_MAX_FUNCTIONS = 20
|
|
6857
|
+
local function getProfilerService()
|
|
6858
|
+
local provider = game
|
|
6859
|
+
local ok, service = pcall(function()
|
|
6860
|
+
return provider:GetService("ScriptProfilerService")
|
|
6861
|
+
end)
|
|
6862
|
+
if not ok or not service then
|
|
6863
|
+
return {
|
|
6864
|
+
error = "script_profiler_unavailable",
|
|
6865
|
+
message = `ScriptProfilerService is unavailable: {tostring(service)}`,
|
|
6866
|
+
}
|
|
6867
|
+
end
|
|
6868
|
+
return service
|
|
6869
|
+
end
|
|
6870
|
+
local function normalizeDurationMs(value)
|
|
6871
|
+
local _value = value
|
|
6872
|
+
if not (type(_value) == "number") then
|
|
6873
|
+
return DEFAULT_DURATION_MS
|
|
6874
|
+
end
|
|
6875
|
+
return math.clamp(math.floor(value), MIN_DURATION_MS, MAX_DURATION_MS)
|
|
6876
|
+
end
|
|
6877
|
+
local function normalizeFrequency(value)
|
|
6878
|
+
local _value = value
|
|
6879
|
+
if not (type(_value) == "number") then
|
|
6880
|
+
return DEFAULT_FREQUENCY
|
|
6881
|
+
end
|
|
6882
|
+
return math.clamp(math.floor(value), 1, 10000)
|
|
6883
|
+
end
|
|
6884
|
+
local function normalizeMaxFunctions(value)
|
|
6885
|
+
local _value = value
|
|
6886
|
+
if not (type(_value) == "number") then
|
|
6887
|
+
return DEFAULT_MAX_FUNCTIONS
|
|
6888
|
+
end
|
|
6889
|
+
return math.clamp(math.floor(value), 1, 100)
|
|
6890
|
+
end
|
|
6891
|
+
local function normalizeMinTotalUs(requestData)
|
|
6892
|
+
local value = requestData.min_total_us
|
|
6893
|
+
if type(value) == "number" then
|
|
6894
|
+
return math.max(0, value)
|
|
6895
|
+
end
|
|
6896
|
+
return 0
|
|
6897
|
+
end
|
|
6898
|
+
local function stringContains(haystack, needle)
|
|
6899
|
+
return (string.find(string.lower(haystack), string.lower(needle), 1, true)) ~= nil
|
|
6900
|
+
end
|
|
6901
|
+
local function localPlayer()
|
|
6902
|
+
local player = Players.LocalPlayer
|
|
6903
|
+
local started = tick()
|
|
6904
|
+
while not player and tick() - started < 5 do
|
|
6905
|
+
task.wait(0.05)
|
|
6906
|
+
player = Players.LocalPlayer
|
|
6907
|
+
end
|
|
6908
|
+
return player
|
|
6909
|
+
end
|
|
6910
|
+
local function functionDisplayName(func)
|
|
6911
|
+
local _name = func.Name
|
|
6912
|
+
local _condition = type(_name) == "string"
|
|
6913
|
+
if _condition then
|
|
6914
|
+
_condition = func.Name ~= ""
|
|
6915
|
+
end
|
|
6916
|
+
if _condition then
|
|
6917
|
+
return func.Name
|
|
6918
|
+
end
|
|
6919
|
+
local _source = func.Source
|
|
6920
|
+
local _condition_1 = type(_source) == "string"
|
|
6921
|
+
if _condition_1 then
|
|
6922
|
+
_condition_1 = func.Source ~= ""
|
|
6923
|
+
end
|
|
6924
|
+
if _condition_1 then
|
|
6925
|
+
local _line = func.Line
|
|
6926
|
+
local _condition_2 = type(_line) == "number"
|
|
6927
|
+
if _condition_2 then
|
|
6928
|
+
_condition_2 = func.Line > 0
|
|
6929
|
+
end
|
|
6930
|
+
if _condition_2 then
|
|
6931
|
+
return `{func.Source}:{func.Line}`
|
|
6932
|
+
end
|
|
6933
|
+
return func.Source
|
|
6934
|
+
end
|
|
6935
|
+
return "<anonymous>"
|
|
6936
|
+
end
|
|
6937
|
+
local function flagsOf(func)
|
|
6938
|
+
local _flags = func.Flags
|
|
6939
|
+
return if type(_flags) == "number" then func.Flags else 0
|
|
6940
|
+
end
|
|
6941
|
+
local function isNativeFunction(func)
|
|
6942
|
+
return bit32.band(flagsOf(func), 1) ~= 0
|
|
6943
|
+
end
|
|
6944
|
+
local function isPluginFunction(func)
|
|
6945
|
+
if bit32.band(flagsOf(func), 2) ~= 0 then
|
|
6946
|
+
return true
|
|
6947
|
+
end
|
|
6948
|
+
local _source = func.Source
|
|
6949
|
+
local _condition = type(_source) == "string"
|
|
6950
|
+
if _condition then
|
|
6951
|
+
_condition = (string.find(func.Source, "MCPPlugin", 1, true)) ~= nil
|
|
6952
|
+
end
|
|
6953
|
+
return _condition
|
|
6954
|
+
end
|
|
6955
|
+
local function isDebugLabel(func, filter)
|
|
6956
|
+
if filter == nil then
|
|
6957
|
+
return false
|
|
6958
|
+
end
|
|
6959
|
+
local _name = func.Name
|
|
6960
|
+
local _condition = not (type(_name) == "string")
|
|
6961
|
+
if not _condition then
|
|
6962
|
+
_condition = func.Name == ""
|
|
6963
|
+
end
|
|
6964
|
+
if _condition then
|
|
6965
|
+
return false
|
|
6966
|
+
end
|
|
6967
|
+
local _source = func.Source
|
|
6968
|
+
local _condition_1 = not (type(_source) == "string")
|
|
6969
|
+
if not _condition_1 then
|
|
6970
|
+
_condition_1 = func.Source == "" or func.Source == "[C]" or func.Source == "GC"
|
|
6971
|
+
end
|
|
6972
|
+
if _condition_1 then
|
|
6973
|
+
return false
|
|
6974
|
+
end
|
|
6975
|
+
if func.Line ~= nil and func.Line ~= 0 then
|
|
6976
|
+
return false
|
|
6977
|
+
end
|
|
6978
|
+
if isNativeFunction(func) or isPluginFunction(func) then
|
|
6979
|
+
return false
|
|
6980
|
+
end
|
|
6981
|
+
return stringContains(func.Name, filter) or stringContains(func.Source, filter)
|
|
6982
|
+
end
|
|
6983
|
+
local function pctOfCapture(row, durationMs)
|
|
6984
|
+
local captureUs = durationMs * 1000
|
|
6985
|
+
if captureUs <= 0 then
|
|
6986
|
+
return nil
|
|
6987
|
+
end
|
|
6988
|
+
return math.floor((row.total_us / captureUs) * 10000 + 0.5) / 100
|
|
6989
|
+
end
|
|
6990
|
+
local function compactFunction(row, rank, durationMs)
|
|
6991
|
+
local out = {
|
|
6992
|
+
rank = rank,
|
|
6993
|
+
function_index = row.function_index,
|
|
6994
|
+
name = row.name,
|
|
6995
|
+
total_us = math.floor(row.total_us + 0.5),
|
|
6996
|
+
}
|
|
6997
|
+
local pct = pctOfCapture(row, durationMs)
|
|
6998
|
+
if pct ~= nil then
|
|
6999
|
+
out.pct_of_capture = pct
|
|
7000
|
+
end
|
|
7001
|
+
if row.source ~= nil then
|
|
7002
|
+
out.source = row.source
|
|
7003
|
+
end
|
|
7004
|
+
if row.line ~= nil then
|
|
7005
|
+
out.line = row.line
|
|
7006
|
+
end
|
|
7007
|
+
if row.is_native == true then
|
|
7008
|
+
out.is_native = true
|
|
7009
|
+
end
|
|
7010
|
+
if row.is_plugin == true then
|
|
7011
|
+
out.is_plugin = true
|
|
7012
|
+
end
|
|
7013
|
+
if row.is_debug_label == true then
|
|
7014
|
+
out.is_debug_label = true
|
|
7015
|
+
end
|
|
7016
|
+
return out
|
|
7017
|
+
end
|
|
7018
|
+
local function summarizeProfile(rawJson, profile, requestData, durationMs, frequency, eventPlayerName)
|
|
7019
|
+
local _functions = profile.Functions
|
|
7020
|
+
local funcs = if type(_functions) == "table" then profile.Functions else {}
|
|
7021
|
+
local _nodes = profile.Nodes
|
|
7022
|
+
local nodes = if type(_nodes) == "table" then profile.Nodes else {}
|
|
7023
|
+
local _categories = profile.Categories
|
|
7024
|
+
local categories = if type(_categories) == "table" then profile.Categories else {}
|
|
7025
|
+
local maxFunctions = normalizeMaxFunctions(requestData.max_functions)
|
|
7026
|
+
local minTotalUs = normalizeMinTotalUs(requestData)
|
|
7027
|
+
local includeNative = requestData.include_native == true
|
|
7028
|
+
local includePlugin = requestData.include_plugin == true
|
|
7029
|
+
local _filter = requestData.filter
|
|
7030
|
+
local _condition = type(_filter) == "string"
|
|
7031
|
+
if _condition then
|
|
7032
|
+
_condition = requestData.filter ~= ""
|
|
7033
|
+
end
|
|
7034
|
+
local filter = if _condition then requestData.filter else nil
|
|
7035
|
+
local rows = {}
|
|
7036
|
+
local debugRows = {}
|
|
7037
|
+
local omittedNative = 0
|
|
7038
|
+
local omittedPlugin = 0
|
|
7039
|
+
local omittedBelowThreshold = 0
|
|
7040
|
+
local omittedByFilter = 0
|
|
7041
|
+
for i = 0, #funcs - 1 do
|
|
7042
|
+
local func = funcs[i + 1]
|
|
7043
|
+
if not (type(func) == "table") then
|
|
7044
|
+
continue
|
|
7045
|
+
end
|
|
7046
|
+
local info = func
|
|
7047
|
+
local _totalDuration = info.TotalDuration
|
|
7048
|
+
local totalUs = if type(_totalDuration) == "number" then info.TotalDuration else 0
|
|
7049
|
+
local name = functionDisplayName(info)
|
|
7050
|
+
local _object = {
|
|
7051
|
+
function_index = i + 1,
|
|
7052
|
+
name = name,
|
|
7053
|
+
}
|
|
7054
|
+
local _left = "source"
|
|
7055
|
+
local _source = info.Source
|
|
7056
|
+
_object[_left] = if type(_source) == "string" then info.Source else nil
|
|
7057
|
+
local _left_1 = "line"
|
|
7058
|
+
local _line = info.Line
|
|
7059
|
+
_object[_left_1] = if type(_line) == "number" then info.Line else nil
|
|
7060
|
+
_object.total_us = totalUs
|
|
7061
|
+
_object.is_native = if isNativeFunction(info) then true else nil
|
|
7062
|
+
_object.is_plugin = if isPluginFunction(info) then true else nil
|
|
7063
|
+
_object.is_debug_label = if isDebugLabel(info, filter) then true else nil
|
|
7064
|
+
local row = _object
|
|
7065
|
+
if not includeNative and row.is_native == true then
|
|
7066
|
+
omittedNative += 1
|
|
7067
|
+
continue
|
|
7068
|
+
end
|
|
7069
|
+
if not includePlugin and row.is_plugin == true then
|
|
7070
|
+
omittedPlugin += 1
|
|
7071
|
+
continue
|
|
7072
|
+
end
|
|
7073
|
+
if totalUs < minTotalUs then
|
|
7074
|
+
omittedBelowThreshold += 1
|
|
7075
|
+
continue
|
|
7076
|
+
end
|
|
7077
|
+
if filter ~= nil then
|
|
7078
|
+
local _exp = row.name
|
|
7079
|
+
local _condition_1 = row.source
|
|
7080
|
+
if _condition_1 == nil then
|
|
7081
|
+
_condition_1 = ""
|
|
7082
|
+
end
|
|
7083
|
+
local text = `{_exp} {_condition_1}`
|
|
7084
|
+
if not stringContains(text, filter) then
|
|
7085
|
+
omittedByFilter += 1
|
|
7086
|
+
continue
|
|
7087
|
+
end
|
|
7088
|
+
end
|
|
7089
|
+
table.insert(rows, row)
|
|
7090
|
+
if row.is_debug_label == true then
|
|
7091
|
+
table.insert(debugRows, row)
|
|
7092
|
+
end
|
|
7093
|
+
end
|
|
7094
|
+
table.sort(rows, function(a, b)
|
|
7095
|
+
return a.total_us > b.total_us
|
|
7096
|
+
end)
|
|
7097
|
+
table.sort(debugRows, function(a, b)
|
|
7098
|
+
return a.total_us > b.total_us
|
|
7099
|
+
end)
|
|
7100
|
+
local topFunctions = {}
|
|
7101
|
+
do
|
|
7102
|
+
local i = 0
|
|
7103
|
+
local _shouldIncrement = false
|
|
7104
|
+
while true do
|
|
7105
|
+
if _shouldIncrement then
|
|
7106
|
+
i += 1
|
|
7107
|
+
else
|
|
7108
|
+
_shouldIncrement = true
|
|
7109
|
+
end
|
|
7110
|
+
if not (i < math.min(maxFunctions, #rows)) then
|
|
7111
|
+
break
|
|
7112
|
+
end
|
|
7113
|
+
local _arg0 = compactFunction(rows[i + 1], i + 1, durationMs)
|
|
7114
|
+
table.insert(topFunctions, _arg0)
|
|
7115
|
+
end
|
|
7116
|
+
end
|
|
7117
|
+
local debugLabels = {}
|
|
7118
|
+
do
|
|
7119
|
+
local i = 0
|
|
7120
|
+
local _shouldIncrement = false
|
|
7121
|
+
while true do
|
|
7122
|
+
if _shouldIncrement then
|
|
7123
|
+
i += 1
|
|
7124
|
+
else
|
|
7125
|
+
_shouldIncrement = true
|
|
7126
|
+
end
|
|
7127
|
+
if not (i < math.min(maxFunctions, #debugRows)) then
|
|
7128
|
+
break
|
|
7129
|
+
end
|
|
7130
|
+
local _arg0 = compactFunction(debugRows[i + 1], i + 1, durationMs)
|
|
7131
|
+
table.insert(debugLabels, _arg0)
|
|
7132
|
+
end
|
|
7133
|
+
end
|
|
7134
|
+
local categoryNames = {}
|
|
7135
|
+
for i = 0, #categories - 1 do
|
|
7136
|
+
local category = categories[i + 1]
|
|
7137
|
+
if type(category) == "table" then
|
|
7138
|
+
local name = category.Name
|
|
7139
|
+
if type(name) == "string" then
|
|
7140
|
+
table.insert(categoryNames, name)
|
|
7141
|
+
end
|
|
7142
|
+
end
|
|
7143
|
+
end
|
|
7144
|
+
local omitted = {}
|
|
7145
|
+
local hasOmitted = false
|
|
7146
|
+
if omittedNative > 0 then
|
|
7147
|
+
omitted.native = omittedNative
|
|
7148
|
+
hasOmitted = true
|
|
7149
|
+
end
|
|
7150
|
+
if omittedPlugin > 0 then
|
|
7151
|
+
omitted.plugin = omittedPlugin
|
|
7152
|
+
hasOmitted = true
|
|
7153
|
+
end
|
|
7154
|
+
if omittedBelowThreshold > 0 then
|
|
7155
|
+
omitted.below_min_total_us = omittedBelowThreshold
|
|
7156
|
+
hasOmitted = true
|
|
7157
|
+
end
|
|
7158
|
+
if omittedByFilter > 0 then
|
|
7159
|
+
omitted.filtered_out = omittedByFilter
|
|
7160
|
+
hasOmitted = true
|
|
7161
|
+
end
|
|
7162
|
+
local _object = {
|
|
7163
|
+
ok = true,
|
|
7164
|
+
duration_ms = durationMs,
|
|
7165
|
+
frequency = frequency,
|
|
7166
|
+
}
|
|
7167
|
+
local _left = "applied"
|
|
7168
|
+
local _object_1 = {}
|
|
7169
|
+
local _left_1 = "filter"
|
|
7170
|
+
local _condition_1 = filter
|
|
7171
|
+
if _condition_1 == nil then
|
|
7172
|
+
_condition_1 = nil
|
|
7173
|
+
end
|
|
7174
|
+
_object_1[_left_1] = _condition_1
|
|
7175
|
+
_object_1.min_total_us = minTotalUs
|
|
7176
|
+
_object_1.include_native = includeNative
|
|
7177
|
+
_object_1.include_plugin = includePlugin
|
|
7178
|
+
_object_1.max_functions = maxFunctions
|
|
7179
|
+
_object_1.sort = "total_us_desc"
|
|
7180
|
+
_object[_left] = _object_1
|
|
7181
|
+
_object.json_bytes = #rawJson
|
|
7182
|
+
_object.counts = {
|
|
7183
|
+
categories = #categories,
|
|
7184
|
+
nodes = #nodes,
|
|
7185
|
+
functions = #funcs,
|
|
7186
|
+
}
|
|
7187
|
+
_object.top_functions = topFunctions
|
|
7188
|
+
_object.debug_labels = debugLabels
|
|
7189
|
+
local result = _object
|
|
7190
|
+
if #categoryNames > 0 then
|
|
7191
|
+
result.categories = categoryNames
|
|
7192
|
+
end
|
|
7193
|
+
if hasOmitted then
|
|
7194
|
+
result.omitted = omitted
|
|
7195
|
+
end
|
|
7196
|
+
if profile.Version ~= nil then
|
|
7197
|
+
result.version = profile.Version
|
|
7198
|
+
end
|
|
7199
|
+
if profile.SessionStartTime ~= nil or profile.SessionEndTime ~= nil then
|
|
7200
|
+
result.session = {
|
|
7201
|
+
start_time = profile.SessionStartTime,
|
|
7202
|
+
end_time = profile.SessionEndTime,
|
|
7203
|
+
}
|
|
7204
|
+
end
|
|
7205
|
+
if eventPlayerName ~= nil then
|
|
7206
|
+
result.player = eventPlayerName
|
|
7207
|
+
end
|
|
7208
|
+
if requestData.__mcp_include_raw_json == true then
|
|
7209
|
+
result.raw_json = rawJson
|
|
7210
|
+
end
|
|
7211
|
+
return result
|
|
7212
|
+
end
|
|
7213
|
+
local function captureScriptProfiler(requestData)
|
|
7214
|
+
if not RunService:IsRunning() then
|
|
7215
|
+
return {
|
|
7216
|
+
error = "runtime_target_required",
|
|
7217
|
+
message = 'Script profiler capture requires a running playtest target such as target=\"server\" or target=\"client-1\".',
|
|
7218
|
+
}
|
|
7219
|
+
end
|
|
7220
|
+
local serviceOrError = getProfilerService()
|
|
7221
|
+
local _value = serviceOrError.IsA
|
|
7222
|
+
if not (_value ~= 0 and _value == _value and _value ~= "" and _value) then
|
|
7223
|
+
return serviceOrError
|
|
7224
|
+
end
|
|
7225
|
+
local service = serviceOrError
|
|
7226
|
+
local durationMs = normalizeDurationMs(requestData.duration_ms)
|
|
7227
|
+
local frequency = normalizeFrequency(requestData.frequency)
|
|
7228
|
+
local isServer = RunService:IsServer()
|
|
7229
|
+
local isClient = RunService:IsClient() and not isServer
|
|
7230
|
+
local player = if isClient then localPlayer() else nil
|
|
7231
|
+
if not isServer and not player then
|
|
7232
|
+
return {
|
|
7233
|
+
error = "client_player_unavailable",
|
|
7234
|
+
message = "Could not resolve Players.LocalPlayer for client profiling.",
|
|
7235
|
+
}
|
|
7236
|
+
end
|
|
7237
|
+
local rawJson
|
|
7238
|
+
local eventPlayerName
|
|
7239
|
+
local connection = service.OnNewData:Connect(function(playerArg, jsonString)
|
|
7240
|
+
if rawJson ~= nil then
|
|
7241
|
+
return nil
|
|
7242
|
+
end
|
|
7243
|
+
rawJson = jsonString
|
|
7244
|
+
if playerArg then
|
|
7245
|
+
eventPlayerName = playerArg.Name
|
|
7246
|
+
end
|
|
7247
|
+
end)
|
|
7248
|
+
local startOk, startErr = pcall(function()
|
|
7249
|
+
if isServer then
|
|
7250
|
+
service:ServerStart(frequency)
|
|
7251
|
+
else
|
|
7252
|
+
service:ClientStart(player, frequency)
|
|
7253
|
+
end
|
|
7254
|
+
end)
|
|
7255
|
+
if not startOk then
|
|
7256
|
+
connection:Disconnect()
|
|
7257
|
+
return {
|
|
7258
|
+
error = "script_profiler_start_failed",
|
|
7259
|
+
message = tostring(startErr),
|
|
7260
|
+
}
|
|
7261
|
+
end
|
|
7262
|
+
task.wait(durationMs / 1000)
|
|
7263
|
+
local stopOk, stopErr = pcall(function()
|
|
7264
|
+
if isServer then
|
|
7265
|
+
service:ServerStop()
|
|
7266
|
+
service:ServerRequestData()
|
|
7267
|
+
else
|
|
7268
|
+
service:ClientStop(player)
|
|
7269
|
+
service:ClientRequestData(player)
|
|
7270
|
+
end
|
|
7271
|
+
end)
|
|
7272
|
+
if not stopOk then
|
|
7273
|
+
connection:Disconnect()
|
|
7274
|
+
return {
|
|
7275
|
+
error = "script_profiler_stop_failed",
|
|
7276
|
+
message = tostring(stopErr),
|
|
7277
|
+
}
|
|
7278
|
+
end
|
|
7279
|
+
local requestedAt = tick()
|
|
7280
|
+
while rawJson == nil and tick() - requestedAt < 5 do
|
|
7281
|
+
task.wait(0.05)
|
|
7282
|
+
end
|
|
7283
|
+
connection:Disconnect()
|
|
7284
|
+
if rawJson == nil then
|
|
7285
|
+
return {
|
|
7286
|
+
error = "script_profiler_data_timeout",
|
|
7287
|
+
message = "ScriptProfilerService did not emit OnNewData after requesting profiler data.",
|
|
7288
|
+
}
|
|
7289
|
+
end
|
|
7290
|
+
local decodeOk, decoded = pcall(function()
|
|
7291
|
+
return HttpService:JSONDecode(rawJson)
|
|
7292
|
+
end)
|
|
7293
|
+
if not decodeOk then
|
|
7294
|
+
return {
|
|
7295
|
+
error = "script_profiler_decode_failed",
|
|
7296
|
+
message = tostring(decoded),
|
|
7297
|
+
json_bytes = #rawJson,
|
|
7298
|
+
}
|
|
7299
|
+
end
|
|
7300
|
+
return summarizeProfile(rawJson, decoded, requestData, durationMs, frequency, eventPlayerName)
|
|
7301
|
+
end
|
|
7302
|
+
return {
|
|
7303
|
+
captureScriptProfiler = captureScriptProfiler,
|
|
7304
|
+
}
|
|
7305
|
+
]]></string>
|
|
7306
|
+
</Properties>
|
|
7307
|
+
</Item>
|
|
7308
|
+
<Item class="ModuleScript" referent="21">
|
|
6373
7309
|
<Properties>
|
|
6374
7310
|
<string name="Name">SerializationHandlers</string>
|
|
6375
7311
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -6555,7 +7491,7 @@ return {
|
|
|
6555
7491
|
]]></string>
|
|
6556
7492
|
</Properties>
|
|
6557
7493
|
</Item>
|
|
6558
|
-
<Item class="ModuleScript" referent="
|
|
7494
|
+
<Item class="ModuleScript" referent="22">
|
|
6559
7495
|
<Properties>
|
|
6560
7496
|
<string name="Name">TestHandlers</string>
|
|
6561
7497
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -6576,10 +7512,7 @@ local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
|
6576
7512
|
local NAV_SIGNAL = "__MCP_NAV__"
|
|
6577
7513
|
local NAV_RESULT = "__MCP_NAV_RESULT__"
|
|
6578
7514
|
local testRunning = false
|
|
6579
|
-
local
|
|
6580
|
-
local logConnection
|
|
6581
|
-
local testResult
|
|
6582
|
-
local testError
|
|
7515
|
+
local navLogConnection
|
|
6583
7516
|
local stopListenerScript
|
|
6584
7517
|
local navResultCallback
|
|
6585
7518
|
local multiplayerState = {
|
|
@@ -6727,6 +7660,12 @@ local function cleanupStopListener()
|
|
|
6727
7660
|
stopListenerScript = nil
|
|
6728
7661
|
end
|
|
6729
7662
|
end
|
|
7663
|
+
local function disconnectNavLogListener()
|
|
7664
|
+
if navLogConnection then
|
|
7665
|
+
navLogConnection:Disconnect()
|
|
7666
|
+
navLogConnection = nil
|
|
7667
|
+
end
|
|
7668
|
+
end
|
|
6730
7669
|
local function startPlaytest(requestData)
|
|
6731
7670
|
local mode = requestData.mode
|
|
6732
7671
|
local numPlayers = requestData.numPlayers
|
|
@@ -6746,10 +7685,7 @@ local function startPlaytest(requestData)
|
|
|
6746
7685
|
-- Reset it so subsequent starts don't hit a false "already running".
|
|
6747
7686
|
if testRunning and not RunService:IsRunning() then
|
|
6748
7687
|
testRunning = false
|
|
6749
|
-
|
|
6750
|
-
logConnection:Disconnect()
|
|
6751
|
-
logConnection = nil
|
|
6752
|
-
end
|
|
7688
|
+
disconnectNavLogListener()
|
|
6753
7689
|
cleanupStopListener()
|
|
6754
7690
|
-- Runtime eval bridges are created by the play server/client plugin
|
|
6755
7691
|
-- peers and disappear with the play DataModels.
|
|
@@ -6760,34 +7696,19 @@ local function startPlaytest(requestData)
|
|
|
6760
7696
|
}
|
|
6761
7697
|
end
|
|
6762
7698
|
testRunning = true
|
|
6763
|
-
outputBuffer = {}
|
|
6764
|
-
testResult = nil
|
|
6765
|
-
testError = nil
|
|
6766
7699
|
cleanupStopListener()
|
|
6767
|
-
|
|
7700
|
+
disconnectNavLogListener()
|
|
7701
|
+
navLogConnection = LogService.MessageOut:Connect(function(message)
|
|
6768
7702
|
local _message = message
|
|
6769
|
-
local _arg1 = #
|
|
6770
|
-
if string.sub(_message, 1, _arg1) ==
|
|
6771
|
-
return nil
|
|
6772
|
-
end
|
|
6773
|
-
local _message_1 = message
|
|
6774
|
-
local _arg1_1 = #NAV_RESULT + 1
|
|
6775
|
-
if string.sub(_message_1, 1, _arg1_1) == `{NAV_RESULT}:` then
|
|
7703
|
+
local _arg1 = #NAV_RESULT + 1
|
|
7704
|
+
if string.sub(_message, 1, _arg1) == `{NAV_RESULT}:` then
|
|
6776
7705
|
if navResultCallback then
|
|
6777
7706
|
local _fn = navResultCallback
|
|
6778
|
-
local
|
|
7707
|
+
local _message_1 = message
|
|
6779
7708
|
local _arg0 = #NAV_RESULT + 2
|
|
6780
|
-
_fn(string.sub(
|
|
7709
|
+
_fn(string.sub(_message_1, _arg0))
|
|
6781
7710
|
end
|
|
6782
|
-
return nil
|
|
6783
7711
|
end
|
|
6784
|
-
local _outputBuffer = outputBuffer
|
|
6785
|
-
local _arg0 = {
|
|
6786
|
-
message = message,
|
|
6787
|
-
messageType = messageType.Name,
|
|
6788
|
-
timestamp = tick(),
|
|
6789
|
-
}
|
|
6790
|
-
table.insert(_outputBuffer, _arg0)
|
|
6791
7712
|
end)
|
|
6792
7713
|
local injected, injErr = pcall(function()
|
|
6793
7714
|
return injectStopListener()
|
|
@@ -6802,15 +7723,10 @@ local function startPlaytest(requestData)
|
|
|
6802
7723
|
end
|
|
6803
7724
|
return StudioTestService:ExecuteRunModeAsync({})
|
|
6804
7725
|
end)
|
|
6805
|
-
if ok then
|
|
6806
|
-
|
|
6807
|
-
else
|
|
6808
|
-
testError = tostring(result)
|
|
6809
|
-
end
|
|
6810
|
-
if logConnection then
|
|
6811
|
-
logConnection:Disconnect()
|
|
6812
|
-
logConnection = nil
|
|
7726
|
+
if not ok then
|
|
7727
|
+
warn(`[robloxstudio-mcp] Playtest ended with error: {result}`)
|
|
6813
7728
|
end
|
|
7729
|
+
disconnectNavLogListener()
|
|
6814
7730
|
testRunning = false
|
|
6815
7731
|
cleanupStopListener()
|
|
6816
7732
|
end)
|
|
@@ -6887,20 +7803,6 @@ local function stopPlaytest(_requestData)
|
|
|
6887
7803
|
message = "Playtest stopped.",
|
|
6888
7804
|
}
|
|
6889
7805
|
end
|
|
6890
|
-
local function getPlaytestOutput(_requestData)
|
|
6891
|
-
local _object = {
|
|
6892
|
-
isRunning = testRunning,
|
|
6893
|
-
}
|
|
6894
|
-
local _left = "output"
|
|
6895
|
-
local _array = {}
|
|
6896
|
-
local _length = #_array
|
|
6897
|
-
table.move(outputBuffer, 1, #outputBuffer, _length + 1, _array)
|
|
6898
|
-
_object[_left] = _array
|
|
6899
|
-
_object.outputCount = #outputBuffer
|
|
6900
|
-
_object.testResult = if testResult ~= nil then tostring(testResult) else nil
|
|
6901
|
-
_object.testError = testError
|
|
6902
|
-
return _object
|
|
6903
|
-
end
|
|
6904
7806
|
local function multiplayerTestStart(requestData)
|
|
6905
7807
|
if RunService:IsRunning() then
|
|
6906
7808
|
return {
|
|
@@ -7155,7 +8057,6 @@ end
|
|
|
7155
8057
|
return {
|
|
7156
8058
|
startPlaytest = startPlaytest,
|
|
7157
8059
|
stopPlaytest = stopPlaytest,
|
|
7158
|
-
getPlaytestOutput = getPlaytestOutput,
|
|
7159
8060
|
multiplayerTestStart = multiplayerTestStart,
|
|
7160
8061
|
multiplayerTestState = multiplayerTestState,
|
|
7161
8062
|
multiplayerTestAddPlayers = multiplayerTestAddPlayers,
|
|
@@ -7167,7 +8068,7 @@ return {
|
|
|
7167
8068
|
</Properties>
|
|
7168
8069
|
</Item>
|
|
7169
8070
|
</Item>
|
|
7170
|
-
<Item class="ModuleScript" referent="
|
|
8071
|
+
<Item class="ModuleScript" referent="23">
|
|
7171
8072
|
<Properties>
|
|
7172
8073
|
<string name="Name">HttpDiagnostics</string>
|
|
7173
8074
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7248,7 +8149,7 @@ return {
|
|
|
7248
8149
|
]]></string>
|
|
7249
8150
|
</Properties>
|
|
7250
8151
|
</Item>
|
|
7251
|
-
<Item class="ModuleScript" referent="
|
|
8152
|
+
<Item class="ModuleScript" referent="24">
|
|
7252
8153
|
<Properties>
|
|
7253
8154
|
<string name="Name">LuauExec</string>
|
|
7254
8155
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7682,7 +8583,7 @@ return {
|
|
|
7682
8583
|
]]></string>
|
|
7683
8584
|
</Properties>
|
|
7684
8585
|
</Item>
|
|
7685
|
-
<Item class="ModuleScript" referent="
|
|
8586
|
+
<Item class="ModuleScript" referent="25">
|
|
7686
8587
|
<Properties>
|
|
7687
8588
|
<string name="Name">Recording</string>
|
|
7688
8589
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7712,7 +8613,7 @@ return {
|
|
|
7712
8613
|
]]></string>
|
|
7713
8614
|
</Properties>
|
|
7714
8615
|
</Item>
|
|
7715
|
-
<Item class="ModuleScript" referent="
|
|
8616
|
+
<Item class="ModuleScript" referent="26">
|
|
7716
8617
|
<Properties>
|
|
7717
8618
|
<string name="Name">RenderMonitor</string>
|
|
7718
8619
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7780,7 +8681,7 @@ return {
|
|
|
7780
8681
|
]]></string>
|
|
7781
8682
|
</Properties>
|
|
7782
8683
|
</Item>
|
|
7783
|
-
<Item class="ModuleScript" referent="
|
|
8684
|
+
<Item class="ModuleScript" referent="27">
|
|
7784
8685
|
<Properties>
|
|
7785
8686
|
<string name="Name">RuntimeLogBuffer</string>
|
|
7786
8687
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7833,6 +8734,43 @@ local function dropOldestUntilFits(incomingBytes)
|
|
|
7833
8734
|
totalDropped += 1
|
|
7834
8735
|
end
|
|
7835
8736
|
end
|
|
8737
|
+
local function pushEntry(msg, t, ts)
|
|
8738
|
+
if ts == nil then
|
|
8739
|
+
ts = nowSec()
|
|
8740
|
+
end
|
|
8741
|
+
local bytes = #msg
|
|
8742
|
+
dropOldestUntilFits(bytes)
|
|
8743
|
+
local _arg0 = {
|
|
8744
|
+
seq = nextSeq,
|
|
8745
|
+
ts = ts,
|
|
8746
|
+
level = levelTag(t),
|
|
8747
|
+
message = msg,
|
|
8748
|
+
}
|
|
8749
|
+
table.insert(entries, _arg0)
|
|
8750
|
+
nextSeq += 1
|
|
8751
|
+
totalBytes += bytes
|
|
8752
|
+
end
|
|
8753
|
+
local function seedRuntimeHistory()
|
|
8754
|
+
if not RunService:IsRunning() then
|
|
8755
|
+
return nil
|
|
8756
|
+
end
|
|
8757
|
+
local ok, history = pcall(function()
|
|
8758
|
+
return LogService:GetLogHistory()
|
|
8759
|
+
end)
|
|
8760
|
+
if not ok then
|
|
8761
|
+
return nil
|
|
8762
|
+
end
|
|
8763
|
+
for _, entry in history do
|
|
8764
|
+
local _message = entry.message
|
|
8765
|
+
if not (type(_message) == "string") then
|
|
8766
|
+
continue
|
|
8767
|
+
end
|
|
8768
|
+
local _exp = entry.message
|
|
8769
|
+
local _exp_1 = entry.messageType
|
|
8770
|
+
local _timestamp = entry.timestamp
|
|
8771
|
+
pushEntry(_exp, _exp_1, if type(_timestamp) == "number" then entry.timestamp else nil)
|
|
8772
|
+
end
|
|
8773
|
+
end
|
|
7836
8774
|
local function install()
|
|
7837
8775
|
if installed then
|
|
7838
8776
|
return nil
|
|
@@ -7841,18 +8779,12 @@ local function install()
|
|
|
7841
8779
|
return nil
|
|
7842
8780
|
end
|
|
7843
8781
|
installed = true
|
|
8782
|
+
-- Play peers can emit startup logs before the plugin finishes loading.
|
|
8783
|
+
-- Seed from per-DataModel LogHistory so get_runtime_logs can still see
|
|
8784
|
+
-- those early messages; skip edit mode to avoid stale prior-session logs.
|
|
8785
|
+
seedRuntimeHistory()
|
|
7844
8786
|
LogService.MessageOut:Connect(function(msg, t)
|
|
7845
|
-
|
|
7846
|
-
dropOldestUntilFits(bytes)
|
|
7847
|
-
local _arg0 = {
|
|
7848
|
-
seq = nextSeq,
|
|
7849
|
-
ts = nowSec(),
|
|
7850
|
-
level = levelTag(t),
|
|
7851
|
-
message = msg,
|
|
7852
|
-
}
|
|
7853
|
-
table.insert(entries, _arg0)
|
|
7854
|
-
nextSeq += 1
|
|
7855
|
-
totalBytes += bytes
|
|
8787
|
+
pushEntry(msg, t)
|
|
7856
8788
|
end)
|
|
7857
8789
|
end
|
|
7858
8790
|
local function detectPeer()
|
|
@@ -7961,7 +8893,7 @@ return {
|
|
|
7961
8893
|
]]></string>
|
|
7962
8894
|
</Properties>
|
|
7963
8895
|
</Item>
|
|
7964
|
-
<Item class="ModuleScript" referent="
|
|
8896
|
+
<Item class="ModuleScript" referent="28">
|
|
7965
8897
|
<Properties>
|
|
7966
8898
|
<string name="Name">ServerUrlSettings</string>
|
|
7967
8899
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -8037,11 +8969,11 @@ return {
|
|
|
8037
8969
|
]]></string>
|
|
8038
8970
|
</Properties>
|
|
8039
8971
|
</Item>
|
|
8040
|
-
<Item class="ModuleScript" referent="
|
|
8972
|
+
<Item class="ModuleScript" referent="29">
|
|
8041
8973
|
<Properties>
|
|
8042
8974
|
<string name="Name">State</string>
|
|
8043
8975
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
8044
|
-
local CURRENT_VERSION = "2.
|
|
8976
|
+
local CURRENT_VERSION = "2.17.0"
|
|
8045
8977
|
local PLUGIN_VARIANT = "inspector"
|
|
8046
8978
|
local MAX_CONNECTIONS = 5
|
|
8047
8979
|
local BASE_PORT = 58741
|
|
@@ -8135,7 +9067,7 @@ return {
|
|
|
8135
9067
|
]]></string>
|
|
8136
9068
|
</Properties>
|
|
8137
9069
|
</Item>
|
|
8138
|
-
<Item class="ModuleScript" referent="
|
|
9070
|
+
<Item class="ModuleScript" referent="30">
|
|
8139
9071
|
<Properties>
|
|
8140
9072
|
<string name="Name">StopPlayMonitor</string>
|
|
8141
9073
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -8433,7 +9365,7 @@ return {
|
|
|
8433
9365
|
]]></string>
|
|
8434
9366
|
</Properties>
|
|
8435
9367
|
</Item>
|
|
8436
|
-
<Item class="ModuleScript" referent="
|
|
9368
|
+
<Item class="ModuleScript" referent="31">
|
|
8437
9369
|
<Properties>
|
|
8438
9370
|
<string name="Name">UI</string>
|
|
8439
9371
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -9204,7 +10136,7 @@ return {
|
|
|
9204
10136
|
]]></string>
|
|
9205
10137
|
</Properties>
|
|
9206
10138
|
</Item>
|
|
9207
|
-
<Item class="ModuleScript" referent="
|
|
10139
|
+
<Item class="ModuleScript" referent="32">
|
|
9208
10140
|
<Properties>
|
|
9209
10141
|
<string name="Name">Utils</string>
|
|
9210
10142
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -9734,11 +10666,11 @@ return {
|
|
|
9734
10666
|
</Properties>
|
|
9735
10667
|
</Item>
|
|
9736
10668
|
</Item>
|
|
9737
|
-
<Item class="Folder" referent="
|
|
10669
|
+
<Item class="Folder" referent="36">
|
|
9738
10670
|
<Properties>
|
|
9739
10671
|
<string name="Name">include</string>
|
|
9740
10672
|
</Properties>
|
|
9741
|
-
<Item class="ModuleScript" referent="
|
|
10673
|
+
<Item class="ModuleScript" referent="33">
|
|
9742
10674
|
<Properties>
|
|
9743
10675
|
<string name="Name">Promise</string>
|
|
9744
10676
|
<string name="Source"><![CDATA[--[[
|
|
@@ -11812,7 +12744,7 @@ return Promise
|
|
|
11812
12744
|
]]></string>
|
|
11813
12745
|
</Properties>
|
|
11814
12746
|
</Item>
|
|
11815
|
-
<Item class="ModuleScript" referent="
|
|
12747
|
+
<Item class="ModuleScript" referent="34">
|
|
11816
12748
|
<Properties>
|
|
11817
12749
|
<string name="Name">RuntimeLib</string>
|
|
11818
12750
|
<string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
|
|
@@ -12079,15 +13011,15 @@ return TS
|
|
|
12079
13011
|
</Properties>
|
|
12080
13012
|
</Item>
|
|
12081
13013
|
</Item>
|
|
12082
|
-
<Item class="Folder" referent="
|
|
13014
|
+
<Item class="Folder" referent="37">
|
|
12083
13015
|
<Properties>
|
|
12084
13016
|
<string name="Name">node_modules</string>
|
|
12085
13017
|
</Properties>
|
|
12086
|
-
<Item class="Folder" referent="
|
|
13018
|
+
<Item class="Folder" referent="38">
|
|
12087
13019
|
<Properties>
|
|
12088
13020
|
<string name="Name">@rbxts</string>
|
|
12089
13021
|
</Properties>
|
|
12090
|
-
<Item class="ModuleScript" referent="
|
|
13022
|
+
<Item class="ModuleScript" referent="35">
|
|
12091
13023
|
<Properties>
|
|
12092
13024
|
<string name="Name">services</string>
|
|
12093
13025
|
<string name="Source"><![CDATA[return setmetatable({}, {
|