@el4cteo/rbx-studio-mcp 0.6.5 → 0.6.7
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/README.md +28 -2
- package/dist/bridge/console.js +182 -0
- package/dist/bridge/console.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/cloudassets.js +233 -0
- package/dist/lib/cloudassets.js.map +1 -0
- package/dist/lib/credentials.js +180 -0
- package/dist/lib/credentials.js.map +1 -0
- package/dist/lib/livedata.js +325 -0
- package/dist/lib/livedata.js.map +1 -0
- package/dist/lib/liveluau.js +83 -0
- package/dist/lib/liveluau.js.map +1 -0
- package/dist/lib/liveops.js +358 -0
- package/dist/lib/liveops.js.map +1 -0
- package/dist/lib/opencloud.js +235 -0
- package/dist/lib/opencloud.js.map +1 -0
- package/dist/tools/audio.js +96 -0
- package/dist/tools/audio.js.map +1 -0
- package/dist/tools/data.js +122 -3
- package/dist/tools/data.js.map +1 -1
- package/dist/tools/exec.js +48 -1
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/scripts.js +112 -4
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/spatial.js +135 -0
- package/dist/tools/spatial.js.map +1 -0
- package/dist/tools/universe.js +177 -0
- package/dist/tools/universe.js.map +1 -0
- package/dist/tools/upload.js +294 -0
- package/dist/tools/upload.js.map +1 -0
- package/dist/tools/world.js +361 -10
- package/dist/tools/world.js.map +1 -1
- package/package.json +74 -74
- package/plugin/src/Commands.luau +646 -622
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Phrase.luau +816 -766
- package/plugin/src/Prompt.luau +965 -961
- package/plugin/src/Secret.luau +86 -0
- package/plugin/src/Serialize.luau +759 -499
- package/plugin/src/handlers/Assets.luau +636 -587
- package/plugin/src/handlers/Audio.luau +411 -0
- package/plugin/src/handlers/Geometry.luau +722 -577
- package/plugin/src/handlers/Instances.luau +84 -4
- package/plugin/src/handlers/Spatial.luau +334 -0
- package/plugin/src/init.server.luau +883 -879
|
@@ -116,7 +116,17 @@ end
|
|
|
116
116
|
model appears in the Explorer complete rather than assembling itself piece by
|
|
117
117
|
piece in front of the user.
|
|
118
118
|
]]
|
|
119
|
-
|
|
119
|
+
--[[
|
|
120
|
+
One property that could not be applied yet, kept for a second attempt.
|
|
121
|
+
]]
|
|
122
|
+
type Deferred = { target: Instance, name: string, spec: PropertySpec, className: string }
|
|
123
|
+
|
|
124
|
+
local function build(
|
|
125
|
+
spec: { [string]: any },
|
|
126
|
+
parent: Instance,
|
|
127
|
+
created: { Instance },
|
|
128
|
+
deferred: { Deferred }
|
|
129
|
+
)
|
|
120
130
|
local ok, instance = pcall(Instance.new, spec.className)
|
|
121
131
|
if not ok or typeof(instance) ~= "Instance" then
|
|
122
132
|
Dispatch.fail(
|
|
@@ -132,11 +142,37 @@ local function build(spec: { [string]: any }, parent: Instance, created: { Insta
|
|
|
132
142
|
target.Name = spec.name
|
|
133
143
|
end
|
|
134
144
|
|
|
145
|
+
--[[
|
|
146
|
+
A property naming an instance that does not exist YET is set aside, not
|
|
147
|
+
failed.
|
|
148
|
+
|
|
149
|
+
The whole subtree is assembled detached and attached at the very end --
|
|
150
|
+
see the last line of this function -- so while a batch is being built,
|
|
151
|
+
nothing in it is reachable by path. That made a one-call rig impossible:
|
|
152
|
+
a HingeConstraint could not point at the Attachment created two lines
|
|
153
|
+
above it, a Motor6D could not find its Part0, and the tool's own promise
|
|
154
|
+
of "build a whole model in a single call" stopped exactly where joints
|
|
155
|
+
began. Each of those needed a second `create` call, which is also a
|
|
156
|
+
second undo step.
|
|
157
|
+
|
|
158
|
+
So these are retried once the batch is attached. Anything else -- a
|
|
159
|
+
misspelled enum, a malformed Vector3 -- still fails immediately, because
|
|
160
|
+
waiting cannot make it right.
|
|
161
|
+
]]
|
|
135
162
|
local failures: { string } = {}
|
|
136
163
|
for name, property in (spec.properties or {}) :: { [string]: PropertySpec } do
|
|
137
164
|
local failure = applyProperty(target, name, property)
|
|
138
165
|
if failure then
|
|
139
|
-
|
|
166
|
+
if string.find(failure, Serialize.UNRESOLVED_PREFIX, 1, true) then
|
|
167
|
+
table.insert(deferred, {
|
|
168
|
+
target = target,
|
|
169
|
+
name = name,
|
|
170
|
+
spec = property,
|
|
171
|
+
className = tostring(spec.className),
|
|
172
|
+
})
|
|
173
|
+
else
|
|
174
|
+
table.insert(failures, failure)
|
|
175
|
+
end
|
|
140
176
|
end
|
|
141
177
|
end
|
|
142
178
|
for _, failure in applyAttributes(target, (spec.attributes or {}) :: { [string]: any }) do
|
|
@@ -158,7 +194,7 @@ local function build(spec: { [string]: any }, parent: Instance, created: { Insta
|
|
|
158
194
|
-- Recorded parent-first, so the response reads top-down like the Explorer.
|
|
159
195
|
table.insert(created, target)
|
|
160
196
|
for _, child in (spec.children or {}) :: { { [string]: any } } do
|
|
161
|
-
build(child, target, created)
|
|
197
|
+
build(child, target, created, deferred)
|
|
162
198
|
end
|
|
163
199
|
|
|
164
200
|
target.Parent = parent
|
|
@@ -183,8 +219,52 @@ function Instances.create(params: { [string]: any }): { [string]: any }
|
|
|
183
219
|
|
|
184
220
|
local created, recorded = Undo.record("StudioMCP.Create", "MCP create", function()
|
|
185
221
|
local built: { Instance } = {}
|
|
222
|
+
local deferred: { Deferred } = {}
|
|
186
223
|
for _, spec in specs do
|
|
187
|
-
build(spec, Paths.resolve(spec.parent), built)
|
|
224
|
+
build(spec, Paths.resolve(spec.parent), built, deferred)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
--[[
|
|
228
|
+
Second pass, after everything is attached.
|
|
229
|
+
|
|
230
|
+
Now every instance in the batch is reachable by path, so a reference
|
|
231
|
+
to a sibling resolves. A failure here is real -- the path names
|
|
232
|
+
something that was never going to exist -- and it fails the batch,
|
|
233
|
+
which cancels the recording and leaves the place untouched.
|
|
234
|
+
]]
|
|
235
|
+
local failures: { string } = {}
|
|
236
|
+
for _, entry in deferred do
|
|
237
|
+
local failure = applyProperty(entry.target, entry.name, entry.spec)
|
|
238
|
+
if failure then
|
|
239
|
+
--[[
|
|
240
|
+
Stripped with find+sub, NOT gsub.
|
|
241
|
+
|
|
242
|
+
`string.gsub` takes a Lua PATTERN, and the marker is
|
|
243
|
+
"[unresolved] " -- which as a pattern is a character class
|
|
244
|
+
matching any one of u, n, r, e, s, o, l, v, d. It quietly
|
|
245
|
+
ate those letters out of the whole message, turning "this
|
|
246
|
+
property holds a BasePart" into "thiproperty holda
|
|
247
|
+
BasePart". Caught in QA; the message is the only thing a
|
|
248
|
+
reader gets here, so a mangled one is the whole failure.
|
|
249
|
+
|
|
250
|
+
The property name is already inside `failure`, so it is not
|
|
251
|
+
prepended a second time -- that read as "Part1: Part1: ...".
|
|
252
|
+
]]
|
|
253
|
+
local clean = failure
|
|
254
|
+
local at = string.find(clean, Serialize.UNRESOLVED_PREFIX, 1, true)
|
|
255
|
+
if at then
|
|
256
|
+
clean = string.sub(clean, 1, at - 1)
|
|
257
|
+
.. string.sub(clean, at + #Serialize.UNRESOLVED_PREFIX)
|
|
258
|
+
end
|
|
259
|
+
table.insert(failures, string.format("%s.%s", entry.className, clean))
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
if #failures > 0 then
|
|
263
|
+
Dispatch.fail(
|
|
264
|
+
"BAD_PROPERTY",
|
|
265
|
+
string.format("Could not set %d reference(s) after the batch was built.", #failures),
|
|
266
|
+
table.concat(failures, "; ")
|
|
267
|
+
)
|
|
188
268
|
end
|
|
189
269
|
return built
|
|
190
270
|
end)
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Spatial queries: casts, and "what is in this box".
|
|
4
|
+
|
|
5
|
+
These answer the one question the Explorer cannot: what is actually THERE.
|
|
6
|
+
A path tells you an instance exists and where its own pivot sits; it does not
|
|
7
|
+
tell you that the door frame overlaps the wall, that the spawn is buried a
|
|
8
|
+
stud inside the floor, or that nothing at all stands between the turret and
|
|
9
|
+
the player. Every one of those is a query against the world, and before this
|
|
10
|
+
the only way to run one was `execute_luau`.
|
|
11
|
+
|
|
12
|
+
All of it goes through a WorldRoot rather than through `workspace` directly,
|
|
13
|
+
for the same reason collision groups do: a `WorldModel` inside a ViewportFrame
|
|
14
|
+
is its own world with its own parts and its own collision groups, and a query
|
|
15
|
+
hard-coded to the Workspace can only ever lie about it.
|
|
16
|
+
|
|
17
|
+
The `collisionGroup` field is the point of doing this now. Roblox moved
|
|
18
|
+
collision group management onto WorldRoot in September 2026, and spatial
|
|
19
|
+
queries inside a WorldModel now honour groups the way Workspace always has.
|
|
20
|
+
A cast run in the wrong group reports a clear path through a wall the player
|
|
21
|
+
cannot walk through -- a wrong answer that looks exactly like a right one.
|
|
22
|
+
]]
|
|
23
|
+
|
|
24
|
+
local Workspace = game:GetService("Workspace")
|
|
25
|
+
|
|
26
|
+
local Dispatch = require(script.Parent.Parent.Dispatch)
|
|
27
|
+
local Paths = require(script.Parent.Parent.Paths)
|
|
28
|
+
local Serialize = require(script.Parent.Parent.Serialize)
|
|
29
|
+
|
|
30
|
+
local Spatial = {}
|
|
31
|
+
|
|
32
|
+
-- A query that returns every part in a large place is not an answer, it is a
|
|
33
|
+
-- transcript. Callers narrow with `filter` or raise this deliberately.
|
|
34
|
+
local DEFAULT_LIMIT = 50
|
|
35
|
+
local MAX_LIMIT = 500
|
|
36
|
+
|
|
37
|
+
--[[
|
|
38
|
+
Which world the query runs in. Same rule as collision groups: the Workspace
|
|
39
|
+
unless a WorldModel is named, and anything that is not a WorldRoot is refused
|
|
40
|
+
by name rather than left to fail inside the engine call.
|
|
41
|
+
]]
|
|
42
|
+
local function root(params: { [string]: any }): Instance
|
|
43
|
+
local path = params.worldModel
|
|
44
|
+
if typeof(path) ~= "string" or path == "" then
|
|
45
|
+
return Workspace
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
local instance = Paths.resolve(path)
|
|
49
|
+
if not instance:IsA("WorldRoot") then
|
|
50
|
+
Dispatch.fail(
|
|
51
|
+
"BAD_PARAMS",
|
|
52
|
+
string.format("%s is a %s, not a WorldModel.", path, instance.ClassName),
|
|
53
|
+
"Spatial queries run on a WorldRoot: the Workspace, or a WorldModel "
|
|
54
|
+
.. "inside a ViewportFrame. Omit `worldModel` for the Workspace."
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
return instance
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
local function vector(value: any, field: string): Vector3
|
|
61
|
+
if typeof(value) == "table" then
|
|
62
|
+
local list = value :: { any }
|
|
63
|
+
local x, y, z = tonumber(list[1]), tonumber(list[2]), tonumber(list[3])
|
|
64
|
+
if x and y and z then
|
|
65
|
+
return Vector3.new(x, y, z)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
if typeof(value) == "string" then
|
|
69
|
+
local ok, parsed = Serialize.parse(value, "Vector3")
|
|
70
|
+
if ok and typeof(parsed) == "Vector3" then
|
|
71
|
+
return parsed
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
Dispatch.fail(
|
|
75
|
+
"BAD_PARAMS",
|
|
76
|
+
string.format("`%s` is not a position.", field),
|
|
77
|
+
'Write it the way the Properties panel does: "12, 0, 5".'
|
|
78
|
+
)
|
|
79
|
+
error("unreachable")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
local function optionalVector(value: any, field: string): Vector3?
|
|
83
|
+
if value == nil or value == "" then
|
|
84
|
+
return nil
|
|
85
|
+
end
|
|
86
|
+
return vector(value, field)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
--[[
|
|
90
|
+
Builds the filter every query shares.
|
|
91
|
+
|
|
92
|
+
`ignore` and `only` are the same engine field with the list flipped, because
|
|
93
|
+
"everything except the character" and "only the doors" are both common and
|
|
94
|
+
only one of them is expressible at a time. Naming a path that no longer
|
|
95
|
+
exists is a caller mistake worth reporting: silently querying against an
|
|
96
|
+
empty filter gives a plausible answer to the wrong question.
|
|
97
|
+
]]
|
|
98
|
+
local function buildParams(params: { [string]: any }): RaycastParams
|
|
99
|
+
local query = RaycastParams.new()
|
|
100
|
+
|
|
101
|
+
local list = params.only or params.ignore
|
|
102
|
+
if list ~= nil then
|
|
103
|
+
if typeof(list) ~= "table" then
|
|
104
|
+
Dispatch.fail("BAD_PARAMS", "`ignore` and `only` take a list of paths.")
|
|
105
|
+
end
|
|
106
|
+
local resolved, missing = Paths.resolveMany(list :: { string })
|
|
107
|
+
if #missing > 0 then
|
|
108
|
+
Dispatch.fail(
|
|
109
|
+
"NOT_FOUND",
|
|
110
|
+
string.format("Could not resolve: %s", table.concat(missing, ", "))
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
query.FilterDescendantsInstances = resolved
|
|
114
|
+
query.FilterType = if params.only ~= nil
|
|
115
|
+
then Enum.RaycastFilterType.Include
|
|
116
|
+
else Enum.RaycastFilterType.Exclude
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
if typeof(params.collisionGroup) == "string" and params.collisionGroup ~= "" then
|
|
120
|
+
query.CollisionGroup = params.collisionGroup
|
|
121
|
+
end
|
|
122
|
+
-- Off by default, matching the engine. A caller asking "can the player see
|
|
123
|
+
-- the exit" usually does want water ignored; one asking "what is here" does
|
|
124
|
+
-- not, and neither can be guessed.
|
|
125
|
+
query.RespectCanCollide = params.respectCanCollide == true
|
|
126
|
+
query.IgnoreWater = params.ignoreWater == true
|
|
127
|
+
query.BruteForceAllSlow = false
|
|
128
|
+
return query
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
local function describeHit(result: RaycastResult, origin: Vector3): { [string]: any }
|
|
132
|
+
return {
|
|
133
|
+
path = Paths.of(result.Instance),
|
|
134
|
+
class = result.Instance.ClassName,
|
|
135
|
+
position = Serialize.value(result.Position),
|
|
136
|
+
normal = Serialize.value(result.Normal),
|
|
137
|
+
material = tostring(result.Material):gsub("Enum%.Material%.", ""),
|
|
138
|
+
distance = math.round((result.Position - origin).Magnitude * 100) / 100,
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
local function describePart(part: BasePart): { [string]: any }
|
|
143
|
+
return {
|
|
144
|
+
path = Paths.of(part),
|
|
145
|
+
class = part.ClassName,
|
|
146
|
+
position = Serialize.value(part.Position),
|
|
147
|
+
size = Serialize.value(part.Size),
|
|
148
|
+
collisionGroup = part.CollisionGroup,
|
|
149
|
+
canCollide = part.CanCollide,
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
--[[
|
|
154
|
+
Casts a ray, a block or a sphere.
|
|
155
|
+
|
|
156
|
+
The three are one operation with one extra argument, and the engine treats
|
|
157
|
+
them that way too -- `Blockcast` and `Spherecast` take the same params and
|
|
158
|
+
return the same RaycastResult. Splitting them into three handlers would only
|
|
159
|
+
mean three copies of the filter code.
|
|
160
|
+
|
|
161
|
+
A miss is reported as `hit = false` with the distance travelled, not as an
|
|
162
|
+
error and not as an empty object. "Nothing is in the way" is a real answer
|
|
163
|
+
and usually the one being checked for.
|
|
164
|
+
]]
|
|
165
|
+
function Spatial.cast(params: { [string]: any }): { [string]: any }
|
|
166
|
+
local world = root(params) :: WorldRoot
|
|
167
|
+
local shape = tostring(params.shape or "ray")
|
|
168
|
+
|
|
169
|
+
local origin = vector(params.from, "from")
|
|
170
|
+
|
|
171
|
+
--[[
|
|
172
|
+
Direction is given either as a target point or as a vector, because both
|
|
173
|
+
are natural and they are not interchangeable. "Can the turret see the
|
|
174
|
+
player" is two positions; "is there ground below" is a direction and a
|
|
175
|
+
distance. Taking only one of them forces the caller to do vector maths
|
|
176
|
+
in the prompt, which is exactly where an off-by-one sign lives.
|
|
177
|
+
]]
|
|
178
|
+
local direction: Vector3
|
|
179
|
+
local to = optionalVector(params.to, "to")
|
|
180
|
+
if to then
|
|
181
|
+
direction = to - origin
|
|
182
|
+
else
|
|
183
|
+
local towards = optionalVector(params.direction, "direction")
|
|
184
|
+
if not towards then
|
|
185
|
+
Dispatch.fail(
|
|
186
|
+
"BAD_PARAMS",
|
|
187
|
+
"A cast needs somewhere to go.",
|
|
188
|
+
"Give `to` for a point to aim at, or `direction` plus `distance`."
|
|
189
|
+
)
|
|
190
|
+
error("unreachable")
|
|
191
|
+
end
|
|
192
|
+
local distance = tonumber(params.distance) or 100
|
|
193
|
+
direction = towards.Unit * distance
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
local query = buildParams(params)
|
|
197
|
+
local result: RaycastResult?
|
|
198
|
+
|
|
199
|
+
if shape == "ray" then
|
|
200
|
+
result = world:Raycast(origin, direction, query)
|
|
201
|
+
elseif shape == "block" then
|
|
202
|
+
local size = optionalVector(params.size, "size") or Vector3.new(1, 1, 1)
|
|
203
|
+
result = world:Blockcast(CFrame.new(origin), size, direction, query)
|
|
204
|
+
elseif shape == "sphere" then
|
|
205
|
+
local radius = tonumber(params.radius) or 1
|
|
206
|
+
result = world:Spherecast(origin, radius, direction, query)
|
|
207
|
+
else
|
|
208
|
+
Dispatch.fail(
|
|
209
|
+
"BAD_PARAMS",
|
|
210
|
+
string.format("unknown cast shape %q", shape),
|
|
211
|
+
'Use "ray", "block" or "sphere".'
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
if not result then
|
|
216
|
+
return {
|
|
217
|
+
hit = false,
|
|
218
|
+
shape = shape,
|
|
219
|
+
from = Serialize.value(origin),
|
|
220
|
+
travelled = math.round(direction.Magnitude * 100) / 100,
|
|
221
|
+
world = Paths.of(world),
|
|
222
|
+
note = "Nothing was in the way for the whole length of the cast.",
|
|
223
|
+
}
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
local hit = describeHit(result, origin)
|
|
227
|
+
hit.hit = true
|
|
228
|
+
hit.shape = shape
|
|
229
|
+
hit.from = Serialize.value(origin)
|
|
230
|
+
hit.world = Paths.of(world)
|
|
231
|
+
return hit
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
--[[
|
|
235
|
+
Everything inside a volume.
|
|
236
|
+
|
|
237
|
+
`part` is the one worth reaching for on a build: it takes an existing part
|
|
238
|
+
and reports what overlaps it, which answers "is this door clipping the wall"
|
|
239
|
+
directly rather than by reconstructing the door's bounds by hand.
|
|
240
|
+
]]
|
|
241
|
+
function Spatial.overlap(params: { [string]: any }): { [string]: any }
|
|
242
|
+
local world = root(params) :: WorldRoot
|
|
243
|
+
local region = tostring(params.region or "box")
|
|
244
|
+
local limit = math.clamp(tonumber(params.limit) or DEFAULT_LIMIT, 1, MAX_LIMIT)
|
|
245
|
+
|
|
246
|
+
local overlap = OverlapParams.new()
|
|
247
|
+
local query = buildParams(params)
|
|
248
|
+
overlap.FilterDescendantsInstances = query.FilterDescendantsInstances
|
|
249
|
+
overlap.FilterType = query.FilterType
|
|
250
|
+
overlap.CollisionGroup = query.CollisionGroup
|
|
251
|
+
overlap.RespectCanCollide = query.RespectCanCollide
|
|
252
|
+
-- Asked for one over the limit so the response can say whether the list was
|
|
253
|
+
-- cut, instead of a caller reading exactly 50 as "there are 50".
|
|
254
|
+
overlap.MaxParts = limit + 1
|
|
255
|
+
|
|
256
|
+
local found: { BasePart }
|
|
257
|
+
local about: { [string]: any } = {}
|
|
258
|
+
|
|
259
|
+
if region == "box" then
|
|
260
|
+
local centre = vector(params.at, "at")
|
|
261
|
+
local size = optionalVector(params.size, "size") or Vector3.new(4, 4, 4)
|
|
262
|
+
found = world:GetPartBoundsInBox(CFrame.new(centre), size, overlap)
|
|
263
|
+
about.at, about.size = Serialize.value(centre), Serialize.value(size)
|
|
264
|
+
elseif region == "radius" then
|
|
265
|
+
local centre = vector(params.at, "at")
|
|
266
|
+
local radius = tonumber(params.radius) or 4
|
|
267
|
+
found = world:GetPartBoundsInRadius(centre, radius, overlap)
|
|
268
|
+
about.at, about.radius = Serialize.value(centre), radius
|
|
269
|
+
elseif region == "part" then
|
|
270
|
+
local path = params.path
|
|
271
|
+
if typeof(path) ~= "string" or path == "" then
|
|
272
|
+
Dispatch.fail("BAD_PARAMS", 'region="part" needs `path`, the part to test.')
|
|
273
|
+
end
|
|
274
|
+
local instance = Paths.resolve(path)
|
|
275
|
+
if not instance:IsA("BasePart") then
|
|
276
|
+
Dispatch.fail(
|
|
277
|
+
"BAD_PARAMS",
|
|
278
|
+
string.format("%s is a %s, not a part.", path, instance.ClassName)
|
|
279
|
+
)
|
|
280
|
+
end
|
|
281
|
+
local subject = instance :: BasePart
|
|
282
|
+
--[[
|
|
283
|
+
The subject always overlaps itself, and a caller who asked what
|
|
284
|
+
touches a door does not mean the door. Added to the filter rather
|
|
285
|
+
than stripped from the results, so it does not eat one of the slots
|
|
286
|
+
the limit allows.
|
|
287
|
+
]]
|
|
288
|
+
if overlap.FilterType == Enum.RaycastFilterType.Exclude then
|
|
289
|
+
local excluded = overlap.FilterDescendantsInstances
|
|
290
|
+
table.insert(excluded, subject)
|
|
291
|
+
overlap.FilterDescendantsInstances = excluded
|
|
292
|
+
end
|
|
293
|
+
found = world:GetPartsInPart(subject, overlap)
|
|
294
|
+
about.path = Paths.of(subject)
|
|
295
|
+
else
|
|
296
|
+
Dispatch.fail(
|
|
297
|
+
"BAD_PARAMS",
|
|
298
|
+
string.format("unknown region %q", region),
|
|
299
|
+
'Use "box", "radius" or "part".'
|
|
300
|
+
)
|
|
301
|
+
error("unreachable")
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
local truncated = #found > limit
|
|
305
|
+
local items: { { [string]: any } } = {}
|
|
306
|
+
for index, part in found do
|
|
307
|
+
if index > limit then
|
|
308
|
+
break
|
|
309
|
+
end
|
|
310
|
+
table.insert(items, describePart(part))
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
about.region = region
|
|
314
|
+
about.items = items
|
|
315
|
+
about.count = #items
|
|
316
|
+
about.truncated = truncated
|
|
317
|
+
about.world = Paths.of(world)
|
|
318
|
+
if truncated then
|
|
319
|
+
about.note = string.format(
|
|
320
|
+
"More than %d parts are in this volume; raise `limit` or narrow with `only`.",
|
|
321
|
+
limit
|
|
322
|
+
)
|
|
323
|
+
end
|
|
324
|
+
return about
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
function Spatial.register()
|
|
328
|
+
Dispatch.registerAll("spatial", {
|
|
329
|
+
cast = Spatial.cast,
|
|
330
|
+
overlap = Spatial.overlap,
|
|
331
|
+
})
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
return Spatial
|