@chrrxs/robloxstudio-mcp 3.2.1 → 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 +12812 -12468
- package/package.json +1 -1
- package/studio-plugin/MCPPlugin.rbxmx +1143 -181
|
@@ -1471,6 +1471,7 @@ local ClientBroker = TS.import(script, script.Parent, "ClientBroker")
|
|
|
1471
1471
|
local ServerUrlSettings = TS.import(script, script.Parent, "ServerUrlSettings")
|
|
1472
1472
|
local PluginSession = TS.import(script, script.Parent, "PluginSession")
|
|
1473
1473
|
local StudioWebSocket = TS.import(script, script.Parent, "StudioWebSocket")
|
|
1474
|
+
local PathGuard = TS.import(script, script.Parent, "PathGuard")
|
|
1474
1475
|
local assignedRole
|
|
1475
1476
|
local lastReadyPlaceKey
|
|
1476
1477
|
local initialRole = PluginSession.getRole()
|
|
@@ -1523,12 +1524,17 @@ local routeMap = {
|
|
|
1523
1524
|
["/api/get-memory-breakdown"] = MemoryHandlers.getMemoryBreakdown,
|
|
1524
1525
|
["/api/get-scene-analysis"] = SceneAnalysisHandlers.getSceneAnalysis,
|
|
1525
1526
|
}
|
|
1527
|
+
local pathGuard = PathGuard.createPathGuard(function(path)
|
|
1528
|
+
return Utils.resolveInstancePath(path).ambiguity
|
|
1529
|
+
end)
|
|
1526
1530
|
local function processRequest(request, context)
|
|
1527
1531
|
local endpoint = request.endpoint
|
|
1528
|
-
local data = request.data or {}
|
|
1532
|
+
local data = (request.data or {})
|
|
1529
1533
|
local handler = routeMap[endpoint]
|
|
1530
1534
|
if handler then
|
|
1531
|
-
return
|
|
1535
|
+
return pathGuard.run(endpoint, data, function()
|
|
1536
|
+
return handler(data, context)
|
|
1537
|
+
end)
|
|
1532
1538
|
else
|
|
1533
1539
|
return {
|
|
1534
1540
|
error = `Unknown endpoint: {endpoint}`,
|
|
@@ -1975,9 +1981,9 @@ local function computeBridgeStamp()
|
|
|
1975
1981
|
for i = 1, #combined do
|
|
1976
1982
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1977
1983
|
end
|
|
1978
|
-
-- "3.
|
|
1984
|
+
-- "3.3.0" is replaced with the package version at package time
|
|
1979
1985
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1980
|
-
return `{tostring(h)}-3.
|
|
1986
|
+
return `{tostring(h)}-3.3.0`
|
|
1981
1987
|
end
|
|
1982
1988
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1983
1989
|
local function setSource(scriptInst, source)
|
|
@@ -4213,15 +4219,20 @@ local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
|
4213
4219
|
-- capability and drives the REAL input pipeline: SendKey feeds
|
|
4214
4220
|
-- UserInputService.InputBegan/Ended and the control modules (so WASD walks the
|
|
4215
4221
|
-- character at full WalkSpeed with controls intact, no Humanoid hijack),
|
|
4216
|
-
-- SendMouseButton feeds UIS and activates GUI buttons
|
|
4217
|
-
--
|
|
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.
|
|
4218
4231
|
--
|
|
4219
|
-
--
|
|
4220
|
-
--
|
|
4221
|
-
--
|
|
4222
|
-
--
|
|
4223
|
-
-- There is NO SendMouseMove / SendMouseWheel / SendKeyEvent — so "move" and
|
|
4224
|
-
-- "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.
|
|
4225
4236
|
--
|
|
4226
4237
|
-- Coordinate space: SendMouseButton coordinates are viewport pixels matching
|
|
4227
4238
|
-- what capture_screenshot returns (window space, origin at the top-left of the
|
|
@@ -4254,6 +4265,7 @@ local MOUSE_TYPE_MAP = {
|
|
|
4254
4265
|
Right = Enum.UserInputType.MouseButton2,
|
|
4255
4266
|
Middle = Enum.UserInputType.MouseButton3,
|
|
4256
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."
|
|
4257
4269
|
local function simulateMouseInput(requestData)
|
|
4258
4270
|
local action = requestData.action
|
|
4259
4271
|
local x = requestData.x
|
|
@@ -4263,6 +4275,7 @@ local function simulateMouseInput(requestData)
|
|
|
4263
4275
|
_condition = "Left"
|
|
4264
4276
|
end
|
|
4265
4277
|
local button = _condition
|
|
4278
|
+
local wheel = requestData.wheel
|
|
4266
4279
|
if not (action ~= "" and action) then
|
|
4267
4280
|
return {
|
|
4268
4281
|
error = "action is required",
|
|
@@ -4273,6 +4286,11 @@ local function simulateMouseInput(requestData)
|
|
|
4273
4286
|
error = "x and y are required",
|
|
4274
4287
|
}
|
|
4275
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
|
|
4276
4294
|
-- Input is silently dropped by the engine when the window isn't rendering
|
|
4277
4295
|
-- (e.g. minimized). Surface that instead of returning a false success.
|
|
4278
4296
|
local notRendering = RenderMonitor.notRenderingReason()
|
|
@@ -4281,6 +4299,11 @@ local function simulateMouseInput(requestData)
|
|
|
4281
4299
|
error = notRendering,
|
|
4282
4300
|
}
|
|
4283
4301
|
end
|
|
4302
|
+
if action == "move" and not UserInputService.MouseEnabled then
|
|
4303
|
+
return {
|
|
4304
|
+
error = NO_MOUSE_ERROR,
|
|
4305
|
+
}
|
|
4306
|
+
end
|
|
4284
4307
|
local vi = getVI()
|
|
4285
4308
|
if not vi then
|
|
4286
4309
|
return {
|
|
@@ -4298,18 +4321,29 @@ local function simulateMouseInput(requestData)
|
|
|
4298
4321
|
vi:SendMouseButton(pos, inputType, true)
|
|
4299
4322
|
elseif action == "mouseUp" then
|
|
4300
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
|
+
})
|
|
4301
4330
|
else
|
|
4302
|
-
error(`Unsupported action "{action}".
|
|
4331
|
+
error(`Unsupported action "{action}". Use click, mouseDown, mouseUp, move, or scroll.`)
|
|
4303
4332
|
end
|
|
4304
4333
|
end)
|
|
4305
4334
|
if success then
|
|
4306
|
-
|
|
4335
|
+
local result = {
|
|
4307
4336
|
success = true,
|
|
4308
4337
|
action = action,
|
|
4309
4338
|
x = x,
|
|
4310
4339
|
y = y,
|
|
4311
|
-
button = button,
|
|
4312
4340
|
}
|
|
4341
|
+
if action == "scroll" then
|
|
4342
|
+
result.wheel = wheel
|
|
4343
|
+
elseif action ~= "move" then
|
|
4344
|
+
result.button = button
|
|
4345
|
+
end
|
|
4346
|
+
return result
|
|
4313
4347
|
end
|
|
4314
4348
|
return {
|
|
4315
4349
|
error = `Failed to simulate mouse input: {err}`,
|
|
@@ -4500,6 +4534,7 @@ local Selection = game:GetService("Selection")
|
|
|
4500
4534
|
local _binding = Utils
|
|
4501
4535
|
local getInstancePath = _binding.getInstancePath
|
|
4502
4536
|
local getInstanceByPath = _binding.getInstanceByPath
|
|
4537
|
+
local createPathAmbiguityChecker = _binding.createPathAmbiguityChecker
|
|
4503
4538
|
local function serializeValue(value)
|
|
4504
4539
|
local _value = value
|
|
4505
4540
|
local vType = typeof(_value)
|
|
@@ -4601,15 +4636,20 @@ local function getSelection(_requestData)
|
|
|
4601
4636
|
message = "No objects selected",
|
|
4602
4637
|
}
|
|
4603
4638
|
end
|
|
4639
|
+
local isAmbiguous = createPathAmbiguityChecker()
|
|
4604
4640
|
-- ▼ ReadonlyArray.map ▼
|
|
4605
4641
|
local _newValue = table.create(#selection)
|
|
4606
4642
|
local _callback = function(instance)
|
|
4607
|
-
|
|
4643
|
+
local entry = {
|
|
4608
4644
|
name = instance.Name,
|
|
4609
4645
|
className = instance.ClassName,
|
|
4610
4646
|
path = getInstancePath(instance),
|
|
4611
4647
|
parent = if instance.Parent then getInstancePath(instance.Parent) else nil,
|
|
4612
4648
|
}
|
|
4649
|
+
if isAmbiguous(instance) then
|
|
4650
|
+
entry.ambiguous = true
|
|
4651
|
+
end
|
|
4652
|
+
return entry
|
|
4613
4653
|
end
|
|
4614
4654
|
for _k, _v in selection do
|
|
4615
4655
|
_newValue[_k] = _callback(_v, _k - 1, selection)
|
|
@@ -6748,9 +6788,11 @@ local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
|
6748
6788
|
local Utils = TS.import(script, script.Parent.Parent, "Utils")
|
|
6749
6789
|
local CooperativeJobRunner = TS.import(script, script.Parent.Parent, "CooperativeJobRunner")
|
|
6750
6790
|
local ScriptSearch = TS.import(script, script.Parent.Parent, "ScriptSearch")
|
|
6791
|
+
local InstanceSearch = TS.import(script, script.Parent.Parent, "InstanceSearch")
|
|
6751
6792
|
local _binding = Utils
|
|
6752
6793
|
local getInstancePath = _binding.getInstancePath
|
|
6753
6794
|
local getInstanceByPath = _binding.getInstanceByPath
|
|
6795
|
+
local createPathAmbiguityChecker = _binding.createPathAmbiguityChecker
|
|
6754
6796
|
local readScriptSource = _binding.readScriptSource
|
|
6755
6797
|
local function getFileTree(requestData)
|
|
6756
6798
|
local _condition = (requestData.path)
|
|
@@ -6876,62 +6918,42 @@ local function getPlaceInfo(_requestData)
|
|
|
6876
6918
|
},
|
|
6877
6919
|
}
|
|
6878
6920
|
end
|
|
6879
|
-
local
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
return
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
local
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
end
|
|
6914
|
-
end
|
|
6915
|
-
end
|
|
6916
|
-
if match then
|
|
6917
|
-
local _arg0 = {
|
|
6918
|
-
name = instance.Name,
|
|
6919
|
-
className = instance.ClassName,
|
|
6920
|
-
path = getInstancePath(instance),
|
|
6921
|
-
}
|
|
6922
|
-
table.insert(results, _arg0)
|
|
6923
|
-
end
|
|
6924
|
-
for _, child in instance:GetChildren() do
|
|
6925
|
-
searchRecursive(child)
|
|
6926
|
-
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
|
|
6927
6955
|
end
|
|
6928
|
-
|
|
6929
|
-
return {
|
|
6930
|
-
results = results,
|
|
6931
|
-
query = query,
|
|
6932
|
-
searchType = searchType,
|
|
6933
|
-
count = #results,
|
|
6934
|
-
}
|
|
6956
|
+
return result
|
|
6935
6957
|
end
|
|
6936
6958
|
local function getInstanceProperties(requestData)
|
|
6937
6959
|
local instancePath = requestData.instancePath
|
|
@@ -7199,9 +7221,10 @@ local function getProjectStructure(requestData)
|
|
|
7199
7221
|
error = `Path not found: {startPath}`,
|
|
7200
7222
|
}
|
|
7201
7223
|
end
|
|
7224
|
+
local isAmbiguous = createPathAmbiguityChecker()
|
|
7202
7225
|
local function getStructure(instance, depth)
|
|
7203
7226
|
if depth > maxDepth then
|
|
7204
|
-
|
|
7227
|
+
local truncatedNode = {
|
|
7205
7228
|
name = instance.Name,
|
|
7206
7229
|
className = instance.ClassName,
|
|
7207
7230
|
path = getInstancePath(instance),
|
|
@@ -7209,12 +7232,19 @@ local function getProjectStructure(requestData)
|
|
|
7209
7232
|
hasMore = true,
|
|
7210
7233
|
note = "Max depth reached - use this path to explore further",
|
|
7211
7234
|
}
|
|
7235
|
+
if isAmbiguous(instance) then
|
|
7236
|
+
truncatedNode.ambiguous = true
|
|
7237
|
+
end
|
|
7238
|
+
return truncatedNode
|
|
7212
7239
|
end
|
|
7213
7240
|
local node = {
|
|
7214
7241
|
name = instance.Name,
|
|
7215
7242
|
className = instance.ClassName,
|
|
7216
7243
|
path = getInstancePath(instance),
|
|
7217
7244
|
}
|
|
7245
|
+
if isAmbiguous(instance) then
|
|
7246
|
+
node.ambiguous = true
|
|
7247
|
+
end
|
|
7218
7248
|
if instance:IsA("LuaSourceContainer") then
|
|
7219
7249
|
node.hasSource = true
|
|
7220
7250
|
node.scriptType = instance.ClassName
|
|
@@ -7344,6 +7374,9 @@ local scriptSearch = ScriptSearch.createScriptSearch({
|
|
|
7344
7374
|
resolveRoot = function(self, path)
|
|
7345
7375
|
return getInstanceByPath(path)
|
|
7346
7376
|
end,
|
|
7377
|
+
createAmbiguityChecker = function(self)
|
|
7378
|
+
return createPathAmbiguityChecker()
|
|
7379
|
+
end,
|
|
7347
7380
|
getChildren = function(self, instance)
|
|
7348
7381
|
return instance:GetChildren()
|
|
7349
7382
|
end,
|
|
@@ -7651,6 +7684,9 @@ return {
|
|
|
7651
7684
|
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
7652
7685
|
local Utils = TS.import(script, script.Parent.Parent, "Utils")
|
|
7653
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
|
|
7654
7690
|
local _binding = Utils
|
|
7655
7691
|
local getInstancePath = _binding.getInstancePath
|
|
7656
7692
|
local getInstanceByPath = _binding.getInstanceByPath
|
|
@@ -7766,6 +7802,7 @@ local function getScriptSource(requestData)
|
|
|
7766
7802
|
numberedSource = numberLines(selectedLines, returnedStartLine),
|
|
7767
7803
|
sourceLength = #fullSource,
|
|
7768
7804
|
lineCount = totalLineCount,
|
|
7805
|
+
revision = sourceRevision(fullSource),
|
|
7769
7806
|
startLine = returnedStartLine,
|
|
7770
7807
|
endLine = returnedEndLine,
|
|
7771
7808
|
isPartial = explicitRange,
|
|
@@ -7837,14 +7874,74 @@ local function setScriptSource(requestData)
|
|
|
7837
7874
|
error = `Failed to set script source: {applyResult.error}`,
|
|
7838
7875
|
}
|
|
7839
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
|
|
7840
7927
|
local function editScriptLines(requestData)
|
|
7841
7928
|
local instancePath = requestData.instancePath
|
|
7842
|
-
local
|
|
7843
|
-
local
|
|
7844
|
-
|
|
7845
|
-
|
|
7929
|
+
local expectedRevision = requestData.expectedRevision
|
|
7930
|
+
local dryRun = requestData.dryRun == true
|
|
7931
|
+
if not (instancePath ~= "" and instancePath) then
|
|
7932
|
+
return {
|
|
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
|
|
7846
7943
|
return {
|
|
7847
|
-
error = "
|
|
7944
|
+
error = "expectedRevision must be a string",
|
|
7848
7945
|
}
|
|
7849
7946
|
end
|
|
7850
7947
|
local instance = getInstanceByPath(instancePath)
|
|
@@ -7858,76 +7955,63 @@ local function editScriptLines(requestData)
|
|
|
7858
7955
|
error = `Instance is not a script-like object: {instance.ClassName}`,
|
|
7859
7956
|
}
|
|
7860
7957
|
end
|
|
7861
|
-
local
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
error(`old_string does not match at line {startLine}. Use get_script_source to verify the exact text at that line.`)
|
|
7883
|
-
end
|
|
7884
|
-
matchStart = lineStartByte
|
|
7885
|
-
else
|
|
7886
|
-
local count = 0
|
|
7887
|
-
local searchPos = 1
|
|
7888
|
-
local firstMatch
|
|
7889
|
-
while true do
|
|
7890
|
-
local foundStart = string.find(source, oldString, searchPos, true)
|
|
7891
|
-
if foundStart == nil then
|
|
7892
|
-
break
|
|
7893
|
-
end
|
|
7894
|
-
if firstMatch == nil then
|
|
7895
|
-
firstMatch = foundStart
|
|
7896
|
-
end
|
|
7897
|
-
count += 1
|
|
7898
|
-
if count > 1 then
|
|
7899
|
-
break
|
|
7900
|
-
end
|
|
7901
|
-
searchPos = foundStart + searchLen
|
|
7902
|
-
end
|
|
7903
|
-
if count == 0 then
|
|
7904
|
-
error("old_string not found in script. If old_string contains repeated patterns (e.g. closing braces), pass startLine to anchor the edit.")
|
|
7905
|
-
end
|
|
7906
|
-
if count > 1 then
|
|
7907
|
-
error("old_string matches multiple locations. Provide more surrounding context, or pass startLine to anchor the edit to a specific line.")
|
|
7908
|
-
end
|
|
7909
|
-
matchStart = firstMatch
|
|
7910
|
-
end
|
|
7911
|
-
-- Byte-slice replacement avoids Lua pattern escaping (safe for multi-byte chars like em dashes).
|
|
7912
|
-
local newSource = string.sub(source, 1, matchStart - 1) .. newString .. string.sub(source, matchStart + searchLen)
|
|
7913
|
-
local applyResult = applyScriptSource(instance, newSource, source)
|
|
7914
|
-
if not applyResult.success then
|
|
7915
|
-
error(applyResult.error)
|
|
7916
|
-
end
|
|
7958
|
+
local readSuccess, readResult = pcall(function()
|
|
7959
|
+
return readScriptSource(instance)
|
|
7960
|
+
end)
|
|
7961
|
+
if not readSuccess then
|
|
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
|
|
7917
7979
|
return {
|
|
7980
|
+
error = `Failed to edit script: {plan.error}`,
|
|
7981
|
+
}
|
|
7982
|
+
end
|
|
7983
|
+
if dryRun then
|
|
7984
|
+
local preview = {
|
|
7918
7985
|
success = true,
|
|
7986
|
+
dryRun = true,
|
|
7919
7987
|
instancePath = instancePath,
|
|
7920
|
-
|
|
7921
|
-
|
|
7988
|
+
editCount = #edits,
|
|
7989
|
+
revision = revision,
|
|
7990
|
+
resultRevision = sourceRevision(plan.newSource),
|
|
7991
|
+
diff = plan.diff,
|
|
7922
7992
|
}
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
return
|
|
7993
|
+
if plan.diffTruncated then
|
|
7994
|
+
preview.diffTruncated = true
|
|
7995
|
+
end
|
|
7996
|
+
return preview
|
|
7927
7997
|
end
|
|
7928
|
-
|
|
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)
|
|
7929
8008
|
return {
|
|
7930
|
-
|
|
8009
|
+
success = true,
|
|
8010
|
+
instancePath = instancePath,
|
|
8011
|
+
editCount = #edits,
|
|
8012
|
+
revision = sourceRevision(plan.newSource),
|
|
8013
|
+
method = applyResult.method,
|
|
8014
|
+
message = "Script edited successfully",
|
|
7931
8015
|
}
|
|
7932
8016
|
end
|
|
7933
8017
|
local function insertScriptLines(requestData)
|
|
@@ -9448,6 +9532,166 @@ return {
|
|
|
9448
9532
|
</Properties>
|
|
9449
9533
|
</Item>
|
|
9450
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">
|
|
9451
9695
|
<Properties>
|
|
9452
9696
|
<string name="Name">LuauExec</string>
|
|
9453
9697
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -9881,7 +10125,331 @@ return {
|
|
|
9881
10125
|
]]></string>
|
|
9882
10126
|
</Properties>
|
|
9883
10127
|
</Item>
|
|
9884
|
-
<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">
|
|
9885
10453
|
<Properties>
|
|
9886
10454
|
<string name="Name">PeerRole</string>
|
|
9887
10455
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -9902,7 +10470,7 @@ return {
|
|
|
9902
10470
|
]]></string>
|
|
9903
10471
|
</Properties>
|
|
9904
10472
|
</Item>
|
|
9905
|
-
<Item class="ModuleScript" referent="
|
|
10473
|
+
<Item class="ModuleScript" referent="32">
|
|
9906
10474
|
<Properties>
|
|
9907
10475
|
<string name="Name">PlaytestTickets</string>
|
|
9908
10476
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -10083,7 +10651,7 @@ return {
|
|
|
10083
10651
|
]]></string>
|
|
10084
10652
|
</Properties>
|
|
10085
10653
|
</Item>
|
|
10086
|
-
<Item class="ModuleScript" referent="
|
|
10654
|
+
<Item class="ModuleScript" referent="33">
|
|
10087
10655
|
<Properties>
|
|
10088
10656
|
<string name="Name">PluginSession</string>
|
|
10089
10657
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -10750,7 +11318,7 @@ return {
|
|
|
10750
11318
|
]]></string>
|
|
10751
11319
|
</Properties>
|
|
10752
11320
|
</Item>
|
|
10753
|
-
<Item class="ModuleScript" referent="
|
|
11321
|
+
<Item class="ModuleScript" referent="34">
|
|
10754
11322
|
<Properties>
|
|
10755
11323
|
<string name="Name">Recording</string>
|
|
10756
11324
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -10780,7 +11348,7 @@ return {
|
|
|
10780
11348
|
]]></string>
|
|
10781
11349
|
</Properties>
|
|
10782
11350
|
</Item>
|
|
10783
|
-
<Item class="ModuleScript" referent="
|
|
11351
|
+
<Item class="ModuleScript" referent="35">
|
|
10784
11352
|
<Properties>
|
|
10785
11353
|
<string name="Name">RenderMonitor</string>
|
|
10786
11354
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -10848,7 +11416,7 @@ return {
|
|
|
10848
11416
|
]]></string>
|
|
10849
11417
|
</Properties>
|
|
10850
11418
|
</Item>
|
|
10851
|
-
<Item class="ModuleScript" referent="
|
|
11419
|
+
<Item class="ModuleScript" referent="36">
|
|
10852
11420
|
<Properties>
|
|
10853
11421
|
<string name="Name">RuntimeLogBuffer</string>
|
|
10854
11422
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -11084,7 +11652,357 @@ return {
|
|
|
11084
11652
|
]]></string>
|
|
11085
11653
|
</Properties>
|
|
11086
11654
|
</Item>
|
|
11087
|
-
<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">
|
|
11088
12006
|
<Properties>
|
|
11089
12007
|
<string name="Name">ScriptSearch</string>
|
|
11090
12008
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -11273,6 +12191,18 @@ local function createScriptSearch(corpus)
|
|
|
11273
12191
|
error = `Path not found: {searchPath}`,
|
|
11274
12192
|
}
|
|
11275
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
|
|
11276
12206
|
local searchPattern = if caseSensitive then pattern else string.lower(pattern)
|
|
11277
12207
|
local patternAlternatives = if usePattern then splitLuaAlternation(searchPattern) else nil
|
|
11278
12208
|
local results = {}
|
|
@@ -11284,10 +12214,10 @@ local function createScriptSearch(corpus)
|
|
|
11284
12214
|
control:checkpoint()
|
|
11285
12215
|
-- ▼ Array.pop ▼
|
|
11286
12216
|
local _length = #stack
|
|
11287
|
-
local
|
|
12217
|
+
local _result_2 = stack[_length]
|
|
11288
12218
|
stack[_length] = nil
|
|
11289
12219
|
-- ▲ Array.pop ▲
|
|
11290
|
-
local instance =
|
|
12220
|
+
local instance = _result_2
|
|
11291
12221
|
local snapshot = corpus:readScript(instance, classFilter)
|
|
11292
12222
|
if snapshot ~= nil and (classFilter == nil or snapshot.className == classFilter) then
|
|
11293
12223
|
scriptsSearched += 1
|
|
@@ -11372,6 +12302,9 @@ local function createScriptSearch(corpus)
|
|
|
11372
12302
|
if snapshot.enabled ~= nil then
|
|
11373
12303
|
scriptResult.enabled = snapshot.enabled
|
|
11374
12304
|
end
|
|
12305
|
+
if isAmbiguous(instance) then
|
|
12306
|
+
scriptResult.ambiguous = true
|
|
12307
|
+
end
|
|
11375
12308
|
table.insert(results, scriptResult)
|
|
11376
12309
|
end
|
|
11377
12310
|
end
|
|
@@ -11411,7 +12344,7 @@ return {
|
|
|
11411
12344
|
]]></string>
|
|
11412
12345
|
</Properties>
|
|
11413
12346
|
</Item>
|
|
11414
|
-
<Item class="ModuleScript" referent="
|
|
12347
|
+
<Item class="ModuleScript" referent="39">
|
|
11415
12348
|
<Properties>
|
|
11416
12349
|
<string name="Name">ServerUrlSettings</string>
|
|
11417
12350
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -11525,11 +12458,11 @@ return {
|
|
|
11525
12458
|
]]></string>
|
|
11526
12459
|
</Properties>
|
|
11527
12460
|
</Item>
|
|
11528
|
-
<Item class="ModuleScript" referent="
|
|
12461
|
+
<Item class="ModuleScript" referent="40">
|
|
11529
12462
|
<Properties>
|
|
11530
12463
|
<string name="Name">State</string>
|
|
11531
12464
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
11532
|
-
local CURRENT_VERSION = "3.
|
|
12465
|
+
local CURRENT_VERSION = "3.3.0"
|
|
11533
12466
|
local PLUGIN_VARIANT = "main"
|
|
11534
12467
|
local BASE_PORT = 58741
|
|
11535
12468
|
local SENSITIVE_DIAGNOSTIC_TERMS = { "token", "authorization", "bearer", "password", "cookie", "secret" }
|
|
@@ -11667,7 +12600,7 @@ return {
|
|
|
11667
12600
|
]]></string>
|
|
11668
12601
|
</Properties>
|
|
11669
12602
|
</Item>
|
|
11670
|
-
<Item class="ModuleScript" referent="
|
|
12603
|
+
<Item class="ModuleScript" referent="41">
|
|
11671
12604
|
<Properties>
|
|
11672
12605
|
<string name="Name">StopPlayMonitor</string>
|
|
11673
12606
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -11953,7 +12886,7 @@ return {
|
|
|
11953
12886
|
]]></string>
|
|
11954
12887
|
</Properties>
|
|
11955
12888
|
</Item>
|
|
11956
|
-
<Item class="ModuleScript" referent="
|
|
12889
|
+
<Item class="ModuleScript" referent="42">
|
|
11957
12890
|
<Properties>
|
|
11958
12891
|
<string name="Name">StudioWebSocket</string>
|
|
11959
12892
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -13111,7 +14044,7 @@ return {
|
|
|
13111
14044
|
]]></string>
|
|
13112
14045
|
</Properties>
|
|
13113
14046
|
</Item>
|
|
13114
|
-
<Item class="ModuleScript" referent="
|
|
14047
|
+
<Item class="ModuleScript" referent="43">
|
|
13115
14048
|
<Properties>
|
|
13116
14049
|
<string name="Name">TopologyId</string>
|
|
13117
14050
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -13156,7 +14089,7 @@ return {
|
|
|
13156
14089
|
]]></string>
|
|
13157
14090
|
</Properties>
|
|
13158
14091
|
</Item>
|
|
13159
|
-
<Item class="ModuleScript" referent="
|
|
14092
|
+
<Item class="ModuleScript" referent="44">
|
|
13160
14093
|
<Properties>
|
|
13161
14094
|
<string name="Name">UI</string>
|
|
13162
14095
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -13681,10 +14614,12 @@ return {
|
|
|
13681
14614
|
]]></string>
|
|
13682
14615
|
</Properties>
|
|
13683
14616
|
</Item>
|
|
13684
|
-
<Item class="ModuleScript" referent="
|
|
14617
|
+
<Item class="ModuleScript" referent="45">
|
|
13685
14618
|
<Properties>
|
|
13686
14619
|
<string name="Name">Utils</string>
|
|
13687
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")
|
|
13688
14623
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
13689
14624
|
local LUAU_KEYWORDS = {
|
|
13690
14625
|
["and"] = true,
|
|
@@ -13899,16 +14834,17 @@ local function parseInstancePath(path)
|
|
|
13899
14834
|
end
|
|
13900
14835
|
return parts
|
|
13901
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
|
|
13902
14846
|
local function getRootSegment(instance)
|
|
13903
|
-
if instance.
|
|
13904
|
-
local ok, service = pcall(function()
|
|
13905
|
-
return game:GetService(instance.ClassName)
|
|
13906
|
-
end)
|
|
13907
|
-
if ok and service == instance then
|
|
13908
|
-
return instance.ClassName
|
|
13909
|
-
end
|
|
13910
|
-
end
|
|
13911
|
-
return instance.Name
|
|
14847
|
+
return if isRootService(instance) then instance.ClassName else instance.Name
|
|
13912
14848
|
end
|
|
13913
14849
|
local function getInstancePath(instance)
|
|
13914
14850
|
if not instance or instance == game then
|
|
@@ -13932,13 +14868,7 @@ local function getInstancePath(instance)
|
|
|
13932
14868
|
return path
|
|
13933
14869
|
end
|
|
13934
14870
|
local function getRootInstance(segment)
|
|
13935
|
-
|
|
13936
|
-
return game:GetService(segment)
|
|
13937
|
-
end)
|
|
13938
|
-
if ok and service then
|
|
13939
|
-
return service
|
|
13940
|
-
end
|
|
13941
|
-
return game:FindFirstChild(segment)
|
|
14871
|
+
return getService(segment) or game:FindFirstChild(segment)
|
|
13942
14872
|
end
|
|
13943
14873
|
local function getInstanceByPath(path)
|
|
13944
14874
|
local parts = parseInstancePath(path)
|
|
@@ -13958,6 +14888,36 @@ local function getInstanceByPath(path)
|
|
|
13958
14888
|
end
|
|
13959
14889
|
return current
|
|
13960
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
|
|
13961
14921
|
local function splitLines(source)
|
|
13962
14922
|
local _condition = source
|
|
13963
14923
|
if _condition == nil then
|
|
@@ -14512,6 +15472,8 @@ return {
|
|
|
14512
15472
|
safeCall = safeCall,
|
|
14513
15473
|
getInstancePath = getInstancePath,
|
|
14514
15474
|
getInstanceByPath = getInstanceByPath,
|
|
15475
|
+
resolveInstancePath = resolveInstancePath,
|
|
15476
|
+
createPathAmbiguityChecker = createPathAmbiguityChecker,
|
|
14515
15477
|
splitLines = splitLines,
|
|
14516
15478
|
joinLines = joinLines,
|
|
14517
15479
|
readScriptSource = readScriptSource,
|
|
@@ -14524,11 +15486,11 @@ return {
|
|
|
14524
15486
|
</Properties>
|
|
14525
15487
|
</Item>
|
|
14526
15488
|
</Item>
|
|
14527
|
-
<Item class="Folder" referent="
|
|
15489
|
+
<Item class="Folder" referent="50">
|
|
14528
15490
|
<Properties>
|
|
14529
15491
|
<string name="Name">include</string>
|
|
14530
15492
|
</Properties>
|
|
14531
|
-
<Item class="ModuleScript" referent="
|
|
15493
|
+
<Item class="ModuleScript" referent="46">
|
|
14532
15494
|
<Properties>
|
|
14533
15495
|
<string name="Name">LibMP</string>
|
|
14534
15496
|
<string name="Source"><![CDATA[-- =============================================================================
|
|
@@ -170912,7 +171874,7 @@ return LibMP
|
|
|
170912
171874
|
]]></string>
|
|
170913
171875
|
</Properties>
|
|
170914
171876
|
</Item>
|
|
170915
|
-
<Item class="ModuleScript" referent="
|
|
171877
|
+
<Item class="ModuleScript" referent="47">
|
|
170916
171878
|
<Properties>
|
|
170917
171879
|
<string name="Name">Promise</string>
|
|
170918
171880
|
<string name="Source"><![CDATA[--[[
|
|
@@ -172986,7 +173948,7 @@ return Promise
|
|
|
172986
173948
|
]]></string>
|
|
172987
173949
|
</Properties>
|
|
172988
173950
|
</Item>
|
|
172989
|
-
<Item class="ModuleScript" referent="
|
|
173951
|
+
<Item class="ModuleScript" referent="48">
|
|
172990
173952
|
<Properties>
|
|
172991
173953
|
<string name="Name">RuntimeLib</string>
|
|
172992
173954
|
<string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
|
|
@@ -173253,15 +174215,15 @@ return TS
|
|
|
173253
174215
|
</Properties>
|
|
173254
174216
|
</Item>
|
|
173255
174217
|
</Item>
|
|
173256
|
-
<Item class="Folder" referent="
|
|
174218
|
+
<Item class="Folder" referent="51">
|
|
173257
174219
|
<Properties>
|
|
173258
174220
|
<string name="Name">node_modules</string>
|
|
173259
174221
|
</Properties>
|
|
173260
|
-
<Item class="Folder" referent="
|
|
174222
|
+
<Item class="Folder" referent="52">
|
|
173261
174223
|
<Properties>
|
|
173262
174224
|
<string name="Name">@rbxts</string>
|
|
173263
174225
|
</Properties>
|
|
173264
|
-
<Item class="ModuleScript" referent="
|
|
174226
|
+
<Item class="ModuleScript" referent="49">
|
|
173265
174227
|
<Properties>
|
|
173266
174228
|
<string name="Name">services</string>
|
|
173267
174229
|
<string name="Source"><![CDATA[return setmetatable({}, {
|