@chrrxs/robloxstudio-mcp-inspector 2.17.0 → 2.17.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.
@@ -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 = inheritedServerUrl
99
- local portStr = string.match(conn.serverUrl, ":(%d+)$")
100
- if portStr ~= 0 and portStr == portStr and portStr ~= "" and portStr then
101
- local _condition_1 = tonumber(portStr)
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(inheritedServerUrl)
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
- conn.serverUrl = ui.urlInput.Text
1176
- local portStr = string.match(conn.serverUrl, ":(%d+)$")
1177
- if portStr ~= 0 and portStr == portStr and portStr ~= "" and portStr then
1178
- local _condition_1 = tonumber(portStr)
1179
- if _condition_1 == nil then
1180
- _condition_1 = conn.port
1181
- end
1182
- conn.port = _condition_1
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.17.0" is replaced with the package version at package time
1438
+ -- "2.17.1" 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.17.0`
1440
+ return `{tostring(h)}-2.17.1`
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 SETTING_KEY_PREFIX = "MCP_SERVER_URL_"
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
- if not pluginRef or serverUrl == "" then
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
- local key = settingKey(instanceId)
8944
- pcall(function()
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 key = settingKey(instanceId)
8955
- local ok, value = pcall(function()
8956
- return pluginRef:GetSetting(key)
8957
- end)
8958
- if ok and type(value) == "string" and value ~= "" then
8959
- return value
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.17.0"
8850
+ local CURRENT_VERSION = "2.17.1"
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 = "http://localhost:58741"
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
- conn.serverUrl = urlInput.Text
9835
- local portStr = string.match(conn.serverUrl, ":(%d+)$")
9836
- if portStr ~= 0 and portStr == portStr and portStr ~= "" and portStr then
9837
- local _condition = tonumber(portStr)
9838
- if _condition == nil then
9839
- _condition = conn.port
9840
- end
9841
- conn.port = _condition
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 _name = current.Name
10162
- table.insert(pathParts, 1, _name)
10252
+ local _arg0 = getRootSegment(current)
10253
+ table.insert(pathParts, 1, _arg0)
10163
10254
  current = current.Parent
10164
10255
  end
10165
- return `game.{table.concat(pathParts, ".")}`
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
- if path == "game" or path == "" then
10169
- return game
10276
+ local parts = parseInstancePath(path)
10277
+ if parts == nil then
10278
+ return nil
10170
10279
  end
10171
- local cleaned = (string.gsub(path, "^game%.", ""))
10172
- local parts = {}
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 = game
10177
- for _, part in parts do
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