@chrrxs/robloxstudio-mcp-inspector 2.19.0 → 2.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +958 -294
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +226 -120
- package/studio-plugin/MCPPlugin.rbxmx +226 -120
- package/studio-plugin/src/modules/ServerUrlSettings.ts +6 -3
- package/studio-plugin/src/modules/Utils.ts +17 -0
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +68 -3
- package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +47 -50
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +6 -13
- package/studio-plugin/src/server/index.server.ts +2 -0
|
@@ -33,6 +33,9 @@ StopPlayMonitor.init(plugin)
|
|
|
33
33
|
BreakpointHandlers.init(plugin)
|
|
34
34
|
ServerUrlSettings.init(plugin)
|
|
35
35
|
local function applyRememberedServerUrl()
|
|
36
|
+
if ClientBroker.forkRole() == "client" then
|
|
37
|
+
return nil
|
|
38
|
+
end
|
|
36
39
|
local rememberedServerUrl = ServerUrlSettings.readServerUrl()
|
|
37
40
|
if rememberedServerUrl == nil then
|
|
38
41
|
return nil
|
|
@@ -1410,9 +1413,9 @@ local function computeBridgeStamp()
|
|
|
1410
1413
|
for i = 1, #combined do
|
|
1411
1414
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1412
1415
|
end
|
|
1413
|
-
-- "2.19.
|
|
1416
|
+
-- "2.19.1" is replaced with the package version at package time
|
|
1414
1417
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1415
|
-
return `{tostring(h)}-2.19.
|
|
1418
|
+
return `{tostring(h)}-2.19.1`
|
|
1416
1419
|
end
|
|
1417
1420
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1418
1421
|
local function setSource(scriptInst, source)
|
|
@@ -7419,6 +7422,65 @@ local function getProjectStructure(requestData)
|
|
|
7419
7422
|
result.timestamp = tick()
|
|
7420
7423
|
return result
|
|
7421
7424
|
end
|
|
7425
|
+
-- Split a Lua pattern on TOP-LEVEL "|" into alternatives. Lua patterns have no
|
|
7426
|
+
-- alternation operator, so "foo|bar" would otherwise be matched as the literal
|
|
7427
|
+
-- text "foo|bar" and silently never hit. "%|" stays a literal pipe, and "%bxy"
|
|
7428
|
+
-- keeps both balanced-match delimiter characters.
|
|
7429
|
+
local function splitLuaAlternation(pattern)
|
|
7430
|
+
local parts = {}
|
|
7431
|
+
local current = ""
|
|
7432
|
+
local i = 1
|
|
7433
|
+
local n = #pattern
|
|
7434
|
+
local inCharClass = false
|
|
7435
|
+
while i <= n do
|
|
7436
|
+
local c = string.sub(pattern, i, i)
|
|
7437
|
+
if c == "%" then
|
|
7438
|
+
if string.sub(pattern, i + 1, i + 1) == "b" then
|
|
7439
|
+
current ..= string.sub(pattern, i, math.min(i + 3, n))
|
|
7440
|
+
i += 4
|
|
7441
|
+
continue
|
|
7442
|
+
end
|
|
7443
|
+
-- Preserve an escape pair (e.g. %|, %., %d) intact.
|
|
7444
|
+
current ..= string.sub(pattern, i, i + 1)
|
|
7445
|
+
i += 2
|
|
7446
|
+
elseif c == "[" then
|
|
7447
|
+
inCharClass = true
|
|
7448
|
+
current ..= c
|
|
7449
|
+
i += 1
|
|
7450
|
+
elseif c == "]" then
|
|
7451
|
+
inCharClass = false
|
|
7452
|
+
current ..= c
|
|
7453
|
+
i += 1
|
|
7454
|
+
elseif c == "|" and not inCharClass then
|
|
7455
|
+
local _current = current
|
|
7456
|
+
table.insert(parts, _current)
|
|
7457
|
+
current = ""
|
|
7458
|
+
i += 1
|
|
7459
|
+
else
|
|
7460
|
+
current ..= c
|
|
7461
|
+
i += 1
|
|
7462
|
+
end
|
|
7463
|
+
end
|
|
7464
|
+
local _current = current
|
|
7465
|
+
table.insert(parts, _current)
|
|
7466
|
+
return parts
|
|
7467
|
+
end
|
|
7468
|
+
-- Return the earliest match across alternatives (mirrors regex alternation).
|
|
7469
|
+
local function findFirstPattern(line, alternatives)
|
|
7470
|
+
local bestStart
|
|
7471
|
+
local bestEnd
|
|
7472
|
+
for _, alt in alternatives do
|
|
7473
|
+
if alt == "" then
|
|
7474
|
+
continue
|
|
7475
|
+
end
|
|
7476
|
+
local s, e = string.find(line, alt)
|
|
7477
|
+
if s ~= nil and (bestStart == nil or s < bestStart) then
|
|
7478
|
+
bestStart = s
|
|
7479
|
+
bestEnd = e
|
|
7480
|
+
end
|
|
7481
|
+
end
|
|
7482
|
+
return { bestStart, bestEnd }
|
|
7483
|
+
end
|
|
7422
7484
|
local function grepScripts(requestData)
|
|
7423
7485
|
local pattern = requestData.pattern
|
|
7424
7486
|
if not (pattern ~= "" and pattern) then
|
|
@@ -7426,11 +7488,27 @@ local function grepScripts(requestData)
|
|
|
7426
7488
|
error = "pattern is required",
|
|
7427
7489
|
}
|
|
7428
7490
|
end
|
|
7429
|
-
local _condition = (requestData.
|
|
7491
|
+
local _condition = (requestData.usePattern)
|
|
7430
7492
|
if _condition == nil then
|
|
7431
7493
|
_condition = false
|
|
7432
7494
|
end
|
|
7433
|
-
local
|
|
7495
|
+
local usePattern = _condition
|
|
7496
|
+
if usePattern and requestData.caseSensitive == false then
|
|
7497
|
+
return {
|
|
7498
|
+
error = "Case-insensitive Lua pattern search is not supported. Omit caseSensitive or pass caseSensitive: true with usePattern: true, or use literal search.",
|
|
7499
|
+
}
|
|
7500
|
+
end
|
|
7501
|
+
local _result
|
|
7502
|
+
if usePattern then
|
|
7503
|
+
_result = true
|
|
7504
|
+
else
|
|
7505
|
+
local _condition_1 = (requestData.caseSensitive)
|
|
7506
|
+
if _condition_1 == nil then
|
|
7507
|
+
_condition_1 = false
|
|
7508
|
+
end
|
|
7509
|
+
_result = _condition_1
|
|
7510
|
+
end
|
|
7511
|
+
local caseSensitive = _result
|
|
7434
7512
|
local _condition_1 = (requestData.contextLines)
|
|
7435
7513
|
if _condition_1 == nil then
|
|
7436
7514
|
_condition_1 = 0
|
|
@@ -7446,21 +7524,16 @@ local function grepScripts(requestData)
|
|
|
7446
7524
|
_condition_3 = 0
|
|
7447
7525
|
end
|
|
7448
7526
|
local maxResultsPerScript = _condition_3
|
|
7449
|
-
local _condition_4 = (requestData.
|
|
7527
|
+
local _condition_4 = (requestData.filesOnly)
|
|
7450
7528
|
if _condition_4 == nil then
|
|
7451
7529
|
_condition_4 = false
|
|
7452
7530
|
end
|
|
7453
|
-
local
|
|
7454
|
-
local _condition_5 = (requestData.
|
|
7531
|
+
local filesOnly = _condition_4
|
|
7532
|
+
local _condition_5 = (requestData.path)
|
|
7455
7533
|
if _condition_5 == nil then
|
|
7456
|
-
_condition_5 =
|
|
7534
|
+
_condition_5 = ""
|
|
7457
7535
|
end
|
|
7458
|
-
local
|
|
7459
|
-
local _condition_6 = (requestData.path)
|
|
7460
|
-
if _condition_6 == nil then
|
|
7461
|
-
_condition_6 = ""
|
|
7462
|
-
end
|
|
7463
|
-
local searchPath = _condition_6
|
|
7536
|
+
local searchPath = _condition_5
|
|
7464
7537
|
local classFilter = requestData.classFilter
|
|
7465
7538
|
local startInstance = if searchPath ~= "" then getInstanceByPath(searchPath) else game
|
|
7466
7539
|
if not startInstance then
|
|
@@ -7470,6 +7543,8 @@ local function grepScripts(requestData)
|
|
|
7470
7543
|
end
|
|
7471
7544
|
-- Prepare pattern for matching
|
|
7472
7545
|
local searchPattern = if caseSensitive then pattern else string.lower(pattern)
|
|
7546
|
+
-- Pre-split top-level "|" alternation once (pattern mode only).
|
|
7547
|
+
local patternAlternatives = if usePattern then splitLuaAlternation(searchPattern) else nil
|
|
7473
7548
|
local results = {}
|
|
7474
7549
|
local totalMatches = 0
|
|
7475
7550
|
local scriptsSearched = 0
|
|
@@ -7505,7 +7580,9 @@ local function grepScripts(requestData)
|
|
|
7505
7580
|
local matchStart
|
|
7506
7581
|
local matchEnd
|
|
7507
7582
|
if usePattern then
|
|
7508
|
-
|
|
7583
|
+
local _binding_1 = findFirstPattern(searchLine, patternAlternatives)
|
|
7584
|
+
matchStart = _binding_1[1]
|
|
7585
|
+
matchEnd = _binding_1[2]
|
|
7509
7586
|
else
|
|
7510
7587
|
matchStart, matchEnd = string.find(searchLine, searchPattern, 1, true)
|
|
7511
7588
|
end
|
|
@@ -7997,6 +8074,9 @@ local joinLines = _binding.joinLines
|
|
|
7997
8074
|
local _binding_1 = Recording
|
|
7998
8075
|
local beginRecording = _binding_1.beginRecording
|
|
7999
8076
|
local finishRecording = _binding_1.finishRecording
|
|
8077
|
+
local SOURCE_TRUNCATE_CHAR_BUDGET = 25000
|
|
8078
|
+
local SOURCE_TRUNCATE_LINE_BUDGET = 400
|
|
8079
|
+
local SOURCE_TRUNCATE_TO_LINES = 300
|
|
8000
8080
|
local function normalizeEscapes(s)
|
|
8001
8081
|
local result = s
|
|
8002
8082
|
result = (string.gsub(result, "\\\\", "\x01"))
|
|
@@ -8007,6 +8087,44 @@ local function normalizeEscapes(s)
|
|
|
8007
8087
|
result = (string.gsub(result, "\x01", "\\"))
|
|
8008
8088
|
return result
|
|
8009
8089
|
end
|
|
8090
|
+
local function getTopServiceName(instance)
|
|
8091
|
+
local topServiceInst = instance
|
|
8092
|
+
while topServiceInst.Parent and topServiceInst.Parent ~= game do
|
|
8093
|
+
topServiceInst = topServiceInst.Parent
|
|
8094
|
+
end
|
|
8095
|
+
return topServiceInst.Name
|
|
8096
|
+
end
|
|
8097
|
+
local function sliceLines(lines, startLine, endLine)
|
|
8098
|
+
local selectedLines = {}
|
|
8099
|
+
do
|
|
8100
|
+
local i = startLine
|
|
8101
|
+
local _shouldIncrement = false
|
|
8102
|
+
while true do
|
|
8103
|
+
if _shouldIncrement then
|
|
8104
|
+
i += 1
|
|
8105
|
+
else
|
|
8106
|
+
_shouldIncrement = true
|
|
8107
|
+
end
|
|
8108
|
+
if not (i <= endLine) then
|
|
8109
|
+
break
|
|
8110
|
+
end
|
|
8111
|
+
local _condition = lines[i]
|
|
8112
|
+
if _condition == nil then
|
|
8113
|
+
_condition = ""
|
|
8114
|
+
end
|
|
8115
|
+
table.insert(selectedLines, _condition)
|
|
8116
|
+
end
|
|
8117
|
+
end
|
|
8118
|
+
return selectedLines
|
|
8119
|
+
end
|
|
8120
|
+
local function numberLines(lines, lineOffset)
|
|
8121
|
+
local numberedLines = {}
|
|
8122
|
+
for i = 0, #lines - 1 do
|
|
8123
|
+
local _arg0 = `{i + lineOffset}: {lines[i + 1]}`
|
|
8124
|
+
table.insert(numberedLines, _arg0)
|
|
8125
|
+
end
|
|
8126
|
+
return table.concat(numberedLines, "\n")
|
|
8127
|
+
end
|
|
8010
8128
|
local function getScriptSource(requestData)
|
|
8011
8129
|
local instancePath = requestData.instancePath
|
|
8012
8130
|
local startLine = requestData.startLine
|
|
@@ -8031,105 +8149,58 @@ local function getScriptSource(requestData)
|
|
|
8031
8149
|
local fullSource = readScriptSource(instance)
|
|
8032
8150
|
local lines, hasTrailingNewline = splitLines(fullSource)
|
|
8033
8151
|
local totalLineCount = #lines
|
|
8034
|
-
local
|
|
8035
|
-
local
|
|
8036
|
-
local
|
|
8037
|
-
if
|
|
8152
|
+
local explicitRange = startLine ~= nil or endLine ~= nil
|
|
8153
|
+
local shouldTruncate = not explicitRange and (#fullSource > SOURCE_TRUNCATE_CHAR_BUDGET or totalLineCount > SOURCE_TRUNCATE_LINE_BUDGET)
|
|
8154
|
+
local _result
|
|
8155
|
+
if explicitRange then
|
|
8038
8156
|
local _condition = startLine
|
|
8039
8157
|
if _condition == nil then
|
|
8040
8158
|
_condition = 1
|
|
8041
8159
|
end
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
else
|
|
8057
|
-
_shouldIncrement = true
|
|
8058
|
-
end
|
|
8059
|
-
if not (i <= actualEndLine) then
|
|
8060
|
-
break
|
|
8061
|
-
end
|
|
8062
|
-
local _condition_2 = lines[i]
|
|
8063
|
-
if _condition_2 == nil then
|
|
8064
|
-
_condition_2 = ""
|
|
8065
|
-
end
|
|
8066
|
-
table.insert(selectedLines, _condition_2)
|
|
8160
|
+
_result = math.max(1, _condition)
|
|
8161
|
+
else
|
|
8162
|
+
_result = 1
|
|
8163
|
+
end
|
|
8164
|
+
local returnedStartLine = _result
|
|
8165
|
+
local _result_1
|
|
8166
|
+
if shouldTruncate then
|
|
8167
|
+
_result_1 = math.min(SOURCE_TRUNCATE_TO_LINES, totalLineCount)
|
|
8168
|
+
else
|
|
8169
|
+
local _result_2
|
|
8170
|
+
if explicitRange then
|
|
8171
|
+
local _condition = endLine
|
|
8172
|
+
if _condition == nil then
|
|
8173
|
+
_condition = totalLineCount
|
|
8067
8174
|
end
|
|
8175
|
+
_result_2 = math.min(totalLineCount, _condition)
|
|
8176
|
+
else
|
|
8177
|
+
_result_2 = totalLineCount
|
|
8068
8178
|
end
|
|
8069
|
-
|
|
8070
|
-
if hasTrailingNewline and actualEndLine == #lines and string.sub(sourceToReturn, -1) ~= "\n" then
|
|
8071
|
-
sourceToReturn ..= "\n"
|
|
8072
|
-
end
|
|
8073
|
-
returnedStartLine = actualStartLine
|
|
8074
|
-
returnedEndLine = actualEndLine
|
|
8075
|
-
end
|
|
8076
|
-
local numberedLines = {}
|
|
8077
|
-
local linesToNumber = if startLine ~= nil then (splitLines(sourceToReturn)) else lines
|
|
8078
|
-
local lineOffset = returnedStartLine - 1
|
|
8079
|
-
for i = 0, #linesToNumber - 1 do
|
|
8080
|
-
local _arg0 = `{i + 1 + lineOffset}: {linesToNumber[i + 1]}`
|
|
8081
|
-
table.insert(numberedLines, _arg0)
|
|
8179
|
+
_result_1 = _result_2
|
|
8082
8180
|
end
|
|
8083
|
-
local
|
|
8181
|
+
local returnedEndLine = _result_1
|
|
8182
|
+
local selectedLines = if (explicitRange or shouldTruncate) then sliceLines(lines, returnedStartLine, returnedEndLine) else lines
|
|
8183
|
+
local sourceToReturn = if explicitRange then joinLines(selectedLines, hasTrailingNewline and returnedEndLine == totalLineCount) elseif shouldTruncate then table.concat(selectedLines, "\n") else fullSource
|
|
8084
8184
|
local resp = {
|
|
8085
8185
|
instancePath = instancePath,
|
|
8086
8186
|
className = instance.ClassName,
|
|
8087
8187
|
name = instance.Name,
|
|
8088
8188
|
source = sourceToReturn,
|
|
8089
|
-
numberedSource =
|
|
8189
|
+
numberedSource = numberLines(selectedLines, returnedStartLine),
|
|
8090
8190
|
sourceLength = #fullSource,
|
|
8091
8191
|
lineCount = totalLineCount,
|
|
8092
8192
|
startLine = returnedStartLine,
|
|
8093
8193
|
endLine = returnedEndLine,
|
|
8094
|
-
isPartial =
|
|
8095
|
-
truncated =
|
|
8194
|
+
isPartial = explicitRange,
|
|
8195
|
+
truncated = shouldTruncate,
|
|
8096
8196
|
}
|
|
8097
|
-
if
|
|
8098
|
-
|
|
8099
|
-
local truncatedNumberedLines = {}
|
|
8100
|
-
local maxLines = math.min(1000, #lines)
|
|
8101
|
-
do
|
|
8102
|
-
local i = 0
|
|
8103
|
-
local _shouldIncrement = false
|
|
8104
|
-
while true do
|
|
8105
|
-
if _shouldIncrement then
|
|
8106
|
-
i += 1
|
|
8107
|
-
else
|
|
8108
|
-
_shouldIncrement = true
|
|
8109
|
-
end
|
|
8110
|
-
if not (i < maxLines) then
|
|
8111
|
-
break
|
|
8112
|
-
end
|
|
8113
|
-
local _arg0 = lines[i + 1]
|
|
8114
|
-
table.insert(truncatedLines, _arg0)
|
|
8115
|
-
local _arg0_1 = `{i + 1}: {lines[i + 1]}`
|
|
8116
|
-
table.insert(truncatedNumberedLines, _arg0_1)
|
|
8117
|
-
end
|
|
8118
|
-
end
|
|
8119
|
-
resp.source = table.concat(truncatedLines, "\n")
|
|
8120
|
-
resp.numberedSource = table.concat(truncatedNumberedLines, "\n")
|
|
8121
|
-
resp.truncated = true
|
|
8122
|
-
resp.endLine = maxLines
|
|
8123
|
-
resp.note = "Script truncated to first 1000 lines. Use startLine/endLine parameters to read specific sections."
|
|
8197
|
+
if shouldTruncate then
|
|
8198
|
+
resp.note = `Script truncated to first {returnedEndLine} of {totalLineCount} lines ({#fullSource} chars). Use line_range to read specific sections.`
|
|
8124
8199
|
end
|
|
8125
8200
|
if instance:IsA("BaseScript") then
|
|
8126
8201
|
resp.enabled = instance.Enabled
|
|
8127
8202
|
end
|
|
8128
|
-
|
|
8129
|
-
while topServiceInst.Parent and topServiceInst.Parent ~= game do
|
|
8130
|
-
topServiceInst = topServiceInst.Parent
|
|
8131
|
-
end
|
|
8132
|
-
resp.topService = topServiceInst.Name
|
|
8203
|
+
resp.topService = getTopServiceName(instance)
|
|
8133
8204
|
return resp
|
|
8134
8205
|
end)
|
|
8135
8206
|
if success then
|
|
@@ -9673,25 +9744,13 @@ local function multiplayerTestLeaveClient(_requestData)
|
|
|
9673
9744
|
localPlayer = localPlayer,
|
|
9674
9745
|
}
|
|
9675
9746
|
end
|
|
9676
|
-
local function multiplayerTestEnd(
|
|
9677
|
-
if not RunService:IsRunning() or not RunService:IsServer() then
|
|
9678
|
-
return {
|
|
9679
|
-
error = "multiplayer_test_end must be called on the running server peer. Route with target=server.",
|
|
9680
|
-
}
|
|
9681
|
-
end
|
|
9682
|
-
local value = if requestData.value ~= nil then requestData.value else "ended_by_mcp"
|
|
9683
|
-
local ok, result = pcall(function()
|
|
9684
|
-
return StudioTestService:EndTest(value)
|
|
9685
|
-
end)
|
|
9686
|
-
if not ok then
|
|
9687
|
-
return {
|
|
9688
|
-
error = tostring(result),
|
|
9689
|
-
}
|
|
9690
|
-
end
|
|
9747
|
+
local function multiplayerTestEnd(_requestData)
|
|
9691
9748
|
return {
|
|
9692
|
-
success =
|
|
9693
|
-
|
|
9694
|
-
|
|
9749
|
+
success = false,
|
|
9750
|
+
error = "multiplayer_stop_disabled",
|
|
9751
|
+
message = "Multiplayer playtest stop/end is disabled because StudioTestService:EndTest is currently broken for this flow. Manually close the Studio multiplayer test windows instead.",
|
|
9752
|
+
reason = "StudioTestService:EndTest does not reliably end StudioTestService multiplayer sessions from MCP right now.",
|
|
9753
|
+
manualCleanupRequired = true,
|
|
9695
9754
|
}
|
|
9696
9755
|
end
|
|
9697
9756
|
return {
|
|
@@ -10580,7 +10639,7 @@ local function addUnique(values, value)
|
|
|
10580
10639
|
table.insert(_values_1, _value_1)
|
|
10581
10640
|
end
|
|
10582
10641
|
end
|
|
10583
|
-
local function computeInstanceIds()
|
|
10642
|
+
local function computeInstanceIds(options)
|
|
10584
10643
|
local ids = {}
|
|
10585
10644
|
if game.PlaceId ~= 0 then
|
|
10586
10645
|
addUnique(ids, `place:{tostring(game.PlaceId)}`)
|
|
@@ -10588,12 +10647,22 @@ local function computeInstanceIds()
|
|
|
10588
10647
|
local existing = ServerStorage:GetAttribute("__MCPPlaceId")
|
|
10589
10648
|
if type(existing) == "string" and existing ~= "" then
|
|
10590
10649
|
addUnique(ids, `anon:{existing}`)
|
|
10591
|
-
|
|
10592
|
-
local
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10650
|
+
else
|
|
10651
|
+
local _condition = game.PlaceId == 0
|
|
10652
|
+
if _condition then
|
|
10653
|
+
local _result = options
|
|
10654
|
+
if _result ~= nil then
|
|
10655
|
+
_result = _result.createAnonymous
|
|
10656
|
+
end
|
|
10657
|
+
_condition = _result == true
|
|
10658
|
+
end
|
|
10659
|
+
if _condition then
|
|
10660
|
+
local fresh = HttpService:GenerateGUID(false)
|
|
10661
|
+
pcall(function()
|
|
10662
|
+
return ServerStorage:SetAttribute("__MCPPlaceId", fresh)
|
|
10663
|
+
end)
|
|
10664
|
+
addUnique(ids, `anon:{fresh}`)
|
|
10665
|
+
end
|
|
10597
10666
|
end
|
|
10598
10667
|
return ids
|
|
10599
10668
|
end
|
|
@@ -10630,7 +10699,9 @@ local function rememberServerUrl(serverUrl)
|
|
|
10630
10699
|
return nil
|
|
10631
10700
|
end
|
|
10632
10701
|
writeSettingString(GLOBAL_SETTING_KEY, normalized)
|
|
10633
|
-
for _, instanceId in computeInstanceIds(
|
|
10702
|
+
for _, instanceId in computeInstanceIds({
|
|
10703
|
+
createAnonymous = true,
|
|
10704
|
+
}) do
|
|
10634
10705
|
writeSettingString(settingKey(instanceId), normalized)
|
|
10635
10706
|
writeSettingString(legacySettingKey(instanceId), normalized)
|
|
10636
10707
|
end
|
|
@@ -10639,6 +10710,9 @@ local function readServerUrl()
|
|
|
10639
10710
|
if not pluginRef then
|
|
10640
10711
|
return nil
|
|
10641
10712
|
end
|
|
10713
|
+
-- Reading settings should not mint a place identity. Client play DMs have
|
|
10714
|
+
-- their own ServerStorage; creating an id there makes a misleading anon id
|
|
10715
|
+
-- that never matches the edit/server bridge identity.
|
|
10642
10716
|
for _, instanceId in computeInstanceIds() do
|
|
10643
10717
|
local remembered = readSettingString(settingKey(instanceId))
|
|
10644
10718
|
if remembered ~= nil then
|
|
@@ -10671,7 +10745,7 @@ return {
|
|
|
10671
10745
|
<Properties>
|
|
10672
10746
|
<string name="Name">State</string>
|
|
10673
10747
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
10674
|
-
local CURRENT_VERSION = "2.19.
|
|
10748
|
+
local CURRENT_VERSION = "2.19.1"
|
|
10675
10749
|
local PLUGIN_VARIANT = "main"
|
|
10676
10750
|
local BASE_PORT = 58741
|
|
10677
10751
|
local function createConnection(port)
|
|
@@ -12028,6 +12102,38 @@ local function convertPropertyValue(instance, propertyName, propertyValue)
|
|
|
12028
12102
|
end
|
|
12029
12103
|
return UDim2.new(_condition_1, _condition_2, _condition_3, _condition_4)
|
|
12030
12104
|
end
|
|
12105
|
+
local success, currentVal = pcall(function()
|
|
12106
|
+
return inst[propertyName]
|
|
12107
|
+
end)
|
|
12108
|
+
if success then
|
|
12109
|
+
local currentType = typeof(currentVal)
|
|
12110
|
+
if currentType == "Vector2" then
|
|
12111
|
+
local _condition_1 = (tbl.X)
|
|
12112
|
+
if _condition_1 == nil then
|
|
12113
|
+
_condition_1 = 0
|
|
12114
|
+
end
|
|
12115
|
+
local _condition_2 = (tbl.Y)
|
|
12116
|
+
if _condition_2 == nil then
|
|
12117
|
+
_condition_2 = 0
|
|
12118
|
+
end
|
|
12119
|
+
return Vector2.new(_condition_1, _condition_2)
|
|
12120
|
+
end
|
|
12121
|
+
if currentType == "Vector3" then
|
|
12122
|
+
local _condition_1 = (tbl.X)
|
|
12123
|
+
if _condition_1 == nil then
|
|
12124
|
+
_condition_1 = 0
|
|
12125
|
+
end
|
|
12126
|
+
local _condition_2 = (tbl.Y)
|
|
12127
|
+
if _condition_2 == nil then
|
|
12128
|
+
_condition_2 = 0
|
|
12129
|
+
end
|
|
12130
|
+
local _condition_3 = (tbl.Z)
|
|
12131
|
+
if _condition_3 == nil then
|
|
12132
|
+
_condition_3 = 0
|
|
12133
|
+
end
|
|
12134
|
+
return Vector3.new(_condition_1, _condition_2, _condition_3)
|
|
12135
|
+
end
|
|
12136
|
+
end
|
|
12031
12137
|
local _condition_1 = (tbl.X)
|
|
12032
12138
|
if _condition_1 == nil then
|
|
12033
12139
|
_condition_1 = 0
|
|
@@ -41,7 +41,7 @@ function addUnique(values: string[], value: string): void {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function computeInstanceIds(): string[] {
|
|
44
|
+
function computeInstanceIds(options?: { createAnonymous?: boolean }): string[] {
|
|
45
45
|
const ids: string[] = [];
|
|
46
46
|
if (game.PlaceId !== 0) {
|
|
47
47
|
addUnique(ids, `place:${tostring(game.PlaceId)}`);
|
|
@@ -49,7 +49,7 @@ function computeInstanceIds(): string[] {
|
|
|
49
49
|
const existing = ServerStorage.GetAttribute("__MCPPlaceId");
|
|
50
50
|
if (typeIs(existing, "string") && existing !== "") {
|
|
51
51
|
addUnique(ids, `anon:${existing as string}`);
|
|
52
|
-
} else if (game.PlaceId === 0) {
|
|
52
|
+
} else if (game.PlaceId === 0 && options?.createAnonymous === true) {
|
|
53
53
|
const fresh = HttpService.GenerateGUID(false);
|
|
54
54
|
pcall(() => ServerStorage.SetAttribute("__MCPPlaceId", fresh));
|
|
55
55
|
addUnique(ids, `anon:${fresh}`);
|
|
@@ -83,7 +83,7 @@ function rememberServerUrl(serverUrl: string): void {
|
|
|
83
83
|
const normalized = normalizeServerUrl(serverUrl);
|
|
84
84
|
if (!pluginRef || normalized === "") return;
|
|
85
85
|
writeSettingString(GLOBAL_SETTING_KEY, normalized);
|
|
86
|
-
for (const instanceId of computeInstanceIds()) {
|
|
86
|
+
for (const instanceId of computeInstanceIds({ createAnonymous: true })) {
|
|
87
87
|
writeSettingString(settingKey(instanceId), normalized);
|
|
88
88
|
writeSettingString(legacySettingKey(instanceId), normalized);
|
|
89
89
|
}
|
|
@@ -91,6 +91,9 @@ function rememberServerUrl(serverUrl: string): void {
|
|
|
91
91
|
|
|
92
92
|
function readServerUrl(): string | undefined {
|
|
93
93
|
if (!pluginRef) return undefined;
|
|
94
|
+
// Reading settings should not mint a place identity. Client play DMs have
|
|
95
|
+
// their own ServerStorage; creating an id there makes a misleading anon id
|
|
96
|
+
// that never matches the edit/server bridge identity.
|
|
94
97
|
for (const instanceId of computeInstanceIds()) {
|
|
95
98
|
const remembered = readSettingString(settingKey(instanceId));
|
|
96
99
|
if (remembered !== undefined) return remembered;
|
|
@@ -305,6 +305,23 @@ function convertPropertyValue(instance: Instance, propertyName: string, property
|
|
|
305
305
|
yTbl.Scale ?? 0, yTbl.Offset ?? 0,
|
|
306
306
|
);
|
|
307
307
|
}
|
|
308
|
+
const [success, currentVal] = pcall(() => inst[propertyName]);
|
|
309
|
+
if (success) {
|
|
310
|
+
const currentType = typeOf(currentVal);
|
|
311
|
+
if (currentType === "Vector2") {
|
|
312
|
+
return new Vector2(
|
|
313
|
+
(tbl.X as number) ?? 0,
|
|
314
|
+
(tbl.Y as number) ?? 0,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
if (currentType === "Vector3") {
|
|
318
|
+
return new Vector3(
|
|
319
|
+
(tbl.X as number) ?? 0,
|
|
320
|
+
(tbl.Y as number) ?? 0,
|
|
321
|
+
(tbl.Z as number) ?? 0,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
308
325
|
return new Vector3(
|
|
309
326
|
(tbl.X as number) ?? 0,
|
|
310
327
|
(tbl.Y as number) ?? 0,
|
|
@@ -564,15 +564,78 @@ function getProjectStructure(requestData: Record<string, unknown>) {
|
|
|
564
564
|
return result;
|
|
565
565
|
}
|
|
566
566
|
|
|
567
|
+
// Split a Lua pattern on TOP-LEVEL "|" into alternatives. Lua patterns have no
|
|
568
|
+
// alternation operator, so "foo|bar" would otherwise be matched as the literal
|
|
569
|
+
// text "foo|bar" and silently never hit. "%|" stays a literal pipe, and "%bxy"
|
|
570
|
+
// keeps both balanced-match delimiter characters.
|
|
571
|
+
function splitLuaAlternation(pattern: string): string[] {
|
|
572
|
+
const parts: string[] = [];
|
|
573
|
+
let current = "";
|
|
574
|
+
let i = 1;
|
|
575
|
+
const n = pattern.size();
|
|
576
|
+
let inCharClass = false;
|
|
577
|
+
while (i <= n) {
|
|
578
|
+
const c = string.sub(pattern, i, i);
|
|
579
|
+
if (c === "%") {
|
|
580
|
+
if (string.sub(pattern, i + 1, i + 1) === "b") {
|
|
581
|
+
current += string.sub(pattern, i, math.min(i + 3, n));
|
|
582
|
+
i += 4;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
// Preserve an escape pair (e.g. %|, %., %d) intact.
|
|
586
|
+
current += string.sub(pattern, i, i + 1);
|
|
587
|
+
i += 2;
|
|
588
|
+
} else if (c === "[") {
|
|
589
|
+
inCharClass = true;
|
|
590
|
+
current += c;
|
|
591
|
+
i += 1;
|
|
592
|
+
} else if (c === "]") {
|
|
593
|
+
inCharClass = false;
|
|
594
|
+
current += c;
|
|
595
|
+
i += 1;
|
|
596
|
+
} else if (c === "|" && !inCharClass) {
|
|
597
|
+
parts.push(current);
|
|
598
|
+
current = "";
|
|
599
|
+
i += 1;
|
|
600
|
+
} else {
|
|
601
|
+
current += c;
|
|
602
|
+
i += 1;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
parts.push(current);
|
|
606
|
+
return parts;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Return the earliest match across alternatives (mirrors regex alternation).
|
|
610
|
+
function findFirstPattern(line: string, alternatives: string[]): [number | undefined, number | undefined] {
|
|
611
|
+
let bestStart: number | undefined;
|
|
612
|
+
let bestEnd: number | undefined;
|
|
613
|
+
for (const alt of alternatives) {
|
|
614
|
+
if (alt === "") continue;
|
|
615
|
+
const [s, e] = string.find(line, alt);
|
|
616
|
+
if (s !== undefined && (bestStart === undefined || s < bestStart)) {
|
|
617
|
+
bestStart = s;
|
|
618
|
+
bestEnd = e as number;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return [bestStart, bestEnd];
|
|
622
|
+
}
|
|
623
|
+
|
|
567
624
|
function grepScripts(requestData: Record<string, unknown>) {
|
|
568
625
|
const pattern = requestData.pattern as string;
|
|
569
626
|
if (!pattern) return { error: "pattern is required" };
|
|
570
627
|
|
|
571
|
-
const
|
|
628
|
+
const usePattern = (requestData.usePattern as boolean) ?? false;
|
|
629
|
+
if (usePattern && requestData.caseSensitive === false) {
|
|
630
|
+
return {
|
|
631
|
+
error: "Case-insensitive Lua pattern search is not supported. Omit caseSensitive or pass caseSensitive: true with usePattern: true, or use literal search.",
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const caseSensitive = usePattern ? true : ((requestData.caseSensitive as boolean) ?? false);
|
|
572
636
|
const contextLines = (requestData.contextLines as number) ?? 0;
|
|
573
637
|
const maxResults = (requestData.maxResults as number) ?? 100;
|
|
574
638
|
const maxResultsPerScript = (requestData.maxResultsPerScript as number) ?? 0;
|
|
575
|
-
const usePattern = (requestData.usePattern as boolean) ?? false;
|
|
576
639
|
const filesOnly = (requestData.filesOnly as boolean) ?? false;
|
|
577
640
|
const searchPath = (requestData.path as string) ?? "";
|
|
578
641
|
const classFilter = requestData.classFilter as string | undefined;
|
|
@@ -582,6 +645,8 @@ function grepScripts(requestData: Record<string, unknown>) {
|
|
|
582
645
|
|
|
583
646
|
// Prepare pattern for matching
|
|
584
647
|
const searchPattern = caseSensitive ? pattern : pattern.lower();
|
|
648
|
+
// Pre-split top-level "|" alternation once (pattern mode only).
|
|
649
|
+
const patternAlternatives = usePattern ? splitLuaAlternation(searchPattern) : undefined;
|
|
585
650
|
|
|
586
651
|
interface LineMatch {
|
|
587
652
|
line: number;
|
|
@@ -630,7 +695,7 @@ function grepScripts(requestData: Record<string, unknown>) {
|
|
|
630
695
|
let matchEnd: number | undefined;
|
|
631
696
|
|
|
632
697
|
if (usePattern) {
|
|
633
|
-
[matchStart, matchEnd] =
|
|
698
|
+
[matchStart, matchEnd] = findFirstPattern(searchLine, patternAlternatives!);
|
|
634
699
|
} else {
|
|
635
700
|
[matchStart, matchEnd] = string.find(searchLine, searchPattern, 1, true);
|
|
636
701
|
}
|