@chrrxs/robloxstudio-mcp 2.17.0 → 2.18.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 +968 -253
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +361 -253
- package/studio-plugin/MCPPlugin.rbxmx +361 -253
- package/studio-plugin/src/modules/Communication.ts +7 -5
- package/studio-plugin/src/modules/ServerUrlSettings.ts +62 -9
- package/studio-plugin/src/modules/UI.ts +11 -4
- package/studio-plugin/src/modules/Utils.ts +147 -13
- package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +3 -0
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +1 -177
- package/studio-plugin/src/server/index.server.ts +18 -5
|
@@ -32,6 +32,20 @@ RuntimeLogBuffer.install()
|
|
|
32
32
|
StopPlayMonitor.init(plugin)
|
|
33
33
|
BreakpointHandlers.init(plugin)
|
|
34
34
|
ServerUrlSettings.init(plugin)
|
|
35
|
+
local function applyRememberedServerUrl()
|
|
36
|
+
local rememberedServerUrl = ServerUrlSettings.readServerUrl()
|
|
37
|
+
if rememberedServerUrl == nil then
|
|
38
|
+
return nil
|
|
39
|
+
end
|
|
40
|
+
local conn = State.getActiveConnection()
|
|
41
|
+
conn.serverUrl = rememberedServerUrl
|
|
42
|
+
local port = ServerUrlSettings.extractPort(rememberedServerUrl)
|
|
43
|
+
if port ~= nil then
|
|
44
|
+
conn.port = port
|
|
45
|
+
end
|
|
46
|
+
ClientBroker.setServerUrl(rememberedServerUrl)
|
|
47
|
+
end
|
|
48
|
+
applyRememberedServerUrl()
|
|
35
49
|
UI.init(plugin)
|
|
36
50
|
local elements = UI.getElements()
|
|
37
51
|
local ICON_DISCONNECTED = "rbxassetid://75876056391496"
|
|
@@ -94,17 +108,13 @@ task.delay(2, function()
|
|
|
94
108
|
_condition = ClientBroker.DEFAULT_MCP_URL
|
|
95
109
|
end
|
|
96
110
|
local inheritedServerUrl = _condition
|
|
97
|
-
conn.serverUrl = inheritedServerUrl
|
|
98
|
-
elements.urlInput.Text =
|
|
99
|
-
local
|
|
100
|
-
if
|
|
101
|
-
|
|
102
|
-
if _condition_1 == nil then
|
|
103
|
-
_condition_1 = conn.port
|
|
104
|
-
end
|
|
105
|
-
conn.port = _condition_1
|
|
111
|
+
conn.serverUrl = ServerUrlSettings.normalizeServerUrl(inheritedServerUrl)
|
|
112
|
+
elements.urlInput.Text = conn.serverUrl
|
|
113
|
+
local port = ServerUrlSettings.extractPort(conn.serverUrl)
|
|
114
|
+
if port ~= nil then
|
|
115
|
+
conn.port = port
|
|
106
116
|
end
|
|
107
|
-
ClientBroker.setServerUrl(
|
|
117
|
+
ClientBroker.setServerUrl(conn.serverUrl)
|
|
108
118
|
end
|
|
109
119
|
-- Defensive default: in invisible play-DM UIs, the input field
|
|
110
120
|
-- may not be populated by the time we activate.
|
|
@@ -789,7 +799,6 @@ local routeMap = {
|
|
|
789
799
|
["/api/multiplayer-test-add-players"] = TestHandlers.multiplayerTestAddPlayers,
|
|
790
800
|
["/api/multiplayer-test-leave-client"] = TestHandlers.multiplayerTestLeaveClient,
|
|
791
801
|
["/api/multiplayer-test-end"] = TestHandlers.multiplayerTestEnd,
|
|
792
|
-
["/api/character-navigation"] = TestHandlers.characterNavigation,
|
|
793
802
|
["/api/export-build"] = BuildHandlers.exportBuild,
|
|
794
803
|
["/api/import-build"] = BuildHandlers.importBuild,
|
|
795
804
|
["/api/import-scene"] = BuildHandlers.importScene,
|
|
@@ -962,6 +971,7 @@ function sendReady(conn)
|
|
|
962
971
|
end
|
|
963
972
|
end
|
|
964
973
|
lastReadyInstanceId = if _condition then readyData.instanceId else instanceId
|
|
974
|
+
ServerUrlSettings.rememberServerUrl(conn.serverUrl)
|
|
965
975
|
local _condition_1 = assignedRole
|
|
966
976
|
if _condition_1 == nil then
|
|
967
977
|
_condition_1 = detectRole()
|
|
@@ -1172,19 +1182,19 @@ local function activatePlugin(connIndex)
|
|
|
1172
1182
|
conn.consecutiveFailures = 0
|
|
1173
1183
|
conn.currentRetryDelay = 0.5
|
|
1174
1184
|
if idx == State.getActiveTabIndex() then
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
if
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1185
|
+
local normalizedUrl = ServerUrlSettings.normalizeServerUrl(ui.urlInput.Text)
|
|
1186
|
+
conn.serverUrl = if normalizedUrl ~= "" then normalizedUrl else conn.serverUrl
|
|
1187
|
+
if conn.serverUrl == "" then
|
|
1188
|
+
conn.serverUrl = ClientBroker.DEFAULT_MCP_URL
|
|
1189
|
+
end
|
|
1190
|
+
ui.urlInput.Text = conn.serverUrl
|
|
1191
|
+
local port = ServerUrlSettings.extractPort(conn.serverUrl)
|
|
1192
|
+
if port ~= nil then
|
|
1193
|
+
conn.port = port
|
|
1183
1194
|
end
|
|
1184
1195
|
UI.updateTabLabel(idx)
|
|
1185
1196
|
UI.updateUIState()
|
|
1186
1197
|
end
|
|
1187
|
-
ServerUrlSettings.rememberServerUrl(conn.serverUrl)
|
|
1188
1198
|
UI.updateTabDot(idx)
|
|
1189
1199
|
if not conn.heartbeatConnection then
|
|
1190
1200
|
conn.heartbeatConnection = RunService.Heartbeat:Connect(function()
|
|
@@ -1425,9 +1435,9 @@ local function computeBridgeStamp()
|
|
|
1425
1435
|
for i = 1, #combined do
|
|
1426
1436
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1427
1437
|
end
|
|
1428
|
-
-- "2.
|
|
1438
|
+
-- "2.18.0" is replaced with the package version at package time
|
|
1429
1439
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1430
|
-
return `{tostring(h)}-2.
|
|
1440
|
+
return `{tostring(h)}-2.18.0`
|
|
1431
1441
|
end
|
|
1432
1442
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1433
1443
|
local function setSource(scriptInst, source)
|
|
@@ -6332,6 +6342,9 @@ local function setScriptSource(requestData)
|
|
|
6332
6342
|
ScriptEditorService:UpdateSourceAsync(instance, function()
|
|
6333
6343
|
return sourceToSet
|
|
6334
6344
|
end)
|
|
6345
|
+
if readScriptSource(instance) ~= sourceToSet then
|
|
6346
|
+
error("UpdateSourceAsync completed without updating the script source")
|
|
6347
|
+
end
|
|
6335
6348
|
return {
|
|
6336
6349
|
success = true,
|
|
6337
6350
|
instancePath = instancePath,
|
|
@@ -7498,23 +7511,11 @@ return {
|
|
|
7498
7511
|
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
7499
7512
|
local _services = TS.import(script, script.Parent.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
7500
7513
|
local HttpService = _services.HttpService
|
|
7501
|
-
local LogService = _services.LogService
|
|
7502
7514
|
local Players = _services.Players
|
|
7503
7515
|
local RunService = _services.RunService
|
|
7504
7516
|
local StopPlayMonitor = TS.import(script, script.Parent.Parent, "StopPlayMonitor")
|
|
7505
7517
|
local StudioTestService = game:GetService("StudioTestService")
|
|
7506
|
-
local ServerScriptService = game:GetService("ServerScriptService")
|
|
7507
|
-
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
7508
|
-
-- NAV_SIGNAL flows from the edit DM to the play-server DM via the injected
|
|
7509
|
-
-- __MCP_CommandListener Script + LogService.MessageOut. Stop signaling moved
|
|
7510
|
-
-- off this path entirely (see StopPlayMonitor) because cross-DM MessageOut
|
|
7511
|
-
-- reflection from edit -> play-server does not work in practice.
|
|
7512
|
-
local NAV_SIGNAL = "__MCP_NAV__"
|
|
7513
|
-
local NAV_RESULT = "__MCP_NAV_RESULT__"
|
|
7514
7518
|
local testRunning = false
|
|
7515
|
-
local navLogConnection
|
|
7516
|
-
local stopListenerScript
|
|
7517
|
-
local navResultCallback
|
|
7518
7519
|
local multiplayerState = {
|
|
7519
7520
|
phase = "idle",
|
|
7520
7521
|
}
|
|
@@ -7572,100 +7573,6 @@ local function normalizeNumPlayers(value)
|
|
|
7572
7573
|
end
|
|
7573
7574
|
return n
|
|
7574
7575
|
end
|
|
7575
|
-
local function buildCommandListenerSource()
|
|
7576
|
-
return `local LogService = game:GetService("LogService")\
|
|
7577
|
-
local PathfindingService = game:GetService("PathfindingService")\
|
|
7578
|
-
local Players = game:GetService("Players")\
|
|
7579
|
-
local HttpService = game:GetService("HttpService")\
|
|
7580
|
-
local NAV_SIG = "{NAV_SIGNAL}"\
|
|
7581
|
-
local NAV_RES = "{NAV_RESULT}"\
|
|
7582
|
-
LogService.MessageOut:Connect(function(msg)\
|
|
7583
|
-
if string.sub(msg, 1, #NAV_SIG + 1) == NAV_SIG .. ":" then\
|
|
7584
|
-
local json = string.sub(msg, #NAV_SIG + 2)\
|
|
7585
|
-
task.spawn(function()\
|
|
7586
|
-
local ok, d = pcall(function() return HttpService:JSONDecode(json) end)\
|
|
7587
|
-
if not ok or not d then\
|
|
7588
|
-
print(NAV_RES .. ':\{"success":false,"error":"parse_error"\}')\
|
|
7589
|
-
return\
|
|
7590
|
-
end\
|
|
7591
|
-
local ps = Players:GetPlayers()\
|
|
7592
|
-
if #ps == 0 then\
|
|
7593
|
-
print(NAV_RES .. ':\{"success":false,"error":"no_players"\}')\
|
|
7594
|
-
return\
|
|
7595
|
-
end\
|
|
7596
|
-
local char = ps[1].Character or ps[1].CharacterAdded:Wait()\
|
|
7597
|
-
local hum = char:FindFirstChildOfClass("Humanoid")\
|
|
7598
|
-
local root = char:FindFirstChild("HumanoidRootPart")\
|
|
7599
|
-
if not hum or not root then\
|
|
7600
|
-
print(NAV_RES .. ':\{"success":false,"error":"no_humanoid"\}')\
|
|
7601
|
-
return\
|
|
7602
|
-
end\
|
|
7603
|
-
local target\
|
|
7604
|
-
if d.instancePath then\
|
|
7605
|
-
local parts = string.split(d.instancePath, ".")\
|
|
7606
|
-
local cur = game\
|
|
7607
|
-
for i = 2, #parts do\
|
|
7608
|
-
cur = cur:FindFirstChild(parts[i])\
|
|
7609
|
-
if not cur then\
|
|
7610
|
-
print(NAV_RES .. ':\{"success":false,"error":"instance_not_found"\}')\
|
|
7611
|
-
return\
|
|
7612
|
-
end\
|
|
7613
|
-
end\
|
|
7614
|
-
if cur:IsA("BasePart") then target = cur.Position\
|
|
7615
|
-
elseif cur:IsA("Model") and cur.PrimaryPart then target = cur.PrimaryPart.Position\
|
|
7616
|
-
else target = cur:GetPivot().Position end\
|
|
7617
|
-
else\
|
|
7618
|
-
target = Vector3.new(d.x or 0, d.y or 0, d.z or 0)\
|
|
7619
|
-
end\
|
|
7620
|
-
local path = PathfindingService:CreatePath(\{AgentRadius=2,AgentHeight=5,AgentCanJump=true\})\
|
|
7621
|
-
local pok = pcall(function() path:ComputeAsync(root.Position, target) end)\
|
|
7622
|
-
local method = "direct"\
|
|
7623
|
-
if pok and path.Status == Enum.PathStatus.Success then\
|
|
7624
|
-
method = "pathfinding"\
|
|
7625
|
-
for _, wp in ipairs(path:GetWaypoints()) do\
|
|
7626
|
-
hum:MoveTo(wp.Position)\
|
|
7627
|
-
if wp.Action == Enum.PathWaypointAction.Jump then hum.Jump = true end\
|
|
7628
|
-
hum.MoveToFinished:Wait()\
|
|
7629
|
-
end\
|
|
7630
|
-
else\
|
|
7631
|
-
hum:MoveTo(target)\
|
|
7632
|
-
hum.MoveToFinished:Wait()\
|
|
7633
|
-
end\
|
|
7634
|
-
local fp = root.Position\
|
|
7635
|
-
print(NAV_RES .. ':\{"success":true,"method":"' .. method .. '","position":[' .. fp.X .. ',' .. fp.Y .. ',' .. fp.Z .. ']\}')\
|
|
7636
|
-
end)\
|
|
7637
|
-
end\
|
|
7638
|
-
end)`
|
|
7639
|
-
end
|
|
7640
|
-
local function injectStopListener()
|
|
7641
|
-
local listener = Instance.new("Script")
|
|
7642
|
-
listener.Name = "__MCP_CommandListener"
|
|
7643
|
-
listener.Parent = ServerScriptService
|
|
7644
|
-
local source = buildCommandListenerSource()
|
|
7645
|
-
local seOk = pcall(function()
|
|
7646
|
-
ScriptEditorService:UpdateSourceAsync(listener, function()
|
|
7647
|
-
return source
|
|
7648
|
-
end)
|
|
7649
|
-
end)
|
|
7650
|
-
if not seOk then
|
|
7651
|
-
listener.Source = source
|
|
7652
|
-
end
|
|
7653
|
-
stopListenerScript = listener
|
|
7654
|
-
end
|
|
7655
|
-
local function cleanupStopListener()
|
|
7656
|
-
if stopListenerScript then
|
|
7657
|
-
pcall(function()
|
|
7658
|
-
return stopListenerScript:Destroy()
|
|
7659
|
-
end)
|
|
7660
|
-
stopListenerScript = nil
|
|
7661
|
-
end
|
|
7662
|
-
end
|
|
7663
|
-
local function disconnectNavLogListener()
|
|
7664
|
-
if navLogConnection then
|
|
7665
|
-
navLogConnection:Disconnect()
|
|
7666
|
-
navLogConnection = nil
|
|
7667
|
-
end
|
|
7668
|
-
end
|
|
7669
7576
|
local function startPlaytest(requestData)
|
|
7670
7577
|
local mode = requestData.mode
|
|
7671
7578
|
local numPlayers = requestData.numPlayers
|
|
@@ -7685,8 +7592,6 @@ local function startPlaytest(requestData)
|
|
|
7685
7592
|
-- Reset it so subsequent starts don't hit a false "already running".
|
|
7686
7593
|
if testRunning and not RunService:IsRunning() then
|
|
7687
7594
|
testRunning = false
|
|
7688
|
-
disconnectNavLogListener()
|
|
7689
|
-
cleanupStopListener()
|
|
7690
7595
|
-- Runtime eval bridges are created by the play server/client plugin
|
|
7691
7596
|
-- peers and disappear with the play DataModels.
|
|
7692
7597
|
end
|
|
@@ -7696,26 +7601,6 @@ local function startPlaytest(requestData)
|
|
|
7696
7601
|
}
|
|
7697
7602
|
end
|
|
7698
7603
|
testRunning = true
|
|
7699
|
-
cleanupStopListener()
|
|
7700
|
-
disconnectNavLogListener()
|
|
7701
|
-
navLogConnection = LogService.MessageOut:Connect(function(message)
|
|
7702
|
-
local _message = message
|
|
7703
|
-
local _arg1 = #NAV_RESULT + 1
|
|
7704
|
-
if string.sub(_message, 1, _arg1) == `{NAV_RESULT}:` then
|
|
7705
|
-
if navResultCallback then
|
|
7706
|
-
local _fn = navResultCallback
|
|
7707
|
-
local _message_1 = message
|
|
7708
|
-
local _arg0 = #NAV_RESULT + 2
|
|
7709
|
-
_fn(string.sub(_message_1, _arg0))
|
|
7710
|
-
end
|
|
7711
|
-
end
|
|
7712
|
-
end)
|
|
7713
|
-
local injected, injErr = pcall(function()
|
|
7714
|
-
return injectStopListener()
|
|
7715
|
-
end)
|
|
7716
|
-
if not injected then
|
|
7717
|
-
warn(`[robloxstudio-mcp] Failed to inject stop listener: {injErr}`)
|
|
7718
|
-
end
|
|
7719
7604
|
task.spawn(function()
|
|
7720
7605
|
local ok, result = pcall(function()
|
|
7721
7606
|
if mode == "play" then
|
|
@@ -7726,9 +7611,7 @@ local function startPlaytest(requestData)
|
|
|
7726
7611
|
if not ok then
|
|
7727
7612
|
warn(`[robloxstudio-mcp] Playtest ended with error: {result}`)
|
|
7728
7613
|
end
|
|
7729
|
-
disconnectNavLogListener()
|
|
7730
7614
|
testRunning = false
|
|
7731
|
-
cleanupStopListener()
|
|
7732
7615
|
end)
|
|
7733
7616
|
local response = {
|
|
7734
7617
|
success = true,
|
|
@@ -7987,73 +7870,6 @@ local function multiplayerTestEnd(requestData)
|
|
|
7987
7870
|
value = value,
|
|
7988
7871
|
}
|
|
7989
7872
|
end
|
|
7990
|
-
local function characterNavigation(requestData)
|
|
7991
|
-
if not testRunning then
|
|
7992
|
-
return {
|
|
7993
|
-
error = "Playtest must be running. Start a playtest in 'play' mode first.",
|
|
7994
|
-
}
|
|
7995
|
-
end
|
|
7996
|
-
local position = requestData.position
|
|
7997
|
-
local instancePath = requestData.instancePath
|
|
7998
|
-
local _condition = (requestData.waitForCompletion)
|
|
7999
|
-
if _condition == nil then
|
|
8000
|
-
_condition = true
|
|
8001
|
-
end
|
|
8002
|
-
local waitForCompletion = _condition
|
|
8003
|
-
local _condition_1 = (requestData.timeout)
|
|
8004
|
-
if _condition_1 == nil then
|
|
8005
|
-
_condition_1 = 25
|
|
8006
|
-
end
|
|
8007
|
-
local timeout = _condition_1
|
|
8008
|
-
if not position and not (instancePath ~= "" and instancePath) then
|
|
8009
|
-
return {
|
|
8010
|
-
error = "Either position [x, y, z] or instancePath is required",
|
|
8011
|
-
}
|
|
8012
|
-
end
|
|
8013
|
-
local navData
|
|
8014
|
-
if position then
|
|
8015
|
-
navData = HttpService:JSONEncode({
|
|
8016
|
-
x = position[1],
|
|
8017
|
-
y = position[2],
|
|
8018
|
-
z = position[3],
|
|
8019
|
-
})
|
|
8020
|
-
else
|
|
8021
|
-
navData = HttpService:JSONEncode({
|
|
8022
|
-
instancePath = instancePath,
|
|
8023
|
-
})
|
|
8024
|
-
end
|
|
8025
|
-
warn(`{NAV_SIGNAL}:{navData}`)
|
|
8026
|
-
if not waitForCompletion then
|
|
8027
|
-
return {
|
|
8028
|
-
success = true,
|
|
8029
|
-
message = "Navigation command sent",
|
|
8030
|
-
}
|
|
8031
|
-
end
|
|
8032
|
-
local result
|
|
8033
|
-
navResultCallback = function(json)
|
|
8034
|
-
result = json
|
|
8035
|
-
end
|
|
8036
|
-
local startTime = tick()
|
|
8037
|
-
while not (result ~= "" and result) and tick() - startTime < timeout do
|
|
8038
|
-
task.wait(0.2)
|
|
8039
|
-
end
|
|
8040
|
-
navResultCallback = nil
|
|
8041
|
-
if result ~= "" and result then
|
|
8042
|
-
local ok, parsed = pcall(function()
|
|
8043
|
-
return HttpService:JSONDecode(result)
|
|
8044
|
-
end)
|
|
8045
|
-
if ok then
|
|
8046
|
-
return parsed
|
|
8047
|
-
end
|
|
8048
|
-
return {
|
|
8049
|
-
success = true,
|
|
8050
|
-
rawResult = result,
|
|
8051
|
-
}
|
|
8052
|
-
end
|
|
8053
|
-
return {
|
|
8054
|
-
error = `Navigation timed out after {timeout} seconds`,
|
|
8055
|
-
}
|
|
8056
|
-
end
|
|
8057
7873
|
return {
|
|
8058
7874
|
startPlaytest = startPlaytest,
|
|
8059
7875
|
stopPlaytest = stopPlaytest,
|
|
@@ -8062,7 +7878,6 @@ return {
|
|
|
8062
7878
|
multiplayerTestAddPlayers = multiplayerTestAddPlayers,
|
|
8063
7879
|
multiplayerTestLeaveClient = multiplayerTestLeaveClient,
|
|
8064
7880
|
multiplayerTestEnd = multiplayerTestEnd,
|
|
8065
|
-
characterNavigation = characterNavigation,
|
|
8066
7881
|
}
|
|
8067
7882
|
]]></string>
|
|
8068
7883
|
</Properties>
|
|
@@ -8901,11 +8716,37 @@ local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
|
8901
8716
|
local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
8902
8717
|
local HttpService = _services.HttpService
|
|
8903
8718
|
local ServerStorage = _services.ServerStorage
|
|
8904
|
-
local
|
|
8719
|
+
local LEGACY_SETTING_KEY_PREFIX = "MCP_SERVER_URL_"
|
|
8720
|
+
local SETTING_KEY_PREFIX = "MCP_LAST_SUCCESSFUL_SERVER_URL_"
|
|
8721
|
+
local GLOBAL_SETTING_KEY = "MCP_LAST_SUCCESSFUL_SERVER_URL_GLOBAL_V1"
|
|
8905
8722
|
local pluginRef
|
|
8906
8723
|
local function init(p)
|
|
8907
8724
|
pluginRef = p
|
|
8908
8725
|
end
|
|
8726
|
+
local function normalizeServerUrl(serverUrl)
|
|
8727
|
+
local _condition = serverUrl
|
|
8728
|
+
if _condition == nil then
|
|
8729
|
+
_condition = ""
|
|
8730
|
+
end
|
|
8731
|
+
local normalized = (string.gsub((string.gsub(_condition, "^%s+", "")), "%s+$", ""))
|
|
8732
|
+
if normalized == "" then
|
|
8733
|
+
return ""
|
|
8734
|
+
end
|
|
8735
|
+
if (string.match(normalized, "^%a[%w+.-]*://")) == nil then
|
|
8736
|
+
normalized = `http://{normalized}`
|
|
8737
|
+
end
|
|
8738
|
+
while #normalized > 0 and string.sub(normalized, -1) == "/" and (string.match(normalized, "^%a[%w+.-]*://$")) == nil do
|
|
8739
|
+
normalized = string.sub(normalized, 1, -2)
|
|
8740
|
+
end
|
|
8741
|
+
return normalized
|
|
8742
|
+
end
|
|
8743
|
+
local function extractPort(serverUrl)
|
|
8744
|
+
local portStr = string.match(serverUrl, ":(%d+)$")
|
|
8745
|
+
if portStr == nil then
|
|
8746
|
+
return nil
|
|
8747
|
+
end
|
|
8748
|
+
return tonumber(portStr)
|
|
8749
|
+
end
|
|
8909
8750
|
local function addUnique(values, value)
|
|
8910
8751
|
local _values = values
|
|
8911
8752
|
local _value = value
|
|
@@ -8935,15 +8776,39 @@ end
|
|
|
8935
8776
|
local function settingKey(instanceId)
|
|
8936
8777
|
return SETTING_KEY_PREFIX .. instanceId
|
|
8937
8778
|
end
|
|
8779
|
+
local function legacySettingKey(instanceId)
|
|
8780
|
+
return LEGACY_SETTING_KEY_PREFIX .. instanceId
|
|
8781
|
+
end
|
|
8782
|
+
local function readSettingString(key)
|
|
8783
|
+
if not pluginRef then
|
|
8784
|
+
return nil
|
|
8785
|
+
end
|
|
8786
|
+
local ok, value = pcall(function()
|
|
8787
|
+
return pluginRef:GetSetting(key)
|
|
8788
|
+
end)
|
|
8789
|
+
if not ok or not (type(value) == "string") then
|
|
8790
|
+
return nil
|
|
8791
|
+
end
|
|
8792
|
+
local normalized = normalizeServerUrl(value)
|
|
8793
|
+
return if normalized ~= "" then normalized else nil
|
|
8794
|
+
end
|
|
8795
|
+
local function writeSettingString(key, serverUrl)
|
|
8796
|
+
if not pluginRef then
|
|
8797
|
+
return nil
|
|
8798
|
+
end
|
|
8799
|
+
pcall(function()
|
|
8800
|
+
return pluginRef:SetSetting(key, serverUrl)
|
|
8801
|
+
end)
|
|
8802
|
+
end
|
|
8938
8803
|
local function rememberServerUrl(serverUrl)
|
|
8939
|
-
|
|
8804
|
+
local normalized = normalizeServerUrl(serverUrl)
|
|
8805
|
+
if not pluginRef or normalized == "" then
|
|
8940
8806
|
return nil
|
|
8941
8807
|
end
|
|
8808
|
+
writeSettingString(GLOBAL_SETTING_KEY, normalized)
|
|
8942
8809
|
for _, instanceId in computeInstanceIds() do
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
return pluginRef:SetSetting(key, serverUrl)
|
|
8946
|
-
end)
|
|
8810
|
+
writeSettingString(settingKey(instanceId), normalized)
|
|
8811
|
+
writeSettingString(legacySettingKey(instanceId), normalized)
|
|
8947
8812
|
end
|
|
8948
8813
|
end
|
|
8949
8814
|
local function readServerUrl()
|
|
@@ -8951,18 +8816,27 @@ local function readServerUrl()
|
|
|
8951
8816
|
return nil
|
|
8952
8817
|
end
|
|
8953
8818
|
for _, instanceId in computeInstanceIds() do
|
|
8954
|
-
local
|
|
8955
|
-
|
|
8956
|
-
return
|
|
8957
|
-
end
|
|
8958
|
-
|
|
8959
|
-
|
|
8819
|
+
local remembered = readSettingString(settingKey(instanceId))
|
|
8820
|
+
if remembered ~= nil then
|
|
8821
|
+
return remembered
|
|
8822
|
+
end
|
|
8823
|
+
end
|
|
8824
|
+
local globalRemembered = readSettingString(GLOBAL_SETTING_KEY)
|
|
8825
|
+
if globalRemembered ~= nil then
|
|
8826
|
+
return globalRemembered
|
|
8827
|
+
end
|
|
8828
|
+
for _, instanceId in computeInstanceIds() do
|
|
8829
|
+
local legacyRemembered = readSettingString(legacySettingKey(instanceId))
|
|
8830
|
+
if legacyRemembered ~= nil then
|
|
8831
|
+
return legacyRemembered
|
|
8960
8832
|
end
|
|
8961
8833
|
end
|
|
8962
8834
|
return nil
|
|
8963
8835
|
end
|
|
8964
8836
|
return {
|
|
8965
8837
|
init = init,
|
|
8838
|
+
normalizeServerUrl = normalizeServerUrl,
|
|
8839
|
+
extractPort = extractPort,
|
|
8966
8840
|
rememberServerUrl = rememberServerUrl,
|
|
8967
8841
|
readServerUrl = readServerUrl,
|
|
8968
8842
|
}
|
|
@@ -8973,7 +8847,7 @@ return {
|
|
|
8973
8847
|
<Properties>
|
|
8974
8848
|
<string name="Name">State</string>
|
|
8975
8849
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
8976
|
-
local CURRENT_VERSION = "2.
|
|
8850
|
+
local CURRENT_VERSION = "2.18.0"
|
|
8977
8851
|
local PLUGIN_VARIANT = "main"
|
|
8978
8852
|
local MAX_CONNECTIONS = 5
|
|
8979
8853
|
local BASE_PORT = 58741
|
|
@@ -9372,6 +9246,7 @@ return {
|
|
|
9372
9246
|
local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
9373
9247
|
local TweenService = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services").TweenService
|
|
9374
9248
|
local State = TS.import(script, script.Parent, "State")
|
|
9249
|
+
local ServerUrlSettings = TS.import(script, script.Parent, "ServerUrlSettings")
|
|
9375
9250
|
local elements = nil
|
|
9376
9251
|
local pulseAnimation
|
|
9377
9252
|
local buttonHover = false
|
|
@@ -9810,7 +9685,7 @@ local function init(pluginRef)
|
|
|
9810
9685
|
urlInput.Size = UDim2.new(1, 0, 0, 26)
|
|
9811
9686
|
urlInput.BackgroundColor3 = C.bg
|
|
9812
9687
|
urlInput.BorderSizePixel = 0
|
|
9813
|
-
urlInput.Text =
|
|
9688
|
+
urlInput.Text = State.getActiveConnection().serverUrl
|
|
9814
9689
|
urlInput.TextColor3 = C.label
|
|
9815
9690
|
urlInput.TextSize = 11
|
|
9816
9691
|
urlInput.Font = Enum.Font.GothamMedium
|
|
@@ -9831,14 +9706,16 @@ local function init(pluginRef)
|
|
|
9831
9706
|
if not conn or conn.isActive then
|
|
9832
9707
|
return nil
|
|
9833
9708
|
end
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
|
|
9840
|
-
|
|
9841
|
-
|
|
9709
|
+
local normalizedUrl = ServerUrlSettings.normalizeServerUrl(urlInput.Text)
|
|
9710
|
+
if normalizedUrl == "" then
|
|
9711
|
+
urlInput.Text = conn.serverUrl
|
|
9712
|
+
return nil
|
|
9713
|
+
end
|
|
9714
|
+
conn.serverUrl = normalizedUrl
|
|
9715
|
+
urlInput.Text = normalizedUrl
|
|
9716
|
+
local port = ServerUrlSettings.extractPort(conn.serverUrl)
|
|
9717
|
+
if port ~= nil then
|
|
9718
|
+
conn.port = port
|
|
9842
9719
|
end
|
|
9843
9720
|
updateTabLabel(State.getActiveTabIndex())
|
|
9844
9721
|
end)
|
|
@@ -10141,6 +10018,32 @@ return {
|
|
|
10141
10018
|
<string name="Name">Utils</string>
|
|
10142
10019
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
10143
10020
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
10021
|
+
local LUAU_KEYWORDS = {
|
|
10022
|
+
["and"] = true,
|
|
10023
|
+
["break"] = true,
|
|
10024
|
+
["continue"] = true,
|
|
10025
|
+
["do"] = true,
|
|
10026
|
+
["else"] = true,
|
|
10027
|
+
["elseif"] = true,
|
|
10028
|
+
["end"] = true,
|
|
10029
|
+
["export"] = true,
|
|
10030
|
+
["false"] = true,
|
|
10031
|
+
["for"] = true,
|
|
10032
|
+
["function"] = true,
|
|
10033
|
+
["if"] = true,
|
|
10034
|
+
["in"] = true,
|
|
10035
|
+
["local"] = true,
|
|
10036
|
+
["nil"] = true,
|
|
10037
|
+
["not"] = true,
|
|
10038
|
+
["or"] = true,
|
|
10039
|
+
["repeat"] = true,
|
|
10040
|
+
["return"] = true,
|
|
10041
|
+
["then"] = true,
|
|
10042
|
+
["true"] = true,
|
|
10043
|
+
["type"] = true,
|
|
10044
|
+
["until"] = true,
|
|
10045
|
+
["while"] = true,
|
|
10046
|
+
}
|
|
10144
10047
|
local function safeCall(func, ...)
|
|
10145
10048
|
local args = { ... }
|
|
10146
10049
|
local success, result = pcall(func, unpack(args))
|
|
@@ -10151,6 +10054,194 @@ local function safeCall(func, ...)
|
|
|
10151
10054
|
return nil
|
|
10152
10055
|
end
|
|
10153
10056
|
end
|
|
10057
|
+
local function isSimplePathSegment(segment)
|
|
10058
|
+
local _condition = (string.match(segment, "^[%a_][%w_]*$")) ~= nil
|
|
10059
|
+
if _condition then
|
|
10060
|
+
local _segment = segment
|
|
10061
|
+
_condition = not (LUAU_KEYWORDS[_segment] ~= nil)
|
|
10062
|
+
end
|
|
10063
|
+
return _condition
|
|
10064
|
+
end
|
|
10065
|
+
local function quotePathSegment(segment)
|
|
10066
|
+
local escaped = (string.gsub(segment, "\\", "\\\\"))
|
|
10067
|
+
escaped = (string.gsub(escaped, "\n", "\\n"))
|
|
10068
|
+
escaped = (string.gsub(escaped, "\r", "\\r"))
|
|
10069
|
+
escaped = (string.gsub(escaped, "\t", "\\t"))
|
|
10070
|
+
escaped = (string.gsub(escaped, '"', '\\"'))
|
|
10071
|
+
return `"{escaped}"`
|
|
10072
|
+
end
|
|
10073
|
+
local function unescapePathSegment(segment)
|
|
10074
|
+
local chars = {}
|
|
10075
|
+
local i = 1
|
|
10076
|
+
while i <= #segment do
|
|
10077
|
+
local _segment = segment
|
|
10078
|
+
local _i = i
|
|
10079
|
+
local _i_1 = i
|
|
10080
|
+
local ch = string.sub(_segment, _i, _i_1)
|
|
10081
|
+
if ch == "\\" and i < #segment then
|
|
10082
|
+
local _segment_1 = segment
|
|
10083
|
+
local _arg0 = i + 1
|
|
10084
|
+
local _arg1 = i + 1
|
|
10085
|
+
local nextChar = string.sub(_segment_1, _arg0, _arg1)
|
|
10086
|
+
if nextChar == "n" then
|
|
10087
|
+
table.insert(chars, "\n")
|
|
10088
|
+
elseif nextChar == "r" then
|
|
10089
|
+
table.insert(chars, "\r")
|
|
10090
|
+
elseif nextChar == "t" then
|
|
10091
|
+
table.insert(chars, "\t")
|
|
10092
|
+
else
|
|
10093
|
+
table.insert(chars, nextChar)
|
|
10094
|
+
end
|
|
10095
|
+
i += 2
|
|
10096
|
+
else
|
|
10097
|
+
table.insert(chars, ch)
|
|
10098
|
+
i += 1
|
|
10099
|
+
end
|
|
10100
|
+
end
|
|
10101
|
+
return table.concat(chars, "")
|
|
10102
|
+
end
|
|
10103
|
+
local function isCanonicalBracketStart(path, index)
|
|
10104
|
+
local _path = path
|
|
10105
|
+
local _arg0 = index + 1
|
|
10106
|
+
local _arg1 = index + 1
|
|
10107
|
+
local quote = string.sub(_path, _arg0, _arg1)
|
|
10108
|
+
local _condition = (quote == '"' or quote == "'")
|
|
10109
|
+
if _condition then
|
|
10110
|
+
local _path_1 = path
|
|
10111
|
+
local _arg0_1 = index - 1
|
|
10112
|
+
local _arg1_1 = index - 1
|
|
10113
|
+
_condition = string.sub(_path_1, _arg0_1, _arg1_1) ~= "."
|
|
10114
|
+
end
|
|
10115
|
+
return _condition
|
|
10116
|
+
end
|
|
10117
|
+
local function parseInstancePath(path)
|
|
10118
|
+
local i = 1
|
|
10119
|
+
local len = #path
|
|
10120
|
+
local parts = {}
|
|
10121
|
+
local current = ""
|
|
10122
|
+
if path == "" or path == "game" then
|
|
10123
|
+
return parts
|
|
10124
|
+
end
|
|
10125
|
+
if string.sub(path, 1, 5) == "game." then
|
|
10126
|
+
i = 6
|
|
10127
|
+
elseif string.sub(path, 1, 5) == "game[" then
|
|
10128
|
+
i = 5
|
|
10129
|
+
end
|
|
10130
|
+
while i <= len do
|
|
10131
|
+
local _path = path
|
|
10132
|
+
local _i = i
|
|
10133
|
+
local _i_1 = i
|
|
10134
|
+
local ch = string.sub(_path, _i, _i_1)
|
|
10135
|
+
if ch == "." then
|
|
10136
|
+
if current ~= "" then
|
|
10137
|
+
local _current = current
|
|
10138
|
+
table.insert(parts, _current)
|
|
10139
|
+
current = ""
|
|
10140
|
+
i += 1
|
|
10141
|
+
else
|
|
10142
|
+
local _condition = i > 1
|
|
10143
|
+
if _condition then
|
|
10144
|
+
local _path_1 = path
|
|
10145
|
+
local _arg0 = i - 1
|
|
10146
|
+
local _arg1 = i - 1
|
|
10147
|
+
_condition = string.sub(_path_1, _arg0, _arg1) == "."
|
|
10148
|
+
if _condition then
|
|
10149
|
+
_condition = i < len
|
|
10150
|
+
if _condition then
|
|
10151
|
+
local _path_2 = path
|
|
10152
|
+
local _arg0_1 = i + 1
|
|
10153
|
+
local _arg1_1 = i + 1
|
|
10154
|
+
_condition = string.sub(_path_2, _arg0_1, _arg1_1) ~= "["
|
|
10155
|
+
end
|
|
10156
|
+
end
|
|
10157
|
+
end
|
|
10158
|
+
if _condition then
|
|
10159
|
+
-- Back-compat for previously emitted paths such as
|
|
10160
|
+
-- game.ServerScriptService..dir.ReproScript, where ".dir"
|
|
10161
|
+
-- was an actual instance name.
|
|
10162
|
+
current = "."
|
|
10163
|
+
i += 1
|
|
10164
|
+
else
|
|
10165
|
+
i += 1
|
|
10166
|
+
end
|
|
10167
|
+
end
|
|
10168
|
+
elseif ch == "[" and i < len and isCanonicalBracketStart(path, i) then
|
|
10169
|
+
if current ~= "" then
|
|
10170
|
+
local _current = current
|
|
10171
|
+
table.insert(parts, _current)
|
|
10172
|
+
current = ""
|
|
10173
|
+
end
|
|
10174
|
+
local _path_1 = path
|
|
10175
|
+
local _arg0 = i + 1
|
|
10176
|
+
local _arg1 = i + 1
|
|
10177
|
+
local quote = string.sub(_path_1, _arg0, _arg1)
|
|
10178
|
+
if quote ~= '"' and quote ~= "'" then
|
|
10179
|
+
return nil
|
|
10180
|
+
end
|
|
10181
|
+
local j = i + 2
|
|
10182
|
+
local raw = ""
|
|
10183
|
+
while j <= len do
|
|
10184
|
+
local _path_2 = path
|
|
10185
|
+
local _j = j
|
|
10186
|
+
local _j_1 = j
|
|
10187
|
+
local c = string.sub(_path_2, _j, _j_1)
|
|
10188
|
+
if c == "\\" then
|
|
10189
|
+
if j >= len then
|
|
10190
|
+
return nil
|
|
10191
|
+
end
|
|
10192
|
+
local _path_3 = path
|
|
10193
|
+
local _arg0_1 = j + 1
|
|
10194
|
+
local _arg1_1 = j + 1
|
|
10195
|
+
raw ..= c .. string.sub(_path_3, _arg0_1, _arg1_1)
|
|
10196
|
+
j += 2
|
|
10197
|
+
elseif c == quote then
|
|
10198
|
+
break
|
|
10199
|
+
else
|
|
10200
|
+
raw ..= c
|
|
10201
|
+
j += 1
|
|
10202
|
+
end
|
|
10203
|
+
end
|
|
10204
|
+
local _condition = j > len
|
|
10205
|
+
if not _condition then
|
|
10206
|
+
local _path_2 = path
|
|
10207
|
+
local _j = j
|
|
10208
|
+
local _j_1 = j
|
|
10209
|
+
_condition = string.sub(_path_2, _j, _j_1) ~= quote
|
|
10210
|
+
if not _condition then
|
|
10211
|
+
local _path_3 = path
|
|
10212
|
+
local _arg0_1 = j + 1
|
|
10213
|
+
local _arg1_1 = j + 1
|
|
10214
|
+
_condition = string.sub(_path_3, _arg0_1, _arg1_1) ~= "]"
|
|
10215
|
+
end
|
|
10216
|
+
end
|
|
10217
|
+
if _condition then
|
|
10218
|
+
return nil
|
|
10219
|
+
end
|
|
10220
|
+
local _arg0_1 = unescapePathSegment(raw)
|
|
10221
|
+
table.insert(parts, _arg0_1)
|
|
10222
|
+
i = j + 2
|
|
10223
|
+
else
|
|
10224
|
+
current ..= ch
|
|
10225
|
+
i += 1
|
|
10226
|
+
end
|
|
10227
|
+
end
|
|
10228
|
+
if current ~= "" then
|
|
10229
|
+
local _current = current
|
|
10230
|
+
table.insert(parts, _current)
|
|
10231
|
+
end
|
|
10232
|
+
return parts
|
|
10233
|
+
end
|
|
10234
|
+
local function getRootSegment(instance)
|
|
10235
|
+
if instance.Parent == game then
|
|
10236
|
+
local ok, service = pcall(function()
|
|
10237
|
+
return game:GetService(instance.ClassName)
|
|
10238
|
+
end)
|
|
10239
|
+
if ok and service == instance then
|
|
10240
|
+
return instance.ClassName
|
|
10241
|
+
end
|
|
10242
|
+
end
|
|
10243
|
+
return instance.Name
|
|
10244
|
+
end
|
|
10154
10245
|
local function getInstancePath(instance)
|
|
10155
10246
|
if not instance or instance == game then
|
|
10156
10247
|
return "game"
|
|
@@ -10158,23 +10249,40 @@ local function getInstancePath(instance)
|
|
|
10158
10249
|
local pathParts = {}
|
|
10159
10250
|
local current = instance
|
|
10160
10251
|
while current and current ~= game do
|
|
10161
|
-
local
|
|
10162
|
-
table.insert(pathParts, 1,
|
|
10252
|
+
local _arg0 = getRootSegment(current)
|
|
10253
|
+
table.insert(pathParts, 1, _arg0)
|
|
10163
10254
|
current = current.Parent
|
|
10164
10255
|
end
|
|
10165
|
-
|
|
10256
|
+
local path = "game"
|
|
10257
|
+
for _, part in pathParts do
|
|
10258
|
+
if isSimplePathSegment(part) then
|
|
10259
|
+
path ..= `.{part}`
|
|
10260
|
+
else
|
|
10261
|
+
path ..= `[{quotePathSegment(part)}]`
|
|
10262
|
+
end
|
|
10263
|
+
end
|
|
10264
|
+
return path
|
|
10265
|
+
end
|
|
10266
|
+
local function getRootInstance(segment)
|
|
10267
|
+
local ok, service = pcall(function()
|
|
10268
|
+
return game:GetService(segment)
|
|
10269
|
+
end)
|
|
10270
|
+
if ok and service then
|
|
10271
|
+
return service
|
|
10272
|
+
end
|
|
10273
|
+
return game:FindFirstChild(segment)
|
|
10166
10274
|
end
|
|
10167
10275
|
local function getInstanceByPath(path)
|
|
10168
|
-
|
|
10169
|
-
|
|
10276
|
+
local parts = parseInstancePath(path)
|
|
10277
|
+
if parts == nil then
|
|
10278
|
+
return nil
|
|
10170
10279
|
end
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
for part in string.gmatch(cleaned, "[^%.]+") do
|
|
10174
|
-
table.insert(parts, part)
|
|
10280
|
+
if #parts == 0 then
|
|
10281
|
+
return game
|
|
10175
10282
|
end
|
|
10176
|
-
local current =
|
|
10177
|
-
for
|
|
10283
|
+
local current = getRootInstance(parts[1])
|
|
10284
|
+
for i = 1, #parts - 1 do
|
|
10285
|
+
local part = parts[i + 1]
|
|
10178
10286
|
if not current then
|
|
10179
10287
|
return nil
|
|
10180
10288
|
end
|