@chrrxs/robloxstudio-mcp-inspector 3.2.0 → 3.3.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 +12703 -12286
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +1260 -231
|
@@ -64,8 +64,8 @@ applyRememberedServerUrl()
|
|
|
64
64
|
-- down their VM. Close the persistent WebStreamClient while BindToClose still
|
|
65
65
|
-- gives this server peer a live execution context; otherwise each play cycle
|
|
66
66
|
-- can retain one native WebSocket until Studio itself is restarted. The
|
|
67
|
-
-- transport
|
|
68
|
-
--
|
|
67
|
+
-- transport unregisters the server peer on that socket and waits at most two
|
|
68
|
+
-- seconds for the server to close it, so no HTTP request can hold up shutdown.
|
|
69
69
|
if startupRole == "server" then
|
|
70
70
|
game:BindToClose(StudioWebSocket.suspendForShutdown)
|
|
71
71
|
end
|
|
@@ -104,8 +104,20 @@ plugin.Unloading:Connect(function()
|
|
|
104
104
|
Communication.deactivateAll()
|
|
105
105
|
end)
|
|
106
106
|
UI.updateUIState()
|
|
107
|
-
|
|
107
|
+
-- Every DataModel shares Studio's five plugin HTTP request slots, and a
|
|
108
|
+
-- request has no time limit. Check for updates once, from the edit DataModel,
|
|
109
|
+
-- instead of again in every play server and client.
|
|
110
|
+
if startupRole == "edit" then
|
|
111
|
+
Communication.checkForUpdates()
|
|
112
|
+
end
|
|
108
113
|
task.delay(TOOLBAR_REGISTRATION_DELAY_SECONDS, registerToolbarButton)
|
|
114
|
+
--* Runs one startup step so that its failure cannot skip the steps after it.
|
|
115
|
+
local function runStartupStep(description, step)
|
|
116
|
+
local ok, err = pcall(step)
|
|
117
|
+
if not ok then
|
|
118
|
+
warn(`[robloxstudio-mcp] {description} failed: {err}`)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
109
121
|
-- Auto-activate per Peer. Runtime plugin VMs can load before their first
|
|
110
122
|
-- Heartbeat; task.delay() would then wait behind the very multiplayer startup
|
|
111
123
|
-- that needs this Peer to register. Start runtime initialization immediately,
|
|
@@ -123,10 +135,13 @@ local function autoActivatePeer()
|
|
|
123
135
|
if role == "server" then
|
|
124
136
|
-- Yields until this playtest server knows which edit session (or
|
|
125
137
|
-- multiplayer group) it belongs to, so it registers under that identity.
|
|
126
|
-
|
|
138
|
+
-- On failure the server still registers, under its own identity.
|
|
139
|
+
runStartupStep("Playtest topology resolution", function()
|
|
140
|
+
return PluginSession.resolveRuntimeTopology()
|
|
141
|
+
end)
|
|
127
142
|
end
|
|
128
143
|
if role == "edit" or role == "server" then
|
|
129
|
-
|
|
144
|
+
runStartupStep(`Automatic {role} Peer activation`, function()
|
|
130
145
|
local conn = State.getActiveConnection()
|
|
131
146
|
if not conn.isActive then
|
|
132
147
|
if role == "server" then
|
|
@@ -155,19 +170,20 @@ local function autoActivatePeer()
|
|
|
155
170
|
Communication.activatePlugin()
|
|
156
171
|
end
|
|
157
172
|
end)
|
|
158
|
-
if not activationOk then
|
|
159
|
-
warn(`[robloxstudio-mcp] Automatic {role} Peer activation failed: {activationError}`)
|
|
160
|
-
end
|
|
161
173
|
end
|
|
162
174
|
if role == "server" then
|
|
163
|
-
|
|
175
|
+
runStartupStep("Client broker setup", function()
|
|
176
|
+
return ClientBroker.setupServerBroker()
|
|
177
|
+
end)
|
|
164
178
|
-- The play-server DM is the only one where StudioTestService:EndTest is
|
|
165
179
|
-- legal, so the stop-play monitor lives here. It consumes tokenized
|
|
166
180
|
-- stop requests from plugin settings and acknowledges EndTest results.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
181
|
+
runStartupStep("Stop-play monitor startup", function()
|
|
182
|
+
return StopPlayMonitor.startMonitor({
|
|
183
|
+
beforeEndTest = StudioWebSocket.suspendForShutdown,
|
|
184
|
+
afterEndTestFailure = StudioWebSocket.resumeAfterShutdownFailure,
|
|
185
|
+
})
|
|
186
|
+
end)
|
|
171
187
|
elseif role == "client" then
|
|
172
188
|
ClientBroker.setupClientBroker()
|
|
173
189
|
end
|
|
@@ -1455,6 +1471,7 @@ local ClientBroker = TS.import(script, script.Parent, "ClientBroker")
|
|
|
1455
1471
|
local ServerUrlSettings = TS.import(script, script.Parent, "ServerUrlSettings")
|
|
1456
1472
|
local PluginSession = TS.import(script, script.Parent, "PluginSession")
|
|
1457
1473
|
local StudioWebSocket = TS.import(script, script.Parent, "StudioWebSocket")
|
|
1474
|
+
local PathGuard = TS.import(script, script.Parent, "PathGuard")
|
|
1458
1475
|
local assignedRole
|
|
1459
1476
|
local lastReadyPlaceKey
|
|
1460
1477
|
local initialRole = PluginSession.getRole()
|
|
@@ -1507,12 +1524,17 @@ local routeMap = {
|
|
|
1507
1524
|
["/api/get-memory-breakdown"] = MemoryHandlers.getMemoryBreakdown,
|
|
1508
1525
|
["/api/get-scene-analysis"] = SceneAnalysisHandlers.getSceneAnalysis,
|
|
1509
1526
|
}
|
|
1527
|
+
local pathGuard = PathGuard.createPathGuard(function(path)
|
|
1528
|
+
return Utils.resolveInstancePath(path).ambiguity
|
|
1529
|
+
end)
|
|
1510
1530
|
local function processRequest(request, context)
|
|
1511
1531
|
local endpoint = request.endpoint
|
|
1512
|
-
local data = request.data or {}
|
|
1532
|
+
local data = (request.data or {})
|
|
1513
1533
|
local handler = routeMap[endpoint]
|
|
1514
1534
|
if handler then
|
|
1515
|
-
return
|
|
1535
|
+
return pathGuard.run(endpoint, data, function()
|
|
1536
|
+
return handler(data, context)
|
|
1537
|
+
end)
|
|
1516
1538
|
else
|
|
1517
1539
|
return {
|
|
1518
1540
|
error = `Unknown endpoint: {endpoint}`,
|
|
@@ -1959,9 +1981,9 @@ local function computeBridgeStamp()
|
|
|
1959
1981
|
for i = 1, #combined do
|
|
1960
1982
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1961
1983
|
end
|
|
1962
|
-
-- "3.
|
|
1984
|
+
-- "3.3.0" is replaced with the package version at package time
|
|
1963
1985
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1964
|
-
return `{tostring(h)}-3.
|
|
1986
|
+
return `{tostring(h)}-3.3.0`
|
|
1965
1987
|
end
|
|
1966
1988
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1967
1989
|
local function setSource(scriptInst, source)
|
|
@@ -4197,15 +4219,20 @@ local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
|
4197
4219
|
-- capability and drives the REAL input pipeline: SendKey feeds
|
|
4198
4220
|
-- UserInputService.InputBegan/Ended and the control modules (so WASD walks the
|
|
4199
4221
|
-- character at full WalkSpeed with controls intact, no Humanoid hijack),
|
|
4200
|
-
-- SendMouseButton feeds UIS and activates GUI buttons
|
|
4201
|
-
--
|
|
4222
|
+
-- SendMouseButton feeds UIS and activates GUI buttons, SendMousePosition moves
|
|
4223
|
+
-- the cursor (hover, MouseEnter, InputChanged), SendPointerAction scrolls
|
|
4224
|
+
-- under a position, and SendTextInput types into the focused TextBox. Every method throws when the
|
|
4225
|
+
-- input would hit interactive CoreGui.
|
|
4226
|
+
--
|
|
4227
|
+
-- Verified live on a play client: move and scroll reach game scripts with
|
|
4228
|
+
-- the device simulator stopped. Under touch device emulation the client has
|
|
4229
|
+
-- no mouse: clicks arrive as Touch, scroll still works, but moves never reach
|
|
4230
|
+
-- the game, so we reject move there instead of returning a false success.
|
|
4202
4231
|
--
|
|
4203
|
-
--
|
|
4204
|
-
--
|
|
4205
|
-
--
|
|
4206
|
-
--
|
|
4207
|
-
-- There is NO SendMouseMove / SendMouseWheel / SendKeyEvent — so "move" and
|
|
4208
|
-
-- "scroll" mouse actions are not supported.
|
|
4232
|
+
-- SendMouseDelta is deliberately NOT exposed. It only works while the cursor
|
|
4233
|
+
-- is locked (MouseBehavior.LockCenter), and on desktop Studio holds that lock
|
|
4234
|
+
-- by pinning and hiding the user's real OS cursor — an unacceptable takeover
|
|
4235
|
+
-- of the host's input.
|
|
4209
4236
|
--
|
|
4210
4237
|
-- Coordinate space: SendMouseButton coordinates are viewport pixels matching
|
|
4211
4238
|
-- what capture_screenshot returns (window space, origin at the top-left of the
|
|
@@ -4238,6 +4265,7 @@ local MOUSE_TYPE_MAP = {
|
|
|
4238
4265
|
Right = Enum.UserInputType.MouseButton2,
|
|
4239
4266
|
Middle = Enum.UserInputType.MouseButton3,
|
|
4240
4267
|
}
|
|
4268
|
+
local NO_MOUSE_ERROR = "This client has no mouse (touch device emulation is active), so cursor movement never reaches the game. " .. "Stop it with set_device_simulator stopSimulation=true, then retry."
|
|
4241
4269
|
local function simulateMouseInput(requestData)
|
|
4242
4270
|
local action = requestData.action
|
|
4243
4271
|
local x = requestData.x
|
|
@@ -4247,6 +4275,7 @@ local function simulateMouseInput(requestData)
|
|
|
4247
4275
|
_condition = "Left"
|
|
4248
4276
|
end
|
|
4249
4277
|
local button = _condition
|
|
4278
|
+
local wheel = requestData.wheel
|
|
4250
4279
|
if not (action ~= "" and action) then
|
|
4251
4280
|
return {
|
|
4252
4281
|
error = "action is required",
|
|
@@ -4257,6 +4286,11 @@ local function simulateMouseInput(requestData)
|
|
|
4257
4286
|
error = "x and y are required",
|
|
4258
4287
|
}
|
|
4259
4288
|
end
|
|
4289
|
+
if action == "scroll" and (not (type(wheel) == "number") or wheel == 0) then
|
|
4290
|
+
return {
|
|
4291
|
+
error = "wheel must be a non-zero number for scroll",
|
|
4292
|
+
}
|
|
4293
|
+
end
|
|
4260
4294
|
-- Input is silently dropped by the engine when the window isn't rendering
|
|
4261
4295
|
-- (e.g. minimized). Surface that instead of returning a false success.
|
|
4262
4296
|
local notRendering = RenderMonitor.notRenderingReason()
|
|
@@ -4265,6 +4299,11 @@ local function simulateMouseInput(requestData)
|
|
|
4265
4299
|
error = notRendering,
|
|
4266
4300
|
}
|
|
4267
4301
|
end
|
|
4302
|
+
if action == "move" and not UserInputService.MouseEnabled then
|
|
4303
|
+
return {
|
|
4304
|
+
error = NO_MOUSE_ERROR,
|
|
4305
|
+
}
|
|
4306
|
+
end
|
|
4268
4307
|
local vi = getVI()
|
|
4269
4308
|
if not vi then
|
|
4270
4309
|
return {
|
|
@@ -4282,18 +4321,29 @@ local function simulateMouseInput(requestData)
|
|
|
4282
4321
|
vi:SendMouseButton(pos, inputType, true)
|
|
4283
4322
|
elseif action == "mouseUp" then
|
|
4284
4323
|
vi:SendMouseButton(pos, inputType, false)
|
|
4324
|
+
elseif action == "move" then
|
|
4325
|
+
vi:SendMousePosition(pos)
|
|
4326
|
+
elseif action == "scroll" then
|
|
4327
|
+
vi:SendPointerAction(pos, {
|
|
4328
|
+
Wheel = wheel,
|
|
4329
|
+
})
|
|
4285
4330
|
else
|
|
4286
|
-
error(`Unsupported action "{action}".
|
|
4331
|
+
error(`Unsupported action "{action}". Use click, mouseDown, mouseUp, move, or scroll.`)
|
|
4287
4332
|
end
|
|
4288
4333
|
end)
|
|
4289
4334
|
if success then
|
|
4290
|
-
|
|
4335
|
+
local result = {
|
|
4291
4336
|
success = true,
|
|
4292
4337
|
action = action,
|
|
4293
4338
|
x = x,
|
|
4294
4339
|
y = y,
|
|
4295
|
-
button = button,
|
|
4296
4340
|
}
|
|
4341
|
+
if action == "scroll" then
|
|
4342
|
+
result.wheel = wheel
|
|
4343
|
+
elseif action ~= "move" then
|
|
4344
|
+
result.button = button
|
|
4345
|
+
end
|
|
4346
|
+
return result
|
|
4297
4347
|
end
|
|
4298
4348
|
return {
|
|
4299
4349
|
error = `Failed to simulate mouse input: {err}`,
|
|
@@ -4484,6 +4534,7 @@ local Selection = game:GetService("Selection")
|
|
|
4484
4534
|
local _binding = Utils
|
|
4485
4535
|
local getInstancePath = _binding.getInstancePath
|
|
4486
4536
|
local getInstanceByPath = _binding.getInstanceByPath
|
|
4537
|
+
local createPathAmbiguityChecker = _binding.createPathAmbiguityChecker
|
|
4487
4538
|
local function serializeValue(value)
|
|
4488
4539
|
local _value = value
|
|
4489
4540
|
local vType = typeof(_value)
|
|
@@ -4585,15 +4636,20 @@ local function getSelection(_requestData)
|
|
|
4585
4636
|
message = "No objects selected",
|
|
4586
4637
|
}
|
|
4587
4638
|
end
|
|
4639
|
+
local isAmbiguous = createPathAmbiguityChecker()
|
|
4588
4640
|
-- ▼ ReadonlyArray.map ▼
|
|
4589
4641
|
local _newValue = table.create(#selection)
|
|
4590
4642
|
local _callback = function(instance)
|
|
4591
|
-
|
|
4643
|
+
local entry = {
|
|
4592
4644
|
name = instance.Name,
|
|
4593
4645
|
className = instance.ClassName,
|
|
4594
4646
|
path = getInstancePath(instance),
|
|
4595
4647
|
parent = if instance.Parent then getInstancePath(instance.Parent) else nil,
|
|
4596
4648
|
}
|
|
4649
|
+
if isAmbiguous(instance) then
|
|
4650
|
+
entry.ambiguous = true
|
|
4651
|
+
end
|
|
4652
|
+
return entry
|
|
4597
4653
|
end
|
|
4598
4654
|
for _k, _v in selection do
|
|
4599
4655
|
_newValue[_k] = _callback(_v, _k - 1, selection)
|
|
@@ -6732,9 +6788,11 @@ local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
|
6732
6788
|
local Utils = TS.import(script, script.Parent.Parent, "Utils")
|
|
6733
6789
|
local CooperativeJobRunner = TS.import(script, script.Parent.Parent, "CooperativeJobRunner")
|
|
6734
6790
|
local ScriptSearch = TS.import(script, script.Parent.Parent, "ScriptSearch")
|
|
6791
|
+
local InstanceSearch = TS.import(script, script.Parent.Parent, "InstanceSearch")
|
|
6735
6792
|
local _binding = Utils
|
|
6736
6793
|
local getInstancePath = _binding.getInstancePath
|
|
6737
6794
|
local getInstanceByPath = _binding.getInstanceByPath
|
|
6795
|
+
local createPathAmbiguityChecker = _binding.createPathAmbiguityChecker
|
|
6738
6796
|
local readScriptSource = _binding.readScriptSource
|
|
6739
6797
|
local function getFileTree(requestData)
|
|
6740
6798
|
local _condition = (requestData.path)
|
|
@@ -6860,62 +6918,42 @@ local function getPlaceInfo(_requestData)
|
|
|
6860
6918
|
},
|
|
6861
6919
|
}
|
|
6862
6920
|
end
|
|
6863
|
-
local
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
return
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
local
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
end
|
|
6898
|
-
end
|
|
6899
|
-
end
|
|
6900
|
-
if match then
|
|
6901
|
-
local _arg0 = {
|
|
6902
|
-
name = instance.Name,
|
|
6903
|
-
className = instance.ClassName,
|
|
6904
|
-
path = getInstancePath(instance),
|
|
6905
|
-
}
|
|
6906
|
-
table.insert(results, _arg0)
|
|
6907
|
-
end
|
|
6908
|
-
for _, child in instance:GetChildren() do
|
|
6909
|
-
searchRecursive(child)
|
|
6910
|
-
end
|
|
6921
|
+
local instanceSearch = InstanceSearch.createInstanceSearch({
|
|
6922
|
+
resolveRoot = getInstanceByPath,
|
|
6923
|
+
getChildren = function(instance)
|
|
6924
|
+
return instance:GetChildren()
|
|
6925
|
+
end,
|
|
6926
|
+
getName = function(instance)
|
|
6927
|
+
return instance.Name
|
|
6928
|
+
end,
|
|
6929
|
+
getClassName = function(instance)
|
|
6930
|
+
return instance.ClassName
|
|
6931
|
+
end,
|
|
6932
|
+
isA = function(instance, className)
|
|
6933
|
+
return instance:IsA(className)
|
|
6934
|
+
end,
|
|
6935
|
+
readPropertyText = function(instance, propertyName)
|
|
6936
|
+
local success, value = pcall(function()
|
|
6937
|
+
return tostring(instance[propertyName])
|
|
6938
|
+
end)
|
|
6939
|
+
return if success then value else nil
|
|
6940
|
+
end,
|
|
6941
|
+
getPath = getInstancePath,
|
|
6942
|
+
createAmbiguityChecker = createPathAmbiguityChecker,
|
|
6943
|
+
})
|
|
6944
|
+
local function searchObjects(requestData, execution)
|
|
6945
|
+
-- Keyed per request: concurrent searches are independent reads, so the
|
|
6946
|
+
-- runner is used only for yielding, deadlines and cancellation.
|
|
6947
|
+
local result = CooperativeJobRunner.runExclusive(`instance-search:{execution.requestId}`, execution, function(control)
|
|
6948
|
+
return instanceSearch.search(requestData, control)
|
|
6949
|
+
end)
|
|
6950
|
+
if result.error == "deadline_exceeded" then
|
|
6951
|
+
local _object = table.clone(result)
|
|
6952
|
+
setmetatable(_object, nil)
|
|
6953
|
+
_object.message = "search_objects exceeded its bridge deadline before the scan completed. Narrow it with path or className."
|
|
6954
|
+
return _object
|
|
6911
6955
|
end
|
|
6912
|
-
|
|
6913
|
-
return {
|
|
6914
|
-
results = results,
|
|
6915
|
-
query = query,
|
|
6916
|
-
searchType = searchType,
|
|
6917
|
-
count = #results,
|
|
6918
|
-
}
|
|
6956
|
+
return result
|
|
6919
6957
|
end
|
|
6920
6958
|
local function getInstanceProperties(requestData)
|
|
6921
6959
|
local instancePath = requestData.instancePath
|
|
@@ -7183,9 +7221,10 @@ local function getProjectStructure(requestData)
|
|
|
7183
7221
|
error = `Path not found: {startPath}`,
|
|
7184
7222
|
}
|
|
7185
7223
|
end
|
|
7224
|
+
local isAmbiguous = createPathAmbiguityChecker()
|
|
7186
7225
|
local function getStructure(instance, depth)
|
|
7187
7226
|
if depth > maxDepth then
|
|
7188
|
-
|
|
7227
|
+
local truncatedNode = {
|
|
7189
7228
|
name = instance.Name,
|
|
7190
7229
|
className = instance.ClassName,
|
|
7191
7230
|
path = getInstancePath(instance),
|
|
@@ -7193,12 +7232,19 @@ local function getProjectStructure(requestData)
|
|
|
7193
7232
|
hasMore = true,
|
|
7194
7233
|
note = "Max depth reached - use this path to explore further",
|
|
7195
7234
|
}
|
|
7235
|
+
if isAmbiguous(instance) then
|
|
7236
|
+
truncatedNode.ambiguous = true
|
|
7237
|
+
end
|
|
7238
|
+
return truncatedNode
|
|
7196
7239
|
end
|
|
7197
7240
|
local node = {
|
|
7198
7241
|
name = instance.Name,
|
|
7199
7242
|
className = instance.ClassName,
|
|
7200
7243
|
path = getInstancePath(instance),
|
|
7201
7244
|
}
|
|
7245
|
+
if isAmbiguous(instance) then
|
|
7246
|
+
node.ambiguous = true
|
|
7247
|
+
end
|
|
7202
7248
|
if instance:IsA("LuaSourceContainer") then
|
|
7203
7249
|
node.hasSource = true
|
|
7204
7250
|
node.scriptType = instance.ClassName
|
|
@@ -7328,6 +7374,9 @@ local scriptSearch = ScriptSearch.createScriptSearch({
|
|
|
7328
7374
|
resolveRoot = function(self, path)
|
|
7329
7375
|
return getInstanceByPath(path)
|
|
7330
7376
|
end,
|
|
7377
|
+
createAmbiguityChecker = function(self)
|
|
7378
|
+
return createPathAmbiguityChecker()
|
|
7379
|
+
end,
|
|
7331
7380
|
getChildren = function(self, instance)
|
|
7332
7381
|
return instance:GetChildren()
|
|
7333
7382
|
end,
|
|
@@ -7635,6 +7684,9 @@ return {
|
|
|
7635
7684
|
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
7636
7685
|
local Utils = TS.import(script, script.Parent.Parent, "Utils")
|
|
7637
7686
|
local Recording = TS.import(script, script.Parent.Parent, "Recording")
|
|
7687
|
+
local _ScriptEdits = TS.import(script, script.Parent.Parent, "ScriptEdits")
|
|
7688
|
+
local planScriptEdits = _ScriptEdits.planScriptEdits
|
|
7689
|
+
local sourceRevision = _ScriptEdits.sourceRevision
|
|
7638
7690
|
local _binding = Utils
|
|
7639
7691
|
local getInstancePath = _binding.getInstancePath
|
|
7640
7692
|
local getInstanceByPath = _binding.getInstanceByPath
|
|
@@ -7750,6 +7802,7 @@ local function getScriptSource(requestData)
|
|
|
7750
7802
|
numberedSource = numberLines(selectedLines, returnedStartLine),
|
|
7751
7803
|
sourceLength = #fullSource,
|
|
7752
7804
|
lineCount = totalLineCount,
|
|
7805
|
+
revision = sourceRevision(fullSource),
|
|
7753
7806
|
startLine = returnedStartLine,
|
|
7754
7807
|
endLine = returnedEndLine,
|
|
7755
7808
|
isPartial = explicitRange,
|
|
@@ -7821,14 +7874,74 @@ local function setScriptSource(requestData)
|
|
|
7821
7874
|
error = `Failed to set script source: {applyResult.error}`,
|
|
7822
7875
|
}
|
|
7823
7876
|
end
|
|
7877
|
+
-- Accepts the batched `edits` array or the single top-level old_string form.
|
|
7878
|
+
local function readScriptEdits(requestData)
|
|
7879
|
+
local rawEdits = requestData.edits
|
|
7880
|
+
if rawEdits == nil then
|
|
7881
|
+
local oldString = requestData.old_string
|
|
7882
|
+
local newString = requestData.new_string
|
|
7883
|
+
local startLine = requestData.startLine
|
|
7884
|
+
if not (type(oldString) == "string") or not (type(newString) == "string") then
|
|
7885
|
+
return "Instance path, old_string, and new_string are required"
|
|
7886
|
+
end
|
|
7887
|
+
if startLine ~= nil and not (type(startLine) == "number") then
|
|
7888
|
+
return "startLine must be a number"
|
|
7889
|
+
end
|
|
7890
|
+
return { {
|
|
7891
|
+
oldString = oldString,
|
|
7892
|
+
newString = newString,
|
|
7893
|
+
startLine = startLine,
|
|
7894
|
+
} }
|
|
7895
|
+
end
|
|
7896
|
+
if not (type(rawEdits) == "table") then
|
|
7897
|
+
return "edits must be an array"
|
|
7898
|
+
end
|
|
7899
|
+
local edits = {}
|
|
7900
|
+
for _, rawEdit in rawEdits do
|
|
7901
|
+
local index = #edits
|
|
7902
|
+
if not (type(rawEdit) == "table") then
|
|
7903
|
+
return `edits[{index}] must be an object`
|
|
7904
|
+
end
|
|
7905
|
+
local fields = rawEdit
|
|
7906
|
+
local oldString = fields.old_string
|
|
7907
|
+
local newString = fields.new_string
|
|
7908
|
+
local startLine = fields.startLine
|
|
7909
|
+
if not (type(oldString) == "string") or not (type(newString) == "string") then
|
|
7910
|
+
return `edits[{index}] requires old_string and new_string`
|
|
7911
|
+
end
|
|
7912
|
+
if startLine ~= nil and not (type(startLine) == "number") then
|
|
7913
|
+
return `edits[{index}].startLine must be a number`
|
|
7914
|
+
end
|
|
7915
|
+
local _arg0 = {
|
|
7916
|
+
oldString = oldString,
|
|
7917
|
+
newString = newString,
|
|
7918
|
+
startLine = startLine,
|
|
7919
|
+
}
|
|
7920
|
+
table.insert(edits, _arg0)
|
|
7921
|
+
end
|
|
7922
|
+
if #edits == 0 then
|
|
7923
|
+
return "edits must contain at least one edit"
|
|
7924
|
+
end
|
|
7925
|
+
return edits
|
|
7926
|
+
end
|
|
7824
7927
|
local function editScriptLines(requestData)
|
|
7825
7928
|
local instancePath = requestData.instancePath
|
|
7826
|
-
local
|
|
7827
|
-
local
|
|
7828
|
-
|
|
7829
|
-
if not (instancePath ~= "" and instancePath) or oldString == nil or newString == nil then
|
|
7929
|
+
local expectedRevision = requestData.expectedRevision
|
|
7930
|
+
local dryRun = requestData.dryRun == true
|
|
7931
|
+
if not (instancePath ~= "" and instancePath) then
|
|
7830
7932
|
return {
|
|
7831
|
-
error = "Instance path
|
|
7933
|
+
error = "Instance path is required",
|
|
7934
|
+
}
|
|
7935
|
+
end
|
|
7936
|
+
local edits = readScriptEdits(requestData)
|
|
7937
|
+
if type(edits) == "string" then
|
|
7938
|
+
return {
|
|
7939
|
+
error = edits,
|
|
7940
|
+
}
|
|
7941
|
+
end
|
|
7942
|
+
if expectedRevision ~= nil and not (type(expectedRevision) == "string") then
|
|
7943
|
+
return {
|
|
7944
|
+
error = "expectedRevision must be a string",
|
|
7832
7945
|
}
|
|
7833
7946
|
end
|
|
7834
7947
|
local instance = getInstanceByPath(instancePath)
|
|
@@ -7842,76 +7955,63 @@ local function editScriptLines(requestData)
|
|
|
7842
7955
|
error = `Instance is not a script-like object: {instance.ClassName}`,
|
|
7843
7956
|
}
|
|
7844
7957
|
end
|
|
7845
|
-
local
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
local matchStart
|
|
7850
|
-
if startLine ~= nil then
|
|
7851
|
-
if startLine < 1 then
|
|
7852
|
-
error(`startLine must be >= 1 (got {startLine})`)
|
|
7853
|
-
end
|
|
7854
|
-
local lineStartByte = 1
|
|
7855
|
-
local currentLine = 1
|
|
7856
|
-
while currentLine < startLine do
|
|
7857
|
-
local nlPos = string.find(source, "\n", lineStartByte, true)
|
|
7858
|
-
if nlPos == nil then
|
|
7859
|
-
error(`startLine {startLine} is past end of script ({currentLine} lines)`)
|
|
7860
|
-
end
|
|
7861
|
-
lineStartByte = nlPos + 1
|
|
7862
|
-
currentLine += 1
|
|
7863
|
-
end
|
|
7864
|
-
local candidate = string.sub(source, lineStartByte, lineStartByte + searchLen - 1)
|
|
7865
|
-
if candidate ~= oldString then
|
|
7866
|
-
error(`old_string does not match at line {startLine}. Use get_script_source to verify the exact text at that line.`)
|
|
7867
|
-
end
|
|
7868
|
-
matchStart = lineStartByte
|
|
7869
|
-
else
|
|
7870
|
-
local count = 0
|
|
7871
|
-
local searchPos = 1
|
|
7872
|
-
local firstMatch
|
|
7873
|
-
while true do
|
|
7874
|
-
local foundStart = string.find(source, oldString, searchPos, true)
|
|
7875
|
-
if foundStart == nil then
|
|
7876
|
-
break
|
|
7877
|
-
end
|
|
7878
|
-
if firstMatch == nil then
|
|
7879
|
-
firstMatch = foundStart
|
|
7880
|
-
end
|
|
7881
|
-
count += 1
|
|
7882
|
-
if count > 1 then
|
|
7883
|
-
break
|
|
7884
|
-
end
|
|
7885
|
-
searchPos = foundStart + searchLen
|
|
7886
|
-
end
|
|
7887
|
-
if count == 0 then
|
|
7888
|
-
error("old_string not found in script. If old_string contains repeated patterns (e.g. closing braces), pass startLine to anchor the edit.")
|
|
7889
|
-
end
|
|
7890
|
-
if count > 1 then
|
|
7891
|
-
error("old_string matches multiple locations. Provide more surrounding context, or pass startLine to anchor the edit to a specific line.")
|
|
7892
|
-
end
|
|
7893
|
-
matchStart = firstMatch
|
|
7894
|
-
end
|
|
7895
|
-
-- Byte-slice replacement avoids Lua pattern escaping (safe for multi-byte chars like em dashes).
|
|
7896
|
-
local newSource = string.sub(source, 1, matchStart - 1) .. newString .. string.sub(source, matchStart + searchLen)
|
|
7897
|
-
local applyResult = applyScriptSource(instance, newSource, source)
|
|
7898
|
-
if not applyResult.success then
|
|
7899
|
-
error(applyResult.error)
|
|
7900
|
-
end
|
|
7958
|
+
local readSuccess, readResult = pcall(function()
|
|
7959
|
+
return readScriptSource(instance)
|
|
7960
|
+
end)
|
|
7961
|
+
if not readSuccess then
|
|
7901
7962
|
return {
|
|
7963
|
+
error = `Failed to edit script: {readResult}`,
|
|
7964
|
+
}
|
|
7965
|
+
end
|
|
7966
|
+
local source = readResult
|
|
7967
|
+
local revision = sourceRevision(source)
|
|
7968
|
+
if expectedRevision ~= nil and expectedRevision ~= revision then
|
|
7969
|
+
return {
|
|
7970
|
+
error = "Script changed since expected_revision; read it again with get_script_source and retry.",
|
|
7971
|
+
code = "revision_mismatch",
|
|
7972
|
+
instancePath = instancePath,
|
|
7973
|
+
expectedRevision = expectedRevision,
|
|
7974
|
+
revision = revision,
|
|
7975
|
+
}
|
|
7976
|
+
end
|
|
7977
|
+
local plan = planScriptEdits(source, edits, dryRun)
|
|
7978
|
+
if not plan.ok then
|
|
7979
|
+
return {
|
|
7980
|
+
error = `Failed to edit script: {plan.error}`,
|
|
7981
|
+
}
|
|
7982
|
+
end
|
|
7983
|
+
if dryRun then
|
|
7984
|
+
local preview = {
|
|
7902
7985
|
success = true,
|
|
7986
|
+
dryRun = true,
|
|
7903
7987
|
instancePath = instancePath,
|
|
7904
|
-
|
|
7905
|
-
|
|
7988
|
+
editCount = #edits,
|
|
7989
|
+
revision = revision,
|
|
7990
|
+
resultRevision = sourceRevision(plan.newSource),
|
|
7991
|
+
diff = plan.diff,
|
|
7906
7992
|
}
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
return
|
|
7993
|
+
if plan.diffTruncated then
|
|
7994
|
+
preview.diffTruncated = true
|
|
7995
|
+
end
|
|
7996
|
+
return preview
|
|
7911
7997
|
end
|
|
7912
|
-
|
|
7998
|
+
local recordingId = beginRecording(if #edits > 1 then `Edit script ({#edits} edits): {instance.Name}` else `Edit script: {instance.Name}`)
|
|
7999
|
+
-- One compare-and-set write: a draft typed since the read rejects the whole batch.
|
|
8000
|
+
local applyResult = applyScriptSource(instance, plan.newSource, source)
|
|
8001
|
+
if not applyResult.success then
|
|
8002
|
+
finishRecording(recordingId, false)
|
|
8003
|
+
return {
|
|
8004
|
+
error = `Failed to edit script: {applyResult.error}`,
|
|
8005
|
+
}
|
|
8006
|
+
end
|
|
8007
|
+
finishRecording(recordingId, true)
|
|
7913
8008
|
return {
|
|
7914
|
-
|
|
8009
|
+
success = true,
|
|
8010
|
+
instancePath = instancePath,
|
|
8011
|
+
editCount = #edits,
|
|
8012
|
+
revision = sourceRevision(plan.newSource),
|
|
8013
|
+
method = applyResult.method,
|
|
8014
|
+
message = "Script edited successfully",
|
|
7915
8015
|
}
|
|
7916
8016
|
end
|
|
7917
8017
|
local function insertScriptLines(requestData)
|
|
@@ -9432,6 +9532,166 @@ return {
|
|
|
9432
9532
|
</Properties>
|
|
9433
9533
|
</Item>
|
|
9434
9534
|
<Item class="ModuleScript" referent="27">
|
|
9535
|
+
<Properties>
|
|
9536
|
+
<string name="Name">InstanceSearch</string>
|
|
9537
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
9538
|
+
-- Bounded instance search behind search_objects. It walks one subtree in
|
|
9539
|
+
-- GetChildren order, applies an optional IsA filter, and stops after
|
|
9540
|
+
-- maxResults matches. An ambiguous `path` is reported by PathGuard before this
|
|
9541
|
+
-- runs. It is written against an injected corpus so it can run outside Studio.
|
|
9542
|
+
-- Function properties rather than methods, so plain functions can be assigned
|
|
9543
|
+
-- directly under roblox-ts.
|
|
9544
|
+
local SEARCH_TYPES = {
|
|
9545
|
+
["name"] = true,
|
|
9546
|
+
["class"] = true,
|
|
9547
|
+
["property"] = true,
|
|
9548
|
+
}
|
|
9549
|
+
local DEFAULT_MAX_RESULTS = 100
|
|
9550
|
+
local MAX_RESULTS = 10000
|
|
9551
|
+
local function invalid(message)
|
|
9552
|
+
return {
|
|
9553
|
+
error = "invalid_request",
|
|
9554
|
+
message = message,
|
|
9555
|
+
}
|
|
9556
|
+
end
|
|
9557
|
+
local function containsText(haystack, needle)
|
|
9558
|
+
return (string.find(string.lower(haystack), needle, 1, true)) ~= nil
|
|
9559
|
+
end
|
|
9560
|
+
local function createInstanceSearch(corpus)
|
|
9561
|
+
local function search(requestData, control)
|
|
9562
|
+
local rawQuery = requestData.query
|
|
9563
|
+
if rawQuery ~= nil and not (type(rawQuery) == "string") then
|
|
9564
|
+
return invalid("query must be a string")
|
|
9565
|
+
end
|
|
9566
|
+
local _condition = rawQuery
|
|
9567
|
+
if _condition == nil then
|
|
9568
|
+
_condition = ""
|
|
9569
|
+
end
|
|
9570
|
+
local query = _condition
|
|
9571
|
+
local _condition_1 = requestData.searchType
|
|
9572
|
+
if _condition_1 == nil then
|
|
9573
|
+
_condition_1 = "name"
|
|
9574
|
+
end
|
|
9575
|
+
local rawSearchType = _condition_1
|
|
9576
|
+
if not (type(rawSearchType) == "string") or not (SEARCH_TYPES[rawSearchType] ~= nil) then
|
|
9577
|
+
return invalid("searchType must be one of name, class, property")
|
|
9578
|
+
end
|
|
9579
|
+
local searchType = rawSearchType
|
|
9580
|
+
local propertyName = requestData.propertyName
|
|
9581
|
+
if searchType == "property" and (not (type(propertyName) == "string") or propertyName == "") then
|
|
9582
|
+
return invalid("propertyName is required when searchType is property")
|
|
9583
|
+
end
|
|
9584
|
+
local className = requestData.className
|
|
9585
|
+
if className ~= nil and (not (type(className) == "string") or className == "") then
|
|
9586
|
+
return invalid("className must be a non-empty string")
|
|
9587
|
+
end
|
|
9588
|
+
if query == "" and className == nil then
|
|
9589
|
+
return {
|
|
9590
|
+
error = "query is required unless className is set",
|
|
9591
|
+
}
|
|
9592
|
+
end
|
|
9593
|
+
local requestedMaxResults = requestData.maxResults
|
|
9594
|
+
if requestedMaxResults ~= nil and (not (type(requestedMaxResults) == "number") or math.floor(requestedMaxResults) ~= requestedMaxResults or requestedMaxResults < 1 or requestedMaxResults > MAX_RESULTS) then
|
|
9595
|
+
return invalid(`maxResults must be an integer between 1 and {MAX_RESULTS}`)
|
|
9596
|
+
end
|
|
9597
|
+
local _condition_2 = requestedMaxResults
|
|
9598
|
+
if _condition_2 == nil then
|
|
9599
|
+
_condition_2 = DEFAULT_MAX_RESULTS
|
|
9600
|
+
end
|
|
9601
|
+
local maxResults = _condition_2
|
|
9602
|
+
local rawPath = requestData.path
|
|
9603
|
+
if rawPath ~= nil and not (type(rawPath) == "string") then
|
|
9604
|
+
return invalid("path must be a string")
|
|
9605
|
+
end
|
|
9606
|
+
local _condition_3 = rawPath
|
|
9607
|
+
if _condition_3 == nil then
|
|
9608
|
+
_condition_3 = ""
|
|
9609
|
+
end
|
|
9610
|
+
local searchPath = _condition_3
|
|
9611
|
+
local root = corpus.resolveRoot(searchPath)
|
|
9612
|
+
if root == nil then
|
|
9613
|
+
return {
|
|
9614
|
+
error = `Path not found: {searchPath}`,
|
|
9615
|
+
}
|
|
9616
|
+
end
|
|
9617
|
+
local needle = string.lower(query)
|
|
9618
|
+
local isAmbiguous = corpus.createAmbiguityChecker()
|
|
9619
|
+
local function matches(node)
|
|
9620
|
+
if className ~= nil and not corpus.isA(node, className) then
|
|
9621
|
+
return false
|
|
9622
|
+
end
|
|
9623
|
+
if needle == "" then
|
|
9624
|
+
return true
|
|
9625
|
+
end
|
|
9626
|
+
if searchType == "name" then
|
|
9627
|
+
return containsText(corpus.getName(node), needle)
|
|
9628
|
+
end
|
|
9629
|
+
if searchType == "class" then
|
|
9630
|
+
return containsText(corpus.getClassName(node), needle)
|
|
9631
|
+
end
|
|
9632
|
+
local text = corpus.readPropertyText(node, propertyName)
|
|
9633
|
+
return text ~= nil and containsText(text, needle)
|
|
9634
|
+
end
|
|
9635
|
+
local results = {}
|
|
9636
|
+
local stack = { root }
|
|
9637
|
+
local truncated = false
|
|
9638
|
+
while #stack > 0 do
|
|
9639
|
+
control:checkpoint()
|
|
9640
|
+
-- ▼ Array.pop ▼
|
|
9641
|
+
local _length = #stack
|
|
9642
|
+
local _result = stack[_length]
|
|
9643
|
+
stack[_length] = nil
|
|
9644
|
+
-- ▲ Array.pop ▲
|
|
9645
|
+
local node = _result
|
|
9646
|
+
if matches(node) then
|
|
9647
|
+
if #results >= maxResults then
|
|
9648
|
+
truncated = true
|
|
9649
|
+
break
|
|
9650
|
+
end
|
|
9651
|
+
local entry = {
|
|
9652
|
+
name = corpus.getName(node),
|
|
9653
|
+
className = corpus.getClassName(node),
|
|
9654
|
+
path = corpus.getPath(node),
|
|
9655
|
+
}
|
|
9656
|
+
if isAmbiguous(node) then
|
|
9657
|
+
entry.ambiguous = true
|
|
9658
|
+
end
|
|
9659
|
+
table.insert(results, entry)
|
|
9660
|
+
end
|
|
9661
|
+
local children = corpus.getChildren(node)
|
|
9662
|
+
for index = #children - 1, 0, -1 do
|
|
9663
|
+
local _arg0 = children[index + 1]
|
|
9664
|
+
table.insert(stack, _arg0)
|
|
9665
|
+
end
|
|
9666
|
+
end
|
|
9667
|
+
local response = {
|
|
9668
|
+
results = results,
|
|
9669
|
+
query = query,
|
|
9670
|
+
searchType = searchType,
|
|
9671
|
+
count = #results,
|
|
9672
|
+
truncated = truncated,
|
|
9673
|
+
maxResults = maxResults,
|
|
9674
|
+
path = corpus.getPath(root),
|
|
9675
|
+
}
|
|
9676
|
+
if searchType == "property" then
|
|
9677
|
+
response.propertyName = propertyName
|
|
9678
|
+
end
|
|
9679
|
+
if className ~= nil then
|
|
9680
|
+
response.className = className
|
|
9681
|
+
end
|
|
9682
|
+
return response
|
|
9683
|
+
end
|
|
9684
|
+
return {
|
|
9685
|
+
search = search,
|
|
9686
|
+
}
|
|
9687
|
+
end
|
|
9688
|
+
return {
|
|
9689
|
+
createInstanceSearch = createInstanceSearch,
|
|
9690
|
+
}
|
|
9691
|
+
]]></string>
|
|
9692
|
+
</Properties>
|
|
9693
|
+
</Item>
|
|
9694
|
+
<Item class="ModuleScript" referent="28">
|
|
9435
9695
|
<Properties>
|
|
9436
9696
|
<string name="Name">LuauExec</string>
|
|
9437
9697
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -9865,7 +10125,331 @@ return {
|
|
|
9865
10125
|
]]></string>
|
|
9866
10126
|
</Properties>
|
|
9867
10127
|
</Item>
|
|
9868
|
-
<Item class="ModuleScript" referent="
|
|
10128
|
+
<Item class="ModuleScript" referent="29">
|
|
10129
|
+
<Properties>
|
|
10130
|
+
<string name="Name">PathAmbiguity</string>
|
|
10131
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
10132
|
+
-- Canonical instance paths address children by name, so siblings that share a
|
|
10133
|
+
-- name produce identical paths. This module resolves paths while reporting the
|
|
10134
|
+
-- first ambiguous segment, and marks emitted paths that cannot identify one
|
|
10135
|
+
-- instance. It is written against an injected tree so it can run outside Studio.
|
|
10136
|
+
local function findNamedChild(tree, parent, name)
|
|
10137
|
+
local first
|
|
10138
|
+
local count = 0
|
|
10139
|
+
for _, child in tree.getChildren(parent) do
|
|
10140
|
+
if tree.getName(child) == name then
|
|
10141
|
+
if first == nil then
|
|
10142
|
+
first = child
|
|
10143
|
+
end
|
|
10144
|
+
count += 1
|
|
10145
|
+
end
|
|
10146
|
+
end
|
|
10147
|
+
return { first, count }
|
|
10148
|
+
end
|
|
10149
|
+
-- Mirrors Utils.getInstanceByPath: the root segment prefers service lookup and
|
|
10150
|
+
-- later segments take the first child with a matching name. The resolved
|
|
10151
|
+
-- instance is returned even when ambiguous, so callers decide whether to refuse.
|
|
10152
|
+
local function resolvePath(tree, parts)
|
|
10153
|
+
if #parts == 0 then
|
|
10154
|
+
return {
|
|
10155
|
+
instance = tree.root,
|
|
10156
|
+
}
|
|
10157
|
+
end
|
|
10158
|
+
local ambiguity
|
|
10159
|
+
local current = tree.getService(parts[1])
|
|
10160
|
+
if current == nil then
|
|
10161
|
+
local _binding = findNamedChild(tree, tree.root, parts[1])
|
|
10162
|
+
local first = _binding[1]
|
|
10163
|
+
local count = _binding[2]
|
|
10164
|
+
current = first
|
|
10165
|
+
if count > 1 then
|
|
10166
|
+
ambiguity = {
|
|
10167
|
+
segment = parts[1],
|
|
10168
|
+
parentPath = tree.getPath(tree.root),
|
|
10169
|
+
matchCount = count,
|
|
10170
|
+
}
|
|
10171
|
+
end
|
|
10172
|
+
end
|
|
10173
|
+
for i = 1, #parts - 1 do
|
|
10174
|
+
if current == nil then
|
|
10175
|
+
break
|
|
10176
|
+
end
|
|
10177
|
+
local _binding = findNamedChild(tree, current, parts[i + 1])
|
|
10178
|
+
local first = _binding[1]
|
|
10179
|
+
local count = _binding[2]
|
|
10180
|
+
if count > 1 and ambiguity == nil then
|
|
10181
|
+
ambiguity = {
|
|
10182
|
+
segment = parts[i + 1],
|
|
10183
|
+
parentPath = tree.getPath(current),
|
|
10184
|
+
matchCount = count,
|
|
10185
|
+
}
|
|
10186
|
+
end
|
|
10187
|
+
current = first
|
|
10188
|
+
end
|
|
10189
|
+
return {
|
|
10190
|
+
instance = current,
|
|
10191
|
+
ambiguity = ambiguity,
|
|
10192
|
+
}
|
|
10193
|
+
end
|
|
10194
|
+
-- Returns a checker for one request. Sibling name counts and verdicts are
|
|
10195
|
+
-- cached per node, so marking many results under one parent stays linear.
|
|
10196
|
+
local function createAmbiguityChecker(tree)
|
|
10197
|
+
local nameCounts = {}
|
|
10198
|
+
local verdicts = {}
|
|
10199
|
+
local function countNamed(parent, name)
|
|
10200
|
+
local _parent = parent
|
|
10201
|
+
local counts = nameCounts[_parent]
|
|
10202
|
+
if counts == nil then
|
|
10203
|
+
counts = {}
|
|
10204
|
+
for _, child in tree.getChildren(parent) do
|
|
10205
|
+
local childName = tree.getName(child)
|
|
10206
|
+
local _counts = counts
|
|
10207
|
+
local _condition = counts[childName]
|
|
10208
|
+
if _condition == nil then
|
|
10209
|
+
_condition = 0
|
|
10210
|
+
end
|
|
10211
|
+
_counts[childName] = _condition + 1
|
|
10212
|
+
end
|
|
10213
|
+
local _parent_1 = parent
|
|
10214
|
+
local _counts = counts
|
|
10215
|
+
nameCounts[_parent_1] = _counts
|
|
10216
|
+
end
|
|
10217
|
+
local _counts = counts
|
|
10218
|
+
local _name = name
|
|
10219
|
+
local _condition = _counts[_name]
|
|
10220
|
+
if _condition == nil then
|
|
10221
|
+
_condition = 0
|
|
10222
|
+
end
|
|
10223
|
+
return _condition
|
|
10224
|
+
end
|
|
10225
|
+
local function segmentIsAmbiguous(node, parent)
|
|
10226
|
+
local name = tree.getName(node)
|
|
10227
|
+
if parent == tree.root then
|
|
10228
|
+
if tree.isRootService(node) then
|
|
10229
|
+
return false
|
|
10230
|
+
end
|
|
10231
|
+
-- A root segment that names a service resolves to that service,
|
|
10232
|
+
-- never to this child.
|
|
10233
|
+
local service = tree.getService(name)
|
|
10234
|
+
if service ~= nil and service ~= node then
|
|
10235
|
+
return true
|
|
10236
|
+
end
|
|
10237
|
+
end
|
|
10238
|
+
return countNamed(parent, name) > 1
|
|
10239
|
+
end
|
|
10240
|
+
return function(node)
|
|
10241
|
+
local visited = {}
|
|
10242
|
+
local result = false
|
|
10243
|
+
local current = node
|
|
10244
|
+
while current ~= tree.root do
|
|
10245
|
+
local _current = current
|
|
10246
|
+
local cached = verdicts[_current]
|
|
10247
|
+
if cached ~= nil then
|
|
10248
|
+
result = cached
|
|
10249
|
+
break
|
|
10250
|
+
end
|
|
10251
|
+
local parent = tree.getParent(current)
|
|
10252
|
+
if parent == nil then
|
|
10253
|
+
break
|
|
10254
|
+
end
|
|
10255
|
+
local _current_1 = current
|
|
10256
|
+
table.insert(visited, _current_1)
|
|
10257
|
+
if segmentIsAmbiguous(current, parent) then
|
|
10258
|
+
result = true
|
|
10259
|
+
break
|
|
10260
|
+
end
|
|
10261
|
+
current = parent
|
|
10262
|
+
end
|
|
10263
|
+
for _, visitedNode in visited do
|
|
10264
|
+
local _result = result
|
|
10265
|
+
verdicts[visitedNode] = _result
|
|
10266
|
+
end
|
|
10267
|
+
return result
|
|
10268
|
+
end
|
|
10269
|
+
end
|
|
10270
|
+
return {
|
|
10271
|
+
resolvePath = resolvePath,
|
|
10272
|
+
createAmbiguityChecker = createAmbiguityChecker,
|
|
10273
|
+
}
|
|
10274
|
+
]]></string>
|
|
10275
|
+
</Properties>
|
|
10276
|
+
</Item>
|
|
10277
|
+
<Item class="ModuleScript" referent="30">
|
|
10278
|
+
<Properties>
|
|
10279
|
+
<string name="Name">PathGuard</string>
|
|
10280
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
10281
|
+
-- Central handling of request paths that name more than one instance.
|
|
10282
|
+
-- Every endpoint that accepts instance paths declares them here. Before the
|
|
10283
|
+
-- handler runs, each declared path is checked:
|
|
10284
|
+
-- - write: an ambiguous path refuses the request, so no arbitrary duplicate
|
|
10285
|
+
-- is modified;
|
|
10286
|
+
-- - read: the handler runs on the first match, and the response gains
|
|
10287
|
+
-- `ambiguousPaths` and `pathWarning` so the caller knows it may be wrong.
|
|
10288
|
+
-- Written against an injected resolver so it can run outside Studio.
|
|
10289
|
+
local function pathField(field, access)
|
|
10290
|
+
return function(data)
|
|
10291
|
+
local value = data[field]
|
|
10292
|
+
if not (type(value) == "string") or value == "" then
|
|
10293
|
+
return {}
|
|
10294
|
+
end
|
|
10295
|
+
local _object = {
|
|
10296
|
+
field = field,
|
|
10297
|
+
path = value,
|
|
10298
|
+
}
|
|
10299
|
+
local _left = "access"
|
|
10300
|
+
local _access = access
|
|
10301
|
+
_object[_left] = if type(_access) == "string" then access else access(data)
|
|
10302
|
+
return { _object }
|
|
10303
|
+
end
|
|
10304
|
+
end
|
|
10305
|
+
local function pathListField(field, access)
|
|
10306
|
+
return function(data)
|
|
10307
|
+
local value = data[field]
|
|
10308
|
+
local references = {}
|
|
10309
|
+
if not (type(value) == "table") then
|
|
10310
|
+
return references
|
|
10311
|
+
end
|
|
10312
|
+
local index = 0
|
|
10313
|
+
for _, entry in value do
|
|
10314
|
+
if type(entry) == "string" and entry ~= "" then
|
|
10315
|
+
local _arg0 = {
|
|
10316
|
+
field = `{field}[{index}]`,
|
|
10317
|
+
path = entry,
|
|
10318
|
+
access = access,
|
|
10319
|
+
}
|
|
10320
|
+
table.insert(references, _arg0)
|
|
10321
|
+
end
|
|
10322
|
+
index += 1
|
|
10323
|
+
end
|
|
10324
|
+
return references
|
|
10325
|
+
end
|
|
10326
|
+
end
|
|
10327
|
+
-- set_properties takes instance paths as the values of reference properties.
|
|
10328
|
+
local REFERENCE_PROPERTIES = { "Parent", "PrimaryPart" }
|
|
10329
|
+
local function referencePropertyPaths(data)
|
|
10330
|
+
local properties = data.properties
|
|
10331
|
+
local references = {}
|
|
10332
|
+
if not (type(properties) == "table") then
|
|
10333
|
+
return references
|
|
10334
|
+
end
|
|
10335
|
+
for _, propertyName in REFERENCE_PROPERTIES do
|
|
10336
|
+
local value = properties[propertyName]
|
|
10337
|
+
if type(value) == "string" and value ~= "" then
|
|
10338
|
+
local _arg0 = {
|
|
10339
|
+
field = `properties.{propertyName}`,
|
|
10340
|
+
path = value,
|
|
10341
|
+
access = "write",
|
|
10342
|
+
}
|
|
10343
|
+
table.insert(references, _arg0)
|
|
10344
|
+
end
|
|
10345
|
+
end
|
|
10346
|
+
return references
|
|
10347
|
+
end
|
|
10348
|
+
local dryRunIsRead = function(data)
|
|
10349
|
+
return if data.dryRun == true then "read" else "write"
|
|
10350
|
+
end
|
|
10351
|
+
local ENDPOINT_PATHS = {
|
|
10352
|
+
["/api/file-tree"] = { pathField("path", "read") },
|
|
10353
|
+
["/api/search-objects"] = { pathField("path", "read") },
|
|
10354
|
+
["/api/instance-properties"] = { pathField("instancePath", "read") },
|
|
10355
|
+
["/api/project-structure"] = { pathField("path", "read") },
|
|
10356
|
+
["/api/grep-scripts"] = { pathField("path", "read") },
|
|
10357
|
+
["/api/get-script-source"] = { pathField("instancePath", "read") },
|
|
10358
|
+
["/api/get-attributes"] = { pathField("instancePath", "read") },
|
|
10359
|
+
["/api/set-selection"] = { pathListField("paths", "read") },
|
|
10360
|
+
["/api/focus-viewport"] = { pathField("path", "read") },
|
|
10361
|
+
["/api/breakpoints"] = { pathField("script_path", "read") },
|
|
10362
|
+
["/api/export-rbxm"] = { pathListField("instance_paths", "read") },
|
|
10363
|
+
["/api/set-properties"] = { pathField("instancePath", "write"), referencePropertyPaths },
|
|
10364
|
+
["/api/set-script-source"] = { pathField("instancePath", "write") },
|
|
10365
|
+
["/api/edit-script-lines"] = { pathField("instancePath", dryRunIsRead) },
|
|
10366
|
+
["/api/insert-script-lines"] = { pathField("instancePath", "write") },
|
|
10367
|
+
["/api/delete-script-lines"] = { pathField("instancePath", "write") },
|
|
10368
|
+
["/api/find-and-replace-in-scripts"] = { pathField("path", dryRunIsRead) },
|
|
10369
|
+
["/api/insert-asset"] = { pathField("parentPath", "write") },
|
|
10370
|
+
["/api/import-rbxm"] = { pathField("parent_path", "write") },
|
|
10371
|
+
}
|
|
10372
|
+
local function describe(report)
|
|
10373
|
+
return `{report.path} ({report.field}): {report.matchCount} children of {report.parentPath} are named "{report.segment}"`
|
|
10374
|
+
end
|
|
10375
|
+
local function describeAll(reports)
|
|
10376
|
+
local parts = {}
|
|
10377
|
+
for _, report in reports do
|
|
10378
|
+
local _arg0 = describe(report)
|
|
10379
|
+
table.insert(parts, _arg0)
|
|
10380
|
+
end
|
|
10381
|
+
return table.concat(parts, "; ")
|
|
10382
|
+
end
|
|
10383
|
+
local function createPathGuard(findAmbiguity)
|
|
10384
|
+
local function inspect(endpoint, data)
|
|
10385
|
+
local reports = {}
|
|
10386
|
+
local extractors = ENDPOINT_PATHS[endpoint]
|
|
10387
|
+
if extractors == nil then
|
|
10388
|
+
return reports
|
|
10389
|
+
end
|
|
10390
|
+
for _, extract in extractors do
|
|
10391
|
+
for _1, reference in extract(data) do
|
|
10392
|
+
local ambiguity = findAmbiguity(reference.path)
|
|
10393
|
+
if ambiguity ~= nil then
|
|
10394
|
+
local _arg0 = {
|
|
10395
|
+
field = reference.field,
|
|
10396
|
+
path = reference.path,
|
|
10397
|
+
access = reference.access,
|
|
10398
|
+
segment = ambiguity.segment,
|
|
10399
|
+
parentPath = ambiguity.parentPath,
|
|
10400
|
+
matchCount = ambiguity.matchCount,
|
|
10401
|
+
}
|
|
10402
|
+
table.insert(reports, _arg0)
|
|
10403
|
+
end
|
|
10404
|
+
end
|
|
10405
|
+
end
|
|
10406
|
+
return reports
|
|
10407
|
+
end
|
|
10408
|
+
local function run(endpoint, data, handle)
|
|
10409
|
+
local reports = inspect(endpoint, data)
|
|
10410
|
+
if #reports == 0 then
|
|
10411
|
+
return handle()
|
|
10412
|
+
end
|
|
10413
|
+
-- ▼ ReadonlyArray.filter ▼
|
|
10414
|
+
local _newValue = {}
|
|
10415
|
+
local _callback = function(report)
|
|
10416
|
+
return report.access == "write"
|
|
10417
|
+
end
|
|
10418
|
+
local _length = 0
|
|
10419
|
+
for _k, _v in reports do
|
|
10420
|
+
if _callback(_v, _k - 1, reports) == true then
|
|
10421
|
+
_length += 1
|
|
10422
|
+
_newValue[_length] = _v
|
|
10423
|
+
end
|
|
10424
|
+
end
|
|
10425
|
+
-- ▲ ReadonlyArray.filter ▲
|
|
10426
|
+
local writes = _newValue
|
|
10427
|
+
if #writes > 0 then
|
|
10428
|
+
return {
|
|
10429
|
+
error = `Ambiguous path {describeAll(writes)}. Refusing to modify an arbitrary match; rename the duplicates or target the instance with execute_luau.`,
|
|
10430
|
+
ambiguousPaths = writes,
|
|
10431
|
+
}
|
|
10432
|
+
end
|
|
10433
|
+
local result = handle()
|
|
10434
|
+
if type(result) == "table" then
|
|
10435
|
+
local response = result
|
|
10436
|
+
response.ambiguousPaths = reports
|
|
10437
|
+
response.pathWarning = `Ambiguous path {describeAll(reports)}; the first match was used.`
|
|
10438
|
+
end
|
|
10439
|
+
return result
|
|
10440
|
+
end
|
|
10441
|
+
return {
|
|
10442
|
+
inspect = inspect,
|
|
10443
|
+
run = run,
|
|
10444
|
+
}
|
|
10445
|
+
end
|
|
10446
|
+
return {
|
|
10447
|
+
createPathGuard = createPathGuard,
|
|
10448
|
+
}
|
|
10449
|
+
]]></string>
|
|
10450
|
+
</Properties>
|
|
10451
|
+
</Item>
|
|
10452
|
+
<Item class="ModuleScript" referent="31">
|
|
9869
10453
|
<Properties>
|
|
9870
10454
|
<string name="Name">PeerRole</string>
|
|
9871
10455
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -9886,7 +10470,7 @@ return {
|
|
|
9886
10470
|
]]></string>
|
|
9887
10471
|
</Properties>
|
|
9888
10472
|
</Item>
|
|
9889
|
-
<Item class="ModuleScript" referent="
|
|
10473
|
+
<Item class="ModuleScript" referent="32">
|
|
9890
10474
|
<Properties>
|
|
9891
10475
|
<string name="Name">PlaytestTickets</string>
|
|
9892
10476
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -10067,7 +10651,7 @@ return {
|
|
|
10067
10651
|
]]></string>
|
|
10068
10652
|
</Properties>
|
|
10069
10653
|
</Item>
|
|
10070
|
-
<Item class="ModuleScript" referent="
|
|
10654
|
+
<Item class="ModuleScript" referent="33">
|
|
10071
10655
|
<Properties>
|
|
10072
10656
|
<string name="Name">PluginSession</string>
|
|
10073
10657
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -10092,10 +10676,12 @@ local TOPOLOGY_TOKEN_ATTRIBUTE = "__MCPTopologyToken"
|
|
|
10092
10676
|
-- saved or published DataModel, so edit sessions remove them on load.
|
|
10093
10677
|
local LEGACY_TOPOLOGY_ATTRIBUTES = { TOPOLOGY_MODE_ATTRIBUTE, TOPOLOGY_INSTANCE_ID_ATTRIBUTE, TOPOLOGY_GROUP_ID_ATTRIBUTE, TOPOLOGY_TOKEN_ATTRIBUTE }
|
|
10094
10678
|
local SESSION_IDENTITY_NAME = "__MCPSessionIdentity"
|
|
10095
|
-
--
|
|
10096
|
-
-- is never saved with the place or
|
|
10097
|
-
|
|
10098
|
-
|
|
10679
|
+
-- JSON session state lives in StringValue children of the CoreGui identity
|
|
10680
|
+
-- value: CoreGui survives plugin reloads but is never saved with the place or
|
|
10681
|
+
-- copied into playtest DataModels. Not attributes: with NextGenerationReplication,
|
|
10682
|
+
-- playtest DataModels reject attribute strings longer than 50 characters.
|
|
10683
|
+
local PLAY_RECORD_NAME = "PlayRecord"
|
|
10684
|
+
local RUNTIME_TOPOLOGY_NAME = "RuntimeTopology"
|
|
10099
10685
|
-- MCP-started solo and Run playtests carry their topology in StudioTestService
|
|
10100
10686
|
-- test args, which never touch the DataModel.
|
|
10101
10687
|
local TEST_ARGS_TOPOLOGY_KEY = "__mcpTopology"
|
|
@@ -10216,18 +10802,54 @@ local function optionalString(value)
|
|
|
10216
10802
|
end
|
|
10217
10803
|
return if _condition then value else nil
|
|
10218
10804
|
end
|
|
10219
|
-
function
|
|
10220
|
-
local
|
|
10221
|
-
if not (
|
|
10805
|
+
local function readSessionState(name)
|
|
10806
|
+
local holder = sessionIdentity:FindFirstChild(name)
|
|
10807
|
+
if holder == nil or not holder:IsA("StringValue") then
|
|
10222
10808
|
return nil
|
|
10223
10809
|
end
|
|
10224
10810
|
local ok, decoded = pcall(function()
|
|
10225
|
-
return HttpService:JSONDecode(
|
|
10811
|
+
return HttpService:JSONDecode(holder.Value)
|
|
10226
10812
|
end)
|
|
10227
10813
|
if not ok or not (type(decoded) == "table") then
|
|
10228
10814
|
return nil
|
|
10229
10815
|
end
|
|
10230
|
-
|
|
10816
|
+
return decoded
|
|
10817
|
+
end
|
|
10818
|
+
--* Best effort: saved session state only lets a plugin reload resume where it left off.
|
|
10819
|
+
local function writeSessionState(name, state)
|
|
10820
|
+
local ok, err = pcall(function()
|
|
10821
|
+
local existing = sessionIdentity:FindFirstChild(name)
|
|
10822
|
+
if state == nil then
|
|
10823
|
+
local _result = existing
|
|
10824
|
+
if _result ~= nil then
|
|
10825
|
+
_result:Destroy()
|
|
10826
|
+
end
|
|
10827
|
+
return nil
|
|
10828
|
+
end
|
|
10829
|
+
local encoded = HttpService:JSONEncode(state)
|
|
10830
|
+
if existing ~= nil and existing:IsA("StringValue") then
|
|
10831
|
+
existing.Value = encoded
|
|
10832
|
+
return nil
|
|
10833
|
+
end
|
|
10834
|
+
local _result = existing
|
|
10835
|
+
if _result ~= nil then
|
|
10836
|
+
_result:Destroy()
|
|
10837
|
+
end
|
|
10838
|
+
local holder = Instance.new("StringValue")
|
|
10839
|
+
holder.Name = name
|
|
10840
|
+
holder.Value = encoded
|
|
10841
|
+
holder.Archivable = false
|
|
10842
|
+
holder.Parent = sessionIdentity
|
|
10843
|
+
end)
|
|
10844
|
+
if not ok then
|
|
10845
|
+
warn(`[robloxstudio-mcp] Could not save {name} session state: {err}`)
|
|
10846
|
+
end
|
|
10847
|
+
end
|
|
10848
|
+
function readPlayRecord()
|
|
10849
|
+
local data = readSessionState(PLAY_RECORD_NAME)
|
|
10850
|
+
if data == nil then
|
|
10851
|
+
return nil
|
|
10852
|
+
end
|
|
10231
10853
|
local phase = if data.phase == "armed" or data.phase == "active" then data.phase else nil
|
|
10232
10854
|
if phase == nil then
|
|
10233
10855
|
return nil
|
|
@@ -10243,16 +10865,7 @@ function readPlayRecord()
|
|
|
10243
10865
|
}
|
|
10244
10866
|
end
|
|
10245
10867
|
local function writePlayRecord(record)
|
|
10246
|
-
|
|
10247
|
-
sessionIdentity:SetAttribute(PLAY_RECORD_ATTRIBUTE, nil)
|
|
10248
|
-
return nil
|
|
10249
|
-
end
|
|
10250
|
-
local ok, encoded = pcall(function()
|
|
10251
|
-
return HttpService:JSONEncode(record)
|
|
10252
|
-
end)
|
|
10253
|
-
if ok and type(encoded) == "string" then
|
|
10254
|
-
sessionIdentity:SetAttribute(PLAY_RECORD_ATTRIBUTE, encoded)
|
|
10255
|
-
end
|
|
10868
|
+
writeSessionState(PLAY_RECORD_NAME, record)
|
|
10256
10869
|
end
|
|
10257
10870
|
local function createTicket(instanceId, groupId)
|
|
10258
10871
|
local now = PlaytestTickets.nowMs()
|
|
@@ -10544,17 +11157,10 @@ local function topologyFromTestArgs()
|
|
|
10544
11157
|
}
|
|
10545
11158
|
end
|
|
10546
11159
|
local function readRememberedRuntimeTopology()
|
|
10547
|
-
local
|
|
10548
|
-
if
|
|
11160
|
+
local data = readSessionState(RUNTIME_TOPOLOGY_NAME)
|
|
11161
|
+
if data == nil then
|
|
10549
11162
|
return nil
|
|
10550
11163
|
end
|
|
10551
|
-
local ok, decoded = pcall(function()
|
|
10552
|
-
return HttpService:JSONDecode(raw)
|
|
10553
|
-
end)
|
|
10554
|
-
if not ok or not (type(decoded) == "table") then
|
|
10555
|
-
return nil
|
|
10556
|
-
end
|
|
10557
|
-
local data = decoded
|
|
10558
11164
|
return {
|
|
10559
11165
|
instanceId = optionalString(data.instanceId),
|
|
10560
11166
|
groupId = optionalString(data.groupId),
|
|
@@ -10564,12 +11170,7 @@ local function readRememberedRuntimeTopology()
|
|
|
10564
11170
|
end
|
|
10565
11171
|
local function adoptRuntimeTopology(topology)
|
|
10566
11172
|
runtimeTopology = topology
|
|
10567
|
-
|
|
10568
|
-
return HttpService:JSONEncode(topology)
|
|
10569
|
-
end)
|
|
10570
|
-
if ok and type(encoded) == "string" then
|
|
10571
|
-
sessionIdentity:SetAttribute(RUNTIME_TOPOLOGY_ATTRIBUTE, encoded)
|
|
10572
|
-
end
|
|
11173
|
+
writeSessionState(RUNTIME_TOPOLOGY_NAME, topology)
|
|
10573
11174
|
end
|
|
10574
11175
|
--[[
|
|
10575
11176
|
*
|
|
@@ -10717,7 +11318,7 @@ return {
|
|
|
10717
11318
|
]]></string>
|
|
10718
11319
|
</Properties>
|
|
10719
11320
|
</Item>
|
|
10720
|
-
<Item class="ModuleScript" referent="
|
|
11321
|
+
<Item class="ModuleScript" referent="34">
|
|
10721
11322
|
<Properties>
|
|
10722
11323
|
<string name="Name">Recording</string>
|
|
10723
11324
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -10747,7 +11348,7 @@ return {
|
|
|
10747
11348
|
]]></string>
|
|
10748
11349
|
</Properties>
|
|
10749
11350
|
</Item>
|
|
10750
|
-
<Item class="ModuleScript" referent="
|
|
11351
|
+
<Item class="ModuleScript" referent="35">
|
|
10751
11352
|
<Properties>
|
|
10752
11353
|
<string name="Name">RenderMonitor</string>
|
|
10753
11354
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -10815,7 +11416,7 @@ return {
|
|
|
10815
11416
|
]]></string>
|
|
10816
11417
|
</Properties>
|
|
10817
11418
|
</Item>
|
|
10818
|
-
<Item class="ModuleScript" referent="
|
|
11419
|
+
<Item class="ModuleScript" referent="36">
|
|
10819
11420
|
<Properties>
|
|
10820
11421
|
<string name="Name">RuntimeLogBuffer</string>
|
|
10821
11422
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -11051,7 +11652,357 @@ return {
|
|
|
11051
11652
|
]]></string>
|
|
11052
11653
|
</Properties>
|
|
11053
11654
|
</Item>
|
|
11054
|
-
<Item class="ModuleScript" referent="
|
|
11655
|
+
<Item class="ModuleScript" referent="37">
|
|
11656
|
+
<Properties>
|
|
11657
|
+
<string name="Name">ScriptEdits</string>
|
|
11658
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
11659
|
+
-- Exact-text script edits resolved against one source snapshot, so several
|
|
11660
|
+
-- replacements either all apply or none do. Kept free of Studio services so
|
|
11661
|
+
-- the planning logic can be exercised outside Studio.
|
|
11662
|
+
local FNV_OFFSET_BASIS = 2166136261
|
|
11663
|
+
local UINT32_MODULUS = 4294967296
|
|
11664
|
+
local BYTE_CHUNK = 4096
|
|
11665
|
+
local DIFF_CHAR_BUDGET = 20000
|
|
11666
|
+
--[[
|
|
11667
|
+
*
|
|
11668
|
+
* Content revision of the exact script text: byte length plus FNV-1a 32.
|
|
11669
|
+
* It is an optimistic-concurrency token, not a security hash.
|
|
11670
|
+
|
|
11671
|
+
]]
|
|
11672
|
+
local function sourceRevision(source)
|
|
11673
|
+
local length = #source
|
|
11674
|
+
local hash = FNV_OFFSET_BASIS
|
|
11675
|
+
do
|
|
11676
|
+
local chunkStart = 1
|
|
11677
|
+
local _shouldIncrement = false
|
|
11678
|
+
while true do
|
|
11679
|
+
if _shouldIncrement then
|
|
11680
|
+
chunkStart += BYTE_CHUNK
|
|
11681
|
+
else
|
|
11682
|
+
_shouldIncrement = true
|
|
11683
|
+
end
|
|
11684
|
+
if not (chunkStart <= length) then
|
|
11685
|
+
break
|
|
11686
|
+
end
|
|
11687
|
+
local _array = {}
|
|
11688
|
+
local _length = #_array
|
|
11689
|
+
local _array_1 = { string.byte(source, chunkStart, math.min(chunkStart + BYTE_CHUNK - 1, length)) }
|
|
11690
|
+
table.move(_array_1, 1, #_array_1, _length + 1, _array)
|
|
11691
|
+
local bytes = _array
|
|
11692
|
+
for _, byte in bytes do
|
|
11693
|
+
hash = bit32.bxor(hash, byte)
|
|
11694
|
+
-- hash * 16777619 mod 2^32. The FNV prime is 2^24 + 403; splitting the
|
|
11695
|
+
-- product keeps every intermediate below 2^53, where doubles stay exact.
|
|
11696
|
+
hash = (bit32.lshift(hash, 24) + hash * 403) % UINT32_MODULUS
|
|
11697
|
+
end
|
|
11698
|
+
end
|
|
11699
|
+
end
|
|
11700
|
+
return `{length}:{hash}`
|
|
11701
|
+
end
|
|
11702
|
+
-- 1-based byte offset at which each line starts. A trailing newline yields a
|
|
11703
|
+
-- final empty line, which is where an anchored insertion at EOF lands.
|
|
11704
|
+
local function lineStarts(source)
|
|
11705
|
+
local starts = { 1 }
|
|
11706
|
+
local searchFrom = 1
|
|
11707
|
+
while true do
|
|
11708
|
+
local newline = string.find(source, "\n", searchFrom, true)
|
|
11709
|
+
if newline == nil then
|
|
11710
|
+
break
|
|
11711
|
+
end
|
|
11712
|
+
local _arg0 = newline + 1
|
|
11713
|
+
table.insert(starts, _arg0)
|
|
11714
|
+
searchFrom = newline + 1
|
|
11715
|
+
end
|
|
11716
|
+
return starts
|
|
11717
|
+
end
|
|
11718
|
+
-- Line containing a byte offset; a newline belongs to the line it ends.
|
|
11719
|
+
local function lineAt(starts, offset)
|
|
11720
|
+
local low = 0
|
|
11721
|
+
local high = #starts - 1
|
|
11722
|
+
while low < high do
|
|
11723
|
+
local middle = math.floor((low + high + 1) / 2)
|
|
11724
|
+
if starts[middle + 1] <= offset then
|
|
11725
|
+
low = middle
|
|
11726
|
+
else
|
|
11727
|
+
high = middle - 1
|
|
11728
|
+
end
|
|
11729
|
+
end
|
|
11730
|
+
return low + 1
|
|
11731
|
+
end
|
|
11732
|
+
-- Byte offset just past the line's terminator, or past the end of the source.
|
|
11733
|
+
local function lineEnd(starts, source, line)
|
|
11734
|
+
return if line < #starts then starts[line + 1] else #source + 1
|
|
11735
|
+
end
|
|
11736
|
+
local function locateEdit(source, starts, edit, label)
|
|
11737
|
+
local searchLength = #edit.oldString
|
|
11738
|
+
if edit.startLine ~= nil then
|
|
11739
|
+
if edit.startLine < 1 or edit.startLine % 1 ~= 0 then
|
|
11740
|
+
return `{label}line must be a positive whole number (got {edit.startLine}).`
|
|
11741
|
+
end
|
|
11742
|
+
if edit.startLine > #starts then
|
|
11743
|
+
return `{label}line {edit.startLine} is past the end of the script.`
|
|
11744
|
+
end
|
|
11745
|
+
local start = starts[edit.startLine]
|
|
11746
|
+
if string.sub(source, start, start + searchLength - 1) ~= edit.oldString then
|
|
11747
|
+
return `{label}old_string does not match at line {edit.startLine}. Use get_script_source to verify the exact text at that line.`
|
|
11748
|
+
end
|
|
11749
|
+
return start
|
|
11750
|
+
end
|
|
11751
|
+
if searchLength == 0 then
|
|
11752
|
+
return `{label}old_string is empty; pass line_range to insert at the start of a line.`
|
|
11753
|
+
end
|
|
11754
|
+
local first = string.find(source, edit.oldString, 1, true)
|
|
11755
|
+
if first == nil then
|
|
11756
|
+
return `{label}old_string not found in script. If old_string contains repeated patterns (e.g. closing braces), pass line_range to anchor the edit.`
|
|
11757
|
+
end
|
|
11758
|
+
local second = string.find(source, edit.oldString, first + searchLength, true)
|
|
11759
|
+
if second ~= nil then
|
|
11760
|
+
return `{label}old_string matches multiple locations (lines {lineAt(starts, first)} and {lineAt(starts, second)}). Provide more surrounding context, or pass line_range to anchor the edit.`
|
|
11761
|
+
end
|
|
11762
|
+
return first
|
|
11763
|
+
end
|
|
11764
|
+
-- Source text in [regionStart, regionEnd) with every edit in resolved[from, to) applied.
|
|
11765
|
+
local function applyWithin(source, resolved, from, to, regionStart, regionEnd)
|
|
11766
|
+
local parts = {}
|
|
11767
|
+
local cursor = regionStart
|
|
11768
|
+
do
|
|
11769
|
+
local index = from
|
|
11770
|
+
local _shouldIncrement = false
|
|
11771
|
+
while true do
|
|
11772
|
+
if _shouldIncrement then
|
|
11773
|
+
index += 1
|
|
11774
|
+
else
|
|
11775
|
+
_shouldIncrement = true
|
|
11776
|
+
end
|
|
11777
|
+
if not (index < to) then
|
|
11778
|
+
break
|
|
11779
|
+
end
|
|
11780
|
+
local edit = resolved[index + 1]
|
|
11781
|
+
local _arg0 = string.sub(source, cursor, edit.start - 1)
|
|
11782
|
+
table.insert(parts, _arg0)
|
|
11783
|
+
local _newString = edit.newString
|
|
11784
|
+
table.insert(parts, _newString)
|
|
11785
|
+
cursor = edit.finish
|
|
11786
|
+
end
|
|
11787
|
+
end
|
|
11788
|
+
local _arg0 = string.sub(source, cursor, regionEnd - 1)
|
|
11789
|
+
table.insert(parts, _arg0)
|
|
11790
|
+
return table.concat(parts, "")
|
|
11791
|
+
end
|
|
11792
|
+
local NO_FINAL_NEWLINE = "\"
|
|
11793
|
+
-- Lines of a diff region. A last line without its newline only occurs at the
|
|
11794
|
+
-- end of the file; it is flagged so that adding or removing that newline still
|
|
11795
|
+
-- differs, as in a unified diff.
|
|
11796
|
+
local function textLines(text)
|
|
11797
|
+
if text == "" then
|
|
11798
|
+
return {
|
|
11799
|
+
lines = {},
|
|
11800
|
+
missingFinalNewline = false,
|
|
11801
|
+
}
|
|
11802
|
+
end
|
|
11803
|
+
local lines = string.split(text, "\n")
|
|
11804
|
+
local missingFinalNewline = string.sub(text, -1) ~= "\n"
|
|
11805
|
+
if not missingFinalNewline then
|
|
11806
|
+
lines[#lines] = nil
|
|
11807
|
+
end
|
|
11808
|
+
return {
|
|
11809
|
+
lines = lines,
|
|
11810
|
+
missingFinalNewline = missingFinalNewline,
|
|
11811
|
+
}
|
|
11812
|
+
end
|
|
11813
|
+
local function sameLine(a, aIndex, b, bIndex)
|
|
11814
|
+
return a.lines[aIndex + 1] == b.lines[bIndex + 1] and (a.missingFinalNewline and aIndex == #a.lines - 1) == (b.missingFinalNewline and bIndex == #b.lines - 1)
|
|
11815
|
+
end
|
|
11816
|
+
local function pushHunkLines(hunk, text, from, count, prefix)
|
|
11817
|
+
do
|
|
11818
|
+
local index = from
|
|
11819
|
+
local _shouldIncrement = false
|
|
11820
|
+
while true do
|
|
11821
|
+
if _shouldIncrement then
|
|
11822
|
+
index += 1
|
|
11823
|
+
else
|
|
11824
|
+
_shouldIncrement = true
|
|
11825
|
+
end
|
|
11826
|
+
if not (index < from + count) then
|
|
11827
|
+
break
|
|
11828
|
+
end
|
|
11829
|
+
local _hunk = hunk
|
|
11830
|
+
local _arg0 = `{prefix}{text.lines[index + 1]}`
|
|
11831
|
+
table.insert(_hunk, _arg0)
|
|
11832
|
+
if text.missingFinalNewline and index == #text.lines - 1 then
|
|
11833
|
+
table.insert(hunk, NO_FINAL_NEWLINE)
|
|
11834
|
+
end
|
|
11835
|
+
end
|
|
11836
|
+
end
|
|
11837
|
+
end
|
|
11838
|
+
local function hunkStart(line, count)
|
|
11839
|
+
return if count == 0 then line - 1 else line
|
|
11840
|
+
end
|
|
11841
|
+
local function editFirstLine(starts, edit)
|
|
11842
|
+
return lineAt(starts, edit.start)
|
|
11843
|
+
end
|
|
11844
|
+
local function editLastLine(starts, edit)
|
|
11845
|
+
return lineAt(starts, if edit.finish > edit.start then edit.finish - 1 else edit.start)
|
|
11846
|
+
end
|
|
11847
|
+
-- Unified diff of whole changed lines, without context lines. Edits that share
|
|
11848
|
+
-- a line become one hunk; lines the edit left unchanged are trimmed away.
|
|
11849
|
+
local function buildDiff(source, starts, resolved)
|
|
11850
|
+
local output = {}
|
|
11851
|
+
local outputSize = 0
|
|
11852
|
+
local truncated = false
|
|
11853
|
+
local lineDelta = 0
|
|
11854
|
+
local groupStart = 0
|
|
11855
|
+
while groupStart < #resolved and not truncated do
|
|
11856
|
+
local firstLine = editFirstLine(starts, resolved[groupStart + 1])
|
|
11857
|
+
local lastLine = editLastLine(starts, resolved[groupStart + 1])
|
|
11858
|
+
local groupEnd = groupStart + 1
|
|
11859
|
+
local oldText
|
|
11860
|
+
local newText
|
|
11861
|
+
while true do
|
|
11862
|
+
while groupEnd < #resolved and editFirstLine(starts, resolved[groupEnd + 1]) <= lastLine do
|
|
11863
|
+
lastLine = math.max(lastLine, editLastLine(starts, resolved[groupEnd + 1]))
|
|
11864
|
+
groupEnd += 1
|
|
11865
|
+
end
|
|
11866
|
+
local regionStart = starts[firstLine]
|
|
11867
|
+
local regionEnd = lineEnd(starts, source, lastLine)
|
|
11868
|
+
oldText = string.sub(source, regionStart, regionEnd - 1)
|
|
11869
|
+
newText = applyWithin(source, resolved, groupStart, groupEnd, regionStart, regionEnd)
|
|
11870
|
+
-- A replacement that drops the region's final newline joins the next
|
|
11871
|
+
-- line onto it, so that line belongs in the hunk too.
|
|
11872
|
+
local nextLineHasText = lastLine < #starts and starts[lastLine + 1] <= #source
|
|
11873
|
+
if nextLineHasText and string.sub(oldText, -1) == "\n" and string.sub(newText, -1) ~= "\n" then
|
|
11874
|
+
lastLine += 1
|
|
11875
|
+
continue
|
|
11876
|
+
end
|
|
11877
|
+
break
|
|
11878
|
+
end
|
|
11879
|
+
local oldRegion = textLines(oldText)
|
|
11880
|
+
local newRegion = textLines(newText)
|
|
11881
|
+
local oldSize = #oldRegion.lines
|
|
11882
|
+
local newSize = #newRegion.lines
|
|
11883
|
+
local prefix = 0
|
|
11884
|
+
while prefix < oldSize and prefix < newSize and sameLine(oldRegion, prefix, newRegion, prefix) do
|
|
11885
|
+
prefix += 1
|
|
11886
|
+
end
|
|
11887
|
+
local suffix = 0
|
|
11888
|
+
while suffix < oldSize - prefix and suffix < newSize - prefix and sameLine(oldRegion, oldSize - 1 - suffix, newRegion, newSize - 1 - suffix) do
|
|
11889
|
+
suffix += 1
|
|
11890
|
+
end
|
|
11891
|
+
local oldCount = oldSize - prefix - suffix
|
|
11892
|
+
local newCount = newSize - prefix - suffix
|
|
11893
|
+
if oldCount > 0 or newCount > 0 then
|
|
11894
|
+
local oldStart = firstLine + prefix
|
|
11895
|
+
local newStart = oldStart + lineDelta
|
|
11896
|
+
local hunk = { `@@ -{hunkStart(oldStart, oldCount)},{oldCount} +{hunkStart(newStart, newCount)},{newCount} @@` }
|
|
11897
|
+
pushHunkLines(hunk, oldRegion, prefix, oldCount, "-")
|
|
11898
|
+
pushHunkLines(hunk, newRegion, prefix, newCount, "+")
|
|
11899
|
+
for _, line in hunk do
|
|
11900
|
+
if outputSize + #line + 1 > DIFF_CHAR_BUDGET then
|
|
11901
|
+
truncated = true
|
|
11902
|
+
break
|
|
11903
|
+
end
|
|
11904
|
+
table.insert(output, line)
|
|
11905
|
+
outputSize += #line + 1
|
|
11906
|
+
end
|
|
11907
|
+
end
|
|
11908
|
+
lineDelta += newSize - oldSize
|
|
11909
|
+
groupStart = groupEnd
|
|
11910
|
+
end
|
|
11911
|
+
return {
|
|
11912
|
+
diff = table.concat(output, "\n"),
|
|
11913
|
+
truncated = truncated,
|
|
11914
|
+
}
|
|
11915
|
+
end
|
|
11916
|
+
--[[
|
|
11917
|
+
*
|
|
11918
|
+
* Resolves every edit against the same source, rejects overlaps, and builds
|
|
11919
|
+
* the edited source in memory. Nothing is written: the caller applies
|
|
11920
|
+
* newSource in one compare-and-set write, so a bad edit changes nothing.
|
|
11921
|
+
* line_range anchors refer to the source as it was before any edit.
|
|
11922
|
+
|
|
11923
|
+
]]
|
|
11924
|
+
local function planScriptEdits(source, edits, includeDiff)
|
|
11925
|
+
if #edits == 0 then
|
|
11926
|
+
return {
|
|
11927
|
+
ok = false,
|
|
11928
|
+
error = "At least one edit is required.",
|
|
11929
|
+
}
|
|
11930
|
+
end
|
|
11931
|
+
local starts = lineStarts(source)
|
|
11932
|
+
local resolved = {}
|
|
11933
|
+
for index = 0, #edits - 1 do
|
|
11934
|
+
local edit = edits[index + 1]
|
|
11935
|
+
local label = if #edits > 1 then `edits[{index}]: ` else ""
|
|
11936
|
+
local located = locateEdit(source, starts, edit, label)
|
|
11937
|
+
if type(located) == "string" then
|
|
11938
|
+
return {
|
|
11939
|
+
ok = false,
|
|
11940
|
+
error = located,
|
|
11941
|
+
}
|
|
11942
|
+
end
|
|
11943
|
+
local _arg0 = {
|
|
11944
|
+
index = index,
|
|
11945
|
+
start = located,
|
|
11946
|
+
finish = located + #edit.oldString,
|
|
11947
|
+
newString = edit.newString,
|
|
11948
|
+
}
|
|
11949
|
+
table.insert(resolved, _arg0)
|
|
11950
|
+
-- Insertion sort by position; k is small and ties keep request order.
|
|
11951
|
+
do
|
|
11952
|
+
local at = #resolved - 1
|
|
11953
|
+
local _shouldIncrement = false
|
|
11954
|
+
while true do
|
|
11955
|
+
if _shouldIncrement then
|
|
11956
|
+
at -= 1
|
|
11957
|
+
else
|
|
11958
|
+
_shouldIncrement = true
|
|
11959
|
+
end
|
|
11960
|
+
if not (at > 0 and resolved[at].start > resolved[at + 1].start) then
|
|
11961
|
+
break
|
|
11962
|
+
end
|
|
11963
|
+
local swap = resolved[at]
|
|
11964
|
+
resolved[at] = resolved[at + 1]
|
|
11965
|
+
resolved[at + 1] = swap
|
|
11966
|
+
end
|
|
11967
|
+
end
|
|
11968
|
+
end
|
|
11969
|
+
for at = 1, #resolved - 1 do
|
|
11970
|
+
local previous = resolved[at]
|
|
11971
|
+
local current = resolved[at + 1]
|
|
11972
|
+
if current.start < previous.finish or current.start == previous.start then
|
|
11973
|
+
local first = math.min(previous.index, current.index)
|
|
11974
|
+
local second = math.max(previous.index, current.index)
|
|
11975
|
+
return {
|
|
11976
|
+
ok = false,
|
|
11977
|
+
error = `edits[{first}] and edits[{second}] overlap at line {lineAt(starts, current.start)}; combine them into one edit.`,
|
|
11978
|
+
}
|
|
11979
|
+
end
|
|
11980
|
+
end
|
|
11981
|
+
local newSource = applyWithin(source, resolved, 0, #resolved, 1, #source + 1)
|
|
11982
|
+
if not includeDiff then
|
|
11983
|
+
return {
|
|
11984
|
+
ok = true,
|
|
11985
|
+
newSource = newSource,
|
|
11986
|
+
diff = "",
|
|
11987
|
+
diffTruncated = false,
|
|
11988
|
+
}
|
|
11989
|
+
end
|
|
11990
|
+
local built = buildDiff(source, starts, resolved)
|
|
11991
|
+
return {
|
|
11992
|
+
ok = true,
|
|
11993
|
+
newSource = newSource,
|
|
11994
|
+
diff = built.diff,
|
|
11995
|
+
diffTruncated = built.truncated,
|
|
11996
|
+
}
|
|
11997
|
+
end
|
|
11998
|
+
return {
|
|
11999
|
+
sourceRevision = sourceRevision,
|
|
12000
|
+
planScriptEdits = planScriptEdits,
|
|
12001
|
+
}
|
|
12002
|
+
]]></string>
|
|
12003
|
+
</Properties>
|
|
12004
|
+
</Item>
|
|
12005
|
+
<Item class="ModuleScript" referent="38">
|
|
11055
12006
|
<Properties>
|
|
11056
12007
|
<string name="Name">ScriptSearch</string>
|
|
11057
12008
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -11240,6 +12191,18 @@ local function createScriptSearch(corpus)
|
|
|
11240
12191
|
error = `Path not found: {searchPath}`,
|
|
11241
12192
|
}
|
|
11242
12193
|
end
|
|
12194
|
+
local _self = corpus
|
|
12195
|
+
local _result_1 = _self.createAmbiguityChecker
|
|
12196
|
+
if _result_1 ~= nil then
|
|
12197
|
+
_result_1 = _result_1(_self)
|
|
12198
|
+
end
|
|
12199
|
+
local _condition_6 = _result_1
|
|
12200
|
+
if _condition_6 == nil then
|
|
12201
|
+
_condition_6 = (function()
|
|
12202
|
+
return false
|
|
12203
|
+
end)
|
|
12204
|
+
end
|
|
12205
|
+
local isAmbiguous = _condition_6
|
|
11243
12206
|
local searchPattern = if caseSensitive then pattern else string.lower(pattern)
|
|
11244
12207
|
local patternAlternatives = if usePattern then splitLuaAlternation(searchPattern) else nil
|
|
11245
12208
|
local results = {}
|
|
@@ -11251,10 +12214,10 @@ local function createScriptSearch(corpus)
|
|
|
11251
12214
|
control:checkpoint()
|
|
11252
12215
|
-- ▼ Array.pop ▼
|
|
11253
12216
|
local _length = #stack
|
|
11254
|
-
local
|
|
12217
|
+
local _result_2 = stack[_length]
|
|
11255
12218
|
stack[_length] = nil
|
|
11256
12219
|
-- ▲ Array.pop ▲
|
|
11257
|
-
local instance =
|
|
12220
|
+
local instance = _result_2
|
|
11258
12221
|
local snapshot = corpus:readScript(instance, classFilter)
|
|
11259
12222
|
if snapshot ~= nil and (classFilter == nil or snapshot.className == classFilter) then
|
|
11260
12223
|
scriptsSearched += 1
|
|
@@ -11339,6 +12302,9 @@ local function createScriptSearch(corpus)
|
|
|
11339
12302
|
if snapshot.enabled ~= nil then
|
|
11340
12303
|
scriptResult.enabled = snapshot.enabled
|
|
11341
12304
|
end
|
|
12305
|
+
if isAmbiguous(instance) then
|
|
12306
|
+
scriptResult.ambiguous = true
|
|
12307
|
+
end
|
|
11342
12308
|
table.insert(results, scriptResult)
|
|
11343
12309
|
end
|
|
11344
12310
|
end
|
|
@@ -11378,7 +12344,7 @@ return {
|
|
|
11378
12344
|
]]></string>
|
|
11379
12345
|
</Properties>
|
|
11380
12346
|
</Item>
|
|
11381
|
-
<Item class="ModuleScript" referent="
|
|
12347
|
+
<Item class="ModuleScript" referent="39">
|
|
11382
12348
|
<Properties>
|
|
11383
12349
|
<string name="Name">ServerUrlSettings</string>
|
|
11384
12350
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -11492,11 +12458,11 @@ return {
|
|
|
11492
12458
|
]]></string>
|
|
11493
12459
|
</Properties>
|
|
11494
12460
|
</Item>
|
|
11495
|
-
<Item class="ModuleScript" referent="
|
|
12461
|
+
<Item class="ModuleScript" referent="40">
|
|
11496
12462
|
<Properties>
|
|
11497
12463
|
<string name="Name">State</string>
|
|
11498
12464
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
11499
|
-
local CURRENT_VERSION = "3.
|
|
12465
|
+
local CURRENT_VERSION = "3.3.0"
|
|
11500
12466
|
local PLUGIN_VARIANT = "inspector"
|
|
11501
12467
|
local BASE_PORT = 58741
|
|
11502
12468
|
local SENSITIVE_DIAGNOSTIC_TERMS = { "token", "authorization", "bearer", "password", "cookie", "secret" }
|
|
@@ -11634,7 +12600,7 @@ return {
|
|
|
11634
12600
|
]]></string>
|
|
11635
12601
|
</Properties>
|
|
11636
12602
|
</Item>
|
|
11637
|
-
<Item class="ModuleScript" referent="
|
|
12603
|
+
<Item class="ModuleScript" referent="41">
|
|
11638
12604
|
<Properties>
|
|
11639
12605
|
<string name="Name">StopPlayMonitor</string>
|
|
11640
12606
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -11920,7 +12886,7 @@ return {
|
|
|
11920
12886
|
]]></string>
|
|
11921
12887
|
</Properties>
|
|
11922
12888
|
</Item>
|
|
11923
|
-
<Item class="ModuleScript" referent="
|
|
12889
|
+
<Item class="ModuleScript" referent="42">
|
|
11924
12890
|
<Properties>
|
|
11925
12891
|
<string name="Name">StudioWebSocket</string>
|
|
11926
12892
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -11943,6 +12909,10 @@ local MAX_PENDING_RESPONSE_BYTES = 64 * 1024 * 1024
|
|
|
11943
12909
|
local RESERVED_ERROR_BYTES = 4096
|
|
11944
12910
|
-- The bridge admits 128 UTF-16 code units; each can require three UTF-8 bytes.
|
|
11945
12911
|
local MAX_REQUEST_ID_BYTES = 128 * 3
|
|
12912
|
+
-- Upper bound on BindToClose waiting for the server to close the socket after
|
|
12913
|
+
-- the unregister frame; it normally closes within milliseconds.
|
|
12914
|
+
local SHUTDOWN_UNREGISTER_WAIT_SECONDS = 2
|
|
12915
|
+
local SHUTDOWN_UNREGISTER_POLL_SECONDS = 0.05
|
|
11946
12916
|
local options
|
|
11947
12917
|
local active = false
|
|
11948
12918
|
local shutdownSuspended = false
|
|
@@ -12985,8 +13955,13 @@ local function refresh()
|
|
|
12985
13955
|
refreshRequested = true
|
|
12986
13956
|
flushRefresh()
|
|
12987
13957
|
end
|
|
12988
|
-
-- EndTest may tear down this VM without an Unloading callback
|
|
12989
|
-
--
|
|
13958
|
+
-- EndTest may tear down this VM without an Unloading callback, and BindToClose
|
|
13959
|
+
-- waits for this function. It unregisters on the socket itself: Studio runs at
|
|
13960
|
+
-- most five plugin HTTP requests at a time across the whole process, with no
|
|
13961
|
+
-- time limit, so a /disconnect queued behind five that never finish held
|
|
13962
|
+
-- shutdown until Studio's 30 s deadline. Closing right after Send can drop the
|
|
13963
|
+
-- frame, so it waits briefly for the server to close the socket first. A
|
|
13964
|
+
-- failed EndTest can resume and registers again.
|
|
12990
13965
|
local function suspendForShutdown()
|
|
12991
13966
|
local currentOptions = options
|
|
12992
13967
|
if not active or currentOptions == nil then
|
|
@@ -12998,9 +13973,34 @@ local function suspendForShutdown()
|
|
|
12998
13973
|
cancelTransportWork()
|
|
12999
13974
|
refreshRequested = false
|
|
13000
13975
|
reconnectAttempt = 0
|
|
13976
|
+
local client = socketClient
|
|
13977
|
+
local unregistered = false
|
|
13978
|
+
if client ~= nil and socketOpen then
|
|
13979
|
+
local serverClosed = false
|
|
13980
|
+
local closedConnection = client.Closed:Connect(function()
|
|
13981
|
+
serverClosed = true
|
|
13982
|
+
end)
|
|
13983
|
+
local sent = pcall(function()
|
|
13984
|
+
return client:Send(HttpService:JSONEncode({
|
|
13985
|
+
kind = "disconnect",
|
|
13986
|
+
}))
|
|
13987
|
+
end)
|
|
13988
|
+
unregistered = sent
|
|
13989
|
+
local deadline = os.clock() + SHUTDOWN_UNREGISTER_WAIT_SECONDS
|
|
13990
|
+
while sent and not serverClosed and os.clock() < deadline do
|
|
13991
|
+
task.wait(SHUTDOWN_UNREGISTER_POLL_SECONDS)
|
|
13992
|
+
end
|
|
13993
|
+
closedConnection:Disconnect()
|
|
13994
|
+
end
|
|
13001
13995
|
closeCurrentSocket()
|
|
13002
13996
|
cachedReady = nil
|
|
13003
|
-
|
|
13997
|
+
-- Without an open socket, unregister over HTTP on another thread rather
|
|
13998
|
+
-- than wait; the server also drops a peer whose socket stays closed.
|
|
13999
|
+
if not unregistered then
|
|
14000
|
+
task.spawn(function()
|
|
14001
|
+
return disconnectSession(currentOptions)
|
|
14002
|
+
end)
|
|
14003
|
+
end
|
|
13004
14004
|
end
|
|
13005
14005
|
local function resumeAfterShutdownFailure()
|
|
13006
14006
|
if not shutdownSuspended or options == nil then
|
|
@@ -13044,7 +14044,7 @@ return {
|
|
|
13044
14044
|
]]></string>
|
|
13045
14045
|
</Properties>
|
|
13046
14046
|
</Item>
|
|
13047
|
-
<Item class="ModuleScript" referent="
|
|
14047
|
+
<Item class="ModuleScript" referent="43">
|
|
13048
14048
|
<Properties>
|
|
13049
14049
|
<string name="Name">TopologyId</string>
|
|
13050
14050
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -13089,7 +14089,7 @@ return {
|
|
|
13089
14089
|
]]></string>
|
|
13090
14090
|
</Properties>
|
|
13091
14091
|
</Item>
|
|
13092
|
-
<Item class="ModuleScript" referent="
|
|
14092
|
+
<Item class="ModuleScript" referent="44">
|
|
13093
14093
|
<Properties>
|
|
13094
14094
|
<string name="Name">UI</string>
|
|
13095
14095
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -13614,10 +14614,12 @@ return {
|
|
|
13614
14614
|
]]></string>
|
|
13615
14615
|
</Properties>
|
|
13616
14616
|
</Item>
|
|
13617
|
-
<Item class="ModuleScript" referent="
|
|
14617
|
+
<Item class="ModuleScript" referent="45">
|
|
13618
14618
|
<Properties>
|
|
13619
14619
|
<string name="Name">Utils</string>
|
|
13620
14620
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
14621
|
+
local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
14622
|
+
local PathAmbiguity = TS.import(script, script.Parent, "PathAmbiguity")
|
|
13621
14623
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
13622
14624
|
local LUAU_KEYWORDS = {
|
|
13623
14625
|
["and"] = true,
|
|
@@ -13832,16 +14834,17 @@ local function parseInstancePath(path)
|
|
|
13832
14834
|
end
|
|
13833
14835
|
return parts
|
|
13834
14836
|
end
|
|
14837
|
+
local function getService(segment)
|
|
14838
|
+
local ok, service = pcall(function()
|
|
14839
|
+
return game:GetService(segment)
|
|
14840
|
+
end)
|
|
14841
|
+
return if ok and service then service else nil
|
|
14842
|
+
end
|
|
14843
|
+
local function isRootService(instance)
|
|
14844
|
+
return instance.Parent == game and getService(instance.ClassName) == instance
|
|
14845
|
+
end
|
|
13835
14846
|
local function getRootSegment(instance)
|
|
13836
|
-
if instance.
|
|
13837
|
-
local ok, service = pcall(function()
|
|
13838
|
-
return game:GetService(instance.ClassName)
|
|
13839
|
-
end)
|
|
13840
|
-
if ok and service == instance then
|
|
13841
|
-
return instance.ClassName
|
|
13842
|
-
end
|
|
13843
|
-
end
|
|
13844
|
-
return instance.Name
|
|
14847
|
+
return if isRootService(instance) then instance.ClassName else instance.Name
|
|
13845
14848
|
end
|
|
13846
14849
|
local function getInstancePath(instance)
|
|
13847
14850
|
if not instance or instance == game then
|
|
@@ -13865,13 +14868,7 @@ local function getInstancePath(instance)
|
|
|
13865
14868
|
return path
|
|
13866
14869
|
end
|
|
13867
14870
|
local function getRootInstance(segment)
|
|
13868
|
-
|
|
13869
|
-
return game:GetService(segment)
|
|
13870
|
-
end)
|
|
13871
|
-
if ok and service then
|
|
13872
|
-
return service
|
|
13873
|
-
end
|
|
13874
|
-
return game:FindFirstChild(segment)
|
|
14871
|
+
return getService(segment) or game:FindFirstChild(segment)
|
|
13875
14872
|
end
|
|
13876
14873
|
local function getInstanceByPath(path)
|
|
13877
14874
|
local parts = parseInstancePath(path)
|
|
@@ -13891,6 +14888,36 @@ local function getInstanceByPath(path)
|
|
|
13891
14888
|
end
|
|
13892
14889
|
return current
|
|
13893
14890
|
end
|
|
14891
|
+
local instancePathTree = {
|
|
14892
|
+
root = game,
|
|
14893
|
+
getParent = function(instance)
|
|
14894
|
+
return instance.Parent
|
|
14895
|
+
end,
|
|
14896
|
+
getChildren = function(instance)
|
|
14897
|
+
return instance:GetChildren()
|
|
14898
|
+
end,
|
|
14899
|
+
getName = function(instance)
|
|
14900
|
+
return instance.Name
|
|
14901
|
+
end,
|
|
14902
|
+
getService = getService,
|
|
14903
|
+
isRootService = isRootService,
|
|
14904
|
+
getPath = getInstancePath,
|
|
14905
|
+
}
|
|
14906
|
+
-- Like getInstanceByPath, but also reports the first segment that names more
|
|
14907
|
+
-- than one sibling. Costs one child scan per segment. PathGuard applies it to
|
|
14908
|
+
-- every declared request path before the handler runs.
|
|
14909
|
+
local function resolveInstancePath(path)
|
|
14910
|
+
local parts = parseInstancePath(path)
|
|
14911
|
+
if parts == nil then
|
|
14912
|
+
return {}
|
|
14913
|
+
end
|
|
14914
|
+
return PathAmbiguity.resolvePath(instancePathTree, parts)
|
|
14915
|
+
end
|
|
14916
|
+
-- Returns a per-request predicate that is true when getInstancePath(instance)
|
|
14917
|
+
-- does not identify exactly that instance.
|
|
14918
|
+
local function createPathAmbiguityChecker()
|
|
14919
|
+
return PathAmbiguity.createAmbiguityChecker(instancePathTree)
|
|
14920
|
+
end
|
|
13894
14921
|
local function splitLines(source)
|
|
13895
14922
|
local _condition = source
|
|
13896
14923
|
if _condition == nil then
|
|
@@ -14445,6 +15472,8 @@ return {
|
|
|
14445
15472
|
safeCall = safeCall,
|
|
14446
15473
|
getInstancePath = getInstancePath,
|
|
14447
15474
|
getInstanceByPath = getInstanceByPath,
|
|
15475
|
+
resolveInstancePath = resolveInstancePath,
|
|
15476
|
+
createPathAmbiguityChecker = createPathAmbiguityChecker,
|
|
14448
15477
|
splitLines = splitLines,
|
|
14449
15478
|
joinLines = joinLines,
|
|
14450
15479
|
readScriptSource = readScriptSource,
|
|
@@ -14457,11 +15486,11 @@ return {
|
|
|
14457
15486
|
</Properties>
|
|
14458
15487
|
</Item>
|
|
14459
15488
|
</Item>
|
|
14460
|
-
<Item class="Folder" referent="
|
|
15489
|
+
<Item class="Folder" referent="50">
|
|
14461
15490
|
<Properties>
|
|
14462
15491
|
<string name="Name">include</string>
|
|
14463
15492
|
</Properties>
|
|
14464
|
-
<Item class="ModuleScript" referent="
|
|
15493
|
+
<Item class="ModuleScript" referent="46">
|
|
14465
15494
|
<Properties>
|
|
14466
15495
|
<string name="Name">LibMP</string>
|
|
14467
15496
|
<string name="Source"><![CDATA[-- =============================================================================
|
|
@@ -170845,7 +171874,7 @@ return LibMP
|
|
|
170845
171874
|
]]></string>
|
|
170846
171875
|
</Properties>
|
|
170847
171876
|
</Item>
|
|
170848
|
-
<Item class="ModuleScript" referent="
|
|
171877
|
+
<Item class="ModuleScript" referent="47">
|
|
170849
171878
|
<Properties>
|
|
170850
171879
|
<string name="Name">Promise</string>
|
|
170851
171880
|
<string name="Source"><![CDATA[--[[
|
|
@@ -172919,7 +173948,7 @@ return Promise
|
|
|
172919
173948
|
]]></string>
|
|
172920
173949
|
</Properties>
|
|
172921
173950
|
</Item>
|
|
172922
|
-
<Item class="ModuleScript" referent="
|
|
173951
|
+
<Item class="ModuleScript" referent="48">
|
|
172923
173952
|
<Properties>
|
|
172924
173953
|
<string name="Name">RuntimeLib</string>
|
|
172925
173954
|
<string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
|
|
@@ -173186,15 +174215,15 @@ return TS
|
|
|
173186
174215
|
</Properties>
|
|
173187
174216
|
</Item>
|
|
173188
174217
|
</Item>
|
|
173189
|
-
<Item class="Folder" referent="
|
|
174218
|
+
<Item class="Folder" referent="51">
|
|
173190
174219
|
<Properties>
|
|
173191
174220
|
<string name="Name">node_modules</string>
|
|
173192
174221
|
</Properties>
|
|
173193
|
-
<Item class="Folder" referent="
|
|
174222
|
+
<Item class="Folder" referent="52">
|
|
173194
174223
|
<Properties>
|
|
173195
174224
|
<string name="Name">@rbxts</string>
|
|
173196
174225
|
</Properties>
|
|
173197
|
-
<Item class="ModuleScript" referent="
|
|
174226
|
+
<Item class="ModuleScript" referent="49">
|
|
173198
174227
|
<Properties>
|
|
173199
174228
|
<string name="Name">services</string>
|
|
173200
174229
|
<string name="Source"><![CDATA[return setmetatable({}, {
|