@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.
Files changed (46) hide show
  1. package/README.md +28 -2
  2. package/dist/bridge/console.js +182 -0
  3. package/dist/bridge/console.js.map +1 -1
  4. package/dist/index.js +4 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/cloudassets.js +233 -0
  7. package/dist/lib/cloudassets.js.map +1 -0
  8. package/dist/lib/credentials.js +180 -0
  9. package/dist/lib/credentials.js.map +1 -0
  10. package/dist/lib/livedata.js +325 -0
  11. package/dist/lib/livedata.js.map +1 -0
  12. package/dist/lib/liveluau.js +83 -0
  13. package/dist/lib/liveluau.js.map +1 -0
  14. package/dist/lib/liveops.js +358 -0
  15. package/dist/lib/liveops.js.map +1 -0
  16. package/dist/lib/opencloud.js +235 -0
  17. package/dist/lib/opencloud.js.map +1 -0
  18. package/dist/tools/audio.js +96 -0
  19. package/dist/tools/audio.js.map +1 -0
  20. package/dist/tools/data.js +122 -3
  21. package/dist/tools/data.js.map +1 -1
  22. package/dist/tools/exec.js +48 -1
  23. package/dist/tools/exec.js.map +1 -1
  24. package/dist/tools/scripts.js +112 -4
  25. package/dist/tools/scripts.js.map +1 -1
  26. package/dist/tools/spatial.js +135 -0
  27. package/dist/tools/spatial.js.map +1 -0
  28. package/dist/tools/universe.js +177 -0
  29. package/dist/tools/universe.js.map +1 -0
  30. package/dist/tools/upload.js +294 -0
  31. package/dist/tools/upload.js.map +1 -0
  32. package/dist/tools/world.js +361 -10
  33. package/dist/tools/world.js.map +1 -1
  34. package/package.json +74 -74
  35. package/plugin/src/Commands.luau +646 -622
  36. package/plugin/src/Config.luau +1 -1
  37. package/plugin/src/Phrase.luau +816 -766
  38. package/plugin/src/Prompt.luau +965 -961
  39. package/plugin/src/Secret.luau +86 -0
  40. package/plugin/src/Serialize.luau +759 -499
  41. package/plugin/src/handlers/Assets.luau +636 -587
  42. package/plugin/src/handlers/Audio.luau +411 -0
  43. package/plugin/src/handlers/Geometry.luau +722 -577
  44. package/plugin/src/handlers/Instances.luau +84 -4
  45. package/plugin/src/handlers/Spatial.luau +334 -0
  46. package/plugin/src/init.server.luau +883 -879
@@ -0,0 +1,411 @@
1
+ --!strict
2
+ --[[
3
+ The audio graph: wiring AudioPlayers to whatever plays them.
4
+
5
+ Roblox's modern audio API is not one instance with a Play method. It is a
6
+ signal graph: an `AudioPlayer` holds the asset, an `AudioEmitter` puts sound
7
+ in the world or an `AudioDeviceOutput` sends it straight to the player's
8
+ speakers, effects sit in between, and NONE of them are connected until a
9
+ `Wire` joins two named pins. A place can hold a perfectly configured
10
+ AudioPlayer with the right asset, the right volume and `Playing` true, and be
11
+ completely silent, because the one instance that carries sound between them
12
+ was never made.
13
+
14
+ `create` can already build each of those instances -- that is not the gap.
15
+ The gap is that getting sound out requires knowing the pin names, knowing
16
+ which of the eight-odd sink classes fits the case, and getting the wire's
17
+ direction right, and a wrong guess produces silence rather than an error.
18
+ So this file does not wrap the engine's audio API; it wraps the knowledge of
19
+ how to assemble it.
20
+
21
+ Old `Sound` instances still work and are still the shorter path for a plain
22
+ one-off noise. Reach for this when the case needs what Sound cannot do:
23
+ effects, per-listener mixing, or an emitter fed by several sources.
24
+ ]]
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
+ local Undo = require(script.Parent.Parent.Undo)
30
+
31
+ local Audio = {}
32
+
33
+ -- Effects usable in the middle of a chain: exactly one Input and one Output, so
34
+ -- they can be spliced in without the caller naming pins.
35
+ local EFFECTS: { [string]: boolean } = {
36
+ AudioFader = true,
37
+ AudioEqualizer = true,
38
+ AudioCompressor = true,
39
+ AudioReverb = true,
40
+ AudioEcho = true,
41
+ AudioDistortion = true,
42
+ AudioPitchShifter = true,
43
+ AudioChorus = true,
44
+ AudioFlanger = true,
45
+ AudioLimiter = true,
46
+ }
47
+
48
+ local function pinsOf(instance: Instance): ({ string }, { string })
49
+ local okIn, inputs = pcall(function()
50
+ return (instance :: any):GetInputPins()
51
+ end)
52
+ local okOut, outputs = pcall(function()
53
+ return (instance :: any):GetOutputPins()
54
+ end)
55
+ return (if okIn then inputs else {}), (if okOut then outputs else {})
56
+ end
57
+
58
+ local function isAudio(instance: Instance): boolean
59
+ local inputs, outputs = pinsOf(instance)
60
+ return #inputs > 0 or #outputs > 0
61
+ end
62
+
63
+ --[[
64
+ Joins two pins with a Wire.
65
+
66
+ The pin names are checked against the instances' own `GetInputPins` and
67
+ `GetOutputPins` rather than against a table here, because that list comes
68
+ from the running engine and this file would go stale. A wrong pin name is
69
+ the failure worth catching: the engine accepts any string, the Wire reports
70
+ `Connected = false`, and the only symptom is silence.
71
+
72
+ The wire is parented to the TARGET. Nothing in the engine requires that, but
73
+ a wire has to live somewhere and scattering them makes a graph unreadable in
74
+ the Explorer; keeping each one with the thing it feeds means the chain reads
75
+ backwards from the speaker, which is how anyone debugging silence walks it.
76
+ ]]
77
+ local function connect(
78
+ source: Instance,
79
+ target: Instance,
80
+ sourcePin: string?,
81
+ targetPin: string?
82
+ ): (Wire, string?)
83
+ local _, sourceOutputs = pinsOf(source)
84
+ local targetInputs, _ = pinsOf(target)
85
+
86
+ if #sourceOutputs == 0 then
87
+ Dispatch.fail(
88
+ "BAD_PARAMS",
89
+ string.format("%s has no output pins, so nothing can come out of it.", source.ClassName)
90
+ )
91
+ end
92
+ if #targetInputs == 0 then
93
+ Dispatch.fail(
94
+ "BAD_PARAMS",
95
+ string.format("%s has no input pins, so nothing can go into it.", target.ClassName)
96
+ )
97
+ end
98
+
99
+ local fromPin = sourcePin or (if table.find(sourceOutputs, "Output") then "Output" else sourceOutputs[1])
100
+ local toPin = targetPin or (if table.find(targetInputs, "Input") then "Input" else targetInputs[1])
101
+
102
+ if not table.find(sourceOutputs, fromPin) then
103
+ Dispatch.fail(
104
+ "BAD_PARAMS",
105
+ string.format('%s has no output pin named "%s".', source.ClassName, fromPin),
106
+ string.format("It has: %s", table.concat(sourceOutputs, ", "))
107
+ )
108
+ end
109
+ if not table.find(targetInputs, toPin) then
110
+ Dispatch.fail(
111
+ "BAD_PARAMS",
112
+ string.format('%s has no input pin named "%s".', target.ClassName, toPin),
113
+ string.format("It has: %s", table.concat(targetInputs, ", "))
114
+ )
115
+ end
116
+
117
+ local wire = Instance.new("Wire")
118
+ wire.SourceInstance = source
119
+ wire.SourceName = fromPin
120
+ wire.TargetInstance = target
121
+ wire.TargetName = toPin
122
+ wire.Parent = target
123
+
124
+ --[[
125
+ `Connected` is read back rather than assumed. It is the engine's own
126
+ verdict on whether this wire carries anything, and it is the difference
127
+ between "wired" and "wired correctly" -- the whole reason silence is so
128
+ hard to debug by hand.
129
+ ]]
130
+ local warning: string? = nil
131
+ if not wire.Connected then
132
+ warning = string.format(
133
+ "The wire from %s.%s to %s.%s reports Connected = false.",
134
+ source.ClassName,
135
+ fromPin,
136
+ target.ClassName,
137
+ toPin
138
+ )
139
+ end
140
+ return wire, warning
141
+ end
142
+
143
+ --[[
144
+ Wires two existing instances together.
145
+ ]]
146
+ function Audio.wire(params: { [string]: any }): { [string]: any }
147
+ local fromPath, toPath = params.from, params.to
148
+ if typeof(fromPath) ~= "string" or typeof(toPath) ~= "string" then
149
+ Dispatch.fail("BAD_PARAMS", "`from` and `to` are both paths to audio instances.")
150
+ end
151
+
152
+ local source = Paths.resolve(fromPath)
153
+ local target = Paths.resolve(toPath)
154
+
155
+ local result, undoable = Undo.record("MCPAudioWire", "Wire audio", function()
156
+ local wire, warning = connect(
157
+ source,
158
+ target,
159
+ if typeof(params.fromPin) == "string" then params.fromPin else nil,
160
+ if typeof(params.toPin) == "string" then params.toPin else nil
161
+ )
162
+ return {
163
+ wire = Paths.of(wire),
164
+ from = Paths.of(source),
165
+ to = Paths.of(target),
166
+ connected = wire.Connected,
167
+ warning = warning,
168
+ }
169
+ end)
170
+
171
+ result.undoable = undoable
172
+ return result
173
+ end
174
+
175
+ --[[
176
+ Builds a whole working chain in one step.
177
+
178
+ `world` is a sound at a place: AudioPlayer -> [effects] -> AudioEmitter,
179
+ parented to a part, heard from wherever it is. This is the common case and
180
+ the one people get wrong, because it also needs an `AudioListener` somewhere
181
+ -- without one on the camera or the character, a correctly wired emitter is
182
+ still silent. Reported rather than created: where the listener belongs is a
183
+ game design decision, not a default.
184
+
185
+ `ui` is a sound with no position: AudioPlayer -> [effects] ->
186
+ AudioDeviceOutput. Menu clicks, music.
187
+ ]]
188
+ function Audio.graph(params: { [string]: any }): { [string]: any }
189
+ local kind = tostring(params.kind or "world")
190
+ if kind ~= "world" and kind ~= "ui" then
191
+ Dispatch.fail("BAD_PARAMS", string.format("unknown kind %q", kind), 'Use "world" or "ui".')
192
+ end
193
+
194
+ local parentPath = params.parent
195
+ if typeof(parentPath) ~= "string" or parentPath == "" then
196
+ Dispatch.fail(
197
+ "BAD_PARAMS",
198
+ "`parent` says where the graph goes.",
199
+ 'For kind="world" that is the part the sound comes from.'
200
+ )
201
+ end
202
+ local parent = Paths.resolve(parentPath)
203
+
204
+ if kind == "world" and not parent:IsA("BasePart") and not parent:IsA("Attachment") then
205
+ Dispatch.fail(
206
+ "BAD_PARAMS",
207
+ string.format("%s is a %s.", parentPath, parent.ClassName),
208
+ "A world sound needs somewhere in the world to come from: parent it "
209
+ .. 'to a BasePart or an Attachment, or use kind="ui" for a sound '
210
+ .. "with no position."
211
+ )
212
+ end
213
+
214
+ local asset = params.asset
215
+ if asset ~= nil and typeof(asset) ~= "string" then
216
+ Dispatch.fail("BAD_PARAMS", "`asset` is an id like \"rbxassetid://1234\".")
217
+ end
218
+
219
+ local effects: { string } = {}
220
+ if params.effects ~= nil then
221
+ if typeof(params.effects) ~= "table" then
222
+ Dispatch.fail("BAD_PARAMS", "`effects` is a list of class names.")
223
+ end
224
+ for _, name in params.effects :: { any } do
225
+ local class = tostring(name)
226
+ if not EFFECTS[class] then
227
+ local known: { string } = {}
228
+ for effect in EFFECTS do
229
+ table.insert(known, effect)
230
+ end
231
+ table.sort(known)
232
+ Dispatch.fail(
233
+ "BAD_PARAMS",
234
+ string.format("%s is not an audio effect that can sit in a chain.", class),
235
+ string.format("Use one of: %s", table.concat(known, ", "))
236
+ )
237
+ end
238
+ table.insert(effects, class)
239
+ end
240
+ end
241
+
242
+ local name = if typeof(params.name) == "string" and params.name ~= "" then params.name else "Audio"
243
+
244
+ local result, undoable = Undo.record("MCPAudioGraph", "Build audio graph", function()
245
+ local created: { string } = {}
246
+ local warnings: { string } = {}
247
+
248
+ local player = Instance.new("AudioPlayer")
249
+ player.Name = name
250
+ if asset then
251
+ --[[
252
+ `AudioContent` is the modern property and takes a Content, not a
253
+ string -- assigning the id directly throws. `Asset` is the older
254
+ ContentId spelling and still works; this uses the one the running
255
+ engine actually has rather than picking a side.
256
+ ]]
257
+ local okContent = pcall(function()
258
+ player.AudioContent = Content.fromUri(asset :: string)
259
+ end)
260
+ if not okContent then
261
+ pcall(function()
262
+ (player :: any).Asset = asset
263
+ end)
264
+ end
265
+ end
266
+ player.Parent = parent
267
+ table.insert(created, Paths.of(player))
268
+
269
+ -- The chain is built front to back, each link wired to the one before.
270
+ local previous: Instance = player
271
+ for _, class in effects do
272
+ local effect = Instance.new(class)
273
+ effect.Name = class:gsub("^Audio", "")
274
+ effect.Parent = parent
275
+ local _, warning = connect(previous, effect)
276
+ if warning then
277
+ table.insert(warnings, warning)
278
+ end
279
+ table.insert(created, Paths.of(effect))
280
+ previous = effect
281
+ end
282
+
283
+ local sink = Instance.new(if kind == "world" then "AudioEmitter" else "AudioDeviceOutput")
284
+ sink.Name = if kind == "world" then "Emitter" else "Output"
285
+ sink.Parent = parent
286
+ local _, sinkWarning = connect(previous, sink)
287
+ if sinkWarning then
288
+ table.insert(warnings, sinkWarning)
289
+ end
290
+ table.insert(created, Paths.of(sink))
291
+
292
+ local note: string? = nil
293
+ if kind == "world" then
294
+ --[[
295
+ An emitter with no listener anywhere is the single most common
296
+ way a correct graph stays silent, and it is invisible from the
297
+ emitter's own properties. Checked and said plainly.
298
+ ]]
299
+ local listeners = 0
300
+ for _, descendant in game:GetDescendants() do
301
+ if descendant:IsA("AudioListener") then
302
+ listeners += 1
303
+ end
304
+ end
305
+ if listeners == 0 then
306
+ note = "Nothing in this place has an AudioListener, so no emitter can "
307
+ .. "be heard. Put one on the camera or the character -- where it "
308
+ .. "goes decides what the player hears, so it is not created here."
309
+ end
310
+ end
311
+
312
+ return {
313
+ kind = kind,
314
+ player = Paths.of(player),
315
+ sink = Paths.of(sink),
316
+ created = created,
317
+ warnings = warnings,
318
+ note = note,
319
+ }
320
+ end)
321
+
322
+ result.undoable = undoable
323
+ return result
324
+ end
325
+
326
+ --[[
327
+ Reads an existing graph back.
328
+
329
+ Walks whatever audio instances live under a path and reports every wire in
330
+ and out of each, which is the view the Explorer will not give: a Wire shows
331
+ as a child of one instance and names the other two only in its properties, so
332
+ following a chain by hand means clicking through every node.
333
+ ]]
334
+ function Audio.inspect(params: { [string]: any }): { [string]: any }
335
+ local path = params.path
336
+ local scope: Instance = if typeof(path) == "string" and path ~= ""
337
+ then Paths.resolve(path)
338
+ else game
339
+
340
+ local nodes: { { [string]: any } } = {}
341
+ local wires: { { [string]: any } } = {}
342
+ local broken = 0
343
+
344
+ local function visit(instance: Instance)
345
+ if instance:IsA("Wire") then
346
+ local source = instance.SourceInstance
347
+ local target = instance.TargetInstance
348
+ if not instance.Connected then
349
+ broken += 1
350
+ end
351
+ table.insert(wires, {
352
+ path = Paths.of(instance),
353
+ from = if source then string.format("%s.%s", Paths.of(source), instance.SourceName) else "(nothing)",
354
+ to = if target then string.format("%s.%s", Paths.of(target), instance.TargetName) else "(nothing)",
355
+ connected = instance.Connected,
356
+ })
357
+ elseif isAudio(instance) then
358
+ local entry: { [string]: any } = {
359
+ path = Paths.of(instance),
360
+ class = instance.ClassName,
361
+ }
362
+ -- Only the properties that decide whether a node is audible; the
363
+ -- full set is what `inspect` is for.
364
+ for _, property in { "Volume", "Playing", "IsPlaying", "Looping", "AudioInteractionGroup" } do
365
+ local ok, value = pcall(function()
366
+ return (instance :: any)[property]
367
+ end)
368
+ if ok and value ~= nil then
369
+ entry[property] = Serialize.value(value)
370
+ end
371
+ end
372
+ table.insert(nodes, entry)
373
+ end
374
+ end
375
+
376
+ visit(scope)
377
+ for _, descendant in scope:GetDescendants() do
378
+ visit(descendant)
379
+ end
380
+
381
+ local note: string? = nil
382
+ if #nodes > 0 and #wires == 0 then
383
+ note = "There are audio instances here but no wires, so nothing carries sound "
384
+ .. "between them. That is silence with no error anywhere."
385
+ elseif broken > 0 then
386
+ note = string.format(
387
+ "%d wire(s) report Connected = false -- usually a pin name that does not "
388
+ .. "exist on one end, or an instance that was deleted.",
389
+ broken
390
+ )
391
+ end
392
+
393
+ return {
394
+ scope = Paths.of(scope),
395
+ nodes = nodes,
396
+ wires = wires,
397
+ nodeCount = #nodes,
398
+ wireCount = #wires,
399
+ note = note,
400
+ }
401
+ end
402
+
403
+ function Audio.register()
404
+ Dispatch.registerAll("audio", {
405
+ wire = Audio.wire,
406
+ graph = Audio.graph,
407
+ inspect = Audio.inspect,
408
+ })
409
+ end
410
+
411
+ return Audio