@el4cteo/rbx-studio-mcp 0.6.1 → 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 +8 -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/anim.js +159 -0
- package/dist/tools/anim.js.map +1 -0
- package/dist/tools/audio.js +96 -0
- package/dist/tools/audio.js.map +1 -0
- package/dist/tools/character.js +95 -5
- package/dist/tools/character.js.map +1 -1
- package/dist/tools/data.js +292 -0
- package/dist/tools/data.js.map +1 -0
- package/dist/tools/device.js +77 -7
- package/dist/tools/device.js.map +1 -1
- package/dist/tools/discover.js +80 -4
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/exec.js +96 -2
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/input.js +35 -9
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/perf.js +74 -7
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/scripts.js +162 -6
- 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 +675 -51
- package/dist/tools/world.js.map +1 -1
- package/package.json +2 -2
- package/plugin/src/Commands.luau +31 -7
- package/plugin/src/Config.luau +65 -65
- package/plugin/src/Console.luau +1909 -1843
- package/plugin/src/Emulation.luau +172 -0
- package/plugin/src/Phrase.luau +816 -618
- package/plugin/src/Png.luau +8 -4
- package/plugin/src/Prompt.luau +965 -961
- package/plugin/src/Secret.luau +86 -0
- package/plugin/src/Serialize.luau +440 -8
- package/plugin/src/Undo.luau +94 -6
- package/plugin/src/handlers/Anim.luau +897 -0
- package/plugin/src/handlers/Assets.luau +286 -2
- package/plugin/src/handlers/Audio.luau +411 -0
- package/plugin/src/handlers/Capture.luau +155 -20
- package/plugin/src/handlers/Character.luau +823 -361
- package/plugin/src/handlers/Data.luau +539 -0
- package/plugin/src/handlers/Device.luau +394 -139
- package/plugin/src/handlers/Discover.luau +685 -363
- package/plugin/src/handlers/Geometry.luau +722 -450
- package/plugin/src/handlers/Instances.luau +84 -4
- package/plugin/src/handlers/Perf.luau +227 -0
- package/plugin/src/handlers/Scripts.luau +673 -539
- package/plugin/src/handlers/Session.luau +3 -0
- package/plugin/src/handlers/Spatial.luau +334 -0
- package/plugin/src/handlers/Viewport.luau +268 -0
- package/plugin/src/handlers/World.luau +89 -15
- package/plugin/src/init.server.luau +9 -1
- package/scripts/build-plugin.mjs +20 -0
- package/scripts/check-plugin.mjs +171 -124
|
@@ -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)
|
|
@@ -21,6 +21,7 @@ local Stats = game:GetService("Stats")
|
|
|
21
21
|
local Dispatch = require(script.Parent.Parent.Dispatch)
|
|
22
22
|
local LogBuffer = require(script.Parent.Parent.LogBuffer)
|
|
23
23
|
local Paths = require(script.Parent.Parent.Paths)
|
|
24
|
+
local Scope = require(script.Parent.Parent.Scope)
|
|
24
25
|
|
|
25
26
|
-- The log holds thousands of lines in a busy session; an unbounded tail would
|
|
26
27
|
-- swamp any context window.
|
|
@@ -506,6 +507,231 @@ function Perf.coverage(params: { [string]: any }): { [string]: any }
|
|
|
506
507
|
}
|
|
507
508
|
end
|
|
508
509
|
|
|
510
|
+
--[[
|
|
511
|
+
Every reference in the place that points at nothing.
|
|
512
|
+
|
|
513
|
+
A dead asset id is the quietest failure Roblox has. A Sound whose id was
|
|
514
|
+
deleted, made private, or mistyped plays silence; a Decal shows nothing; an
|
|
515
|
+
Animation does nothing. None of them errors, none of them warns, and the
|
|
516
|
+
instance looks perfectly healthy in the Explorer -- the id is a string, and
|
|
517
|
+
it is still a string. The only way to find them today is to play the game and
|
|
518
|
+
notice something missing, which nobody does reliably for the twentieth sound
|
|
519
|
+
effect.
|
|
520
|
+
|
|
521
|
+
`ContentProvider:PreloadAsync` settles it. Measured: a real id came back
|
|
522
|
+
`IsLoaded=true, TimeLength=0.632`, a dead one `IsLoaded=false, TimeLength=0`.
|
|
523
|
+
So this collects every asset-bearing property in the place, asks the engine
|
|
524
|
+
to fetch them, and reports the ones that did not arrive.
|
|
525
|
+
|
|
526
|
+
One honest cost: preloading a dead id writes a line to Studio's Output
|
|
527
|
+
window. That is the engine talking, not this tool, and it corroborates the
|
|
528
|
+
finding rather than contradicting it -- but it is the user's Output window,
|
|
529
|
+
so the tool says up front that it will happen.
|
|
530
|
+
]]
|
|
531
|
+
|
|
532
|
+
--[[
|
|
533
|
+
Assets fetched in one audit.
|
|
534
|
+
|
|
535
|
+
Preloading is network work, and a place with two thousand sounds would spend
|
|
536
|
+
minutes on it while Studio sat still. The cap is generous enough to cover
|
|
537
|
+
most places whole and small enough that the worst case is seconds, and the
|
|
538
|
+
reply says when it stopped short rather than implying it checked everything.
|
|
539
|
+
]]
|
|
540
|
+
local MAX_ASSETS = 200
|
|
541
|
+
|
|
542
|
+
--[[
|
|
543
|
+
How long the whole fetch may take before this gives up on it.
|
|
544
|
+
|
|
545
|
+
Long enough for two hundred ordinary assets over a slow connection, short
|
|
546
|
+
enough that a stuck fetch is a slow answer rather than a hung Studio.
|
|
547
|
+
]]
|
|
548
|
+
local PRELOAD_SECONDS = 20
|
|
549
|
+
|
|
550
|
+
--[[
|
|
551
|
+
Which property on which class holds a content id.
|
|
552
|
+
|
|
553
|
+
Written out rather than discovered from the API dump, because "a string
|
|
554
|
+
property whose name sounds like an asset" catches `Name` and misses
|
|
555
|
+
`MeshContent`. The list is short, and being wrong here means reporting a
|
|
556
|
+
healthy place as broken.
|
|
557
|
+
]]
|
|
558
|
+
local ASSET_FIELDS: { { className: string, property: string } } = {
|
|
559
|
+
{ className = "Sound", property = "SoundId" },
|
|
560
|
+
{ className = "Decal", property = "Texture" },
|
|
561
|
+
{ className = "Texture", property = "Texture" },
|
|
562
|
+
{ className = "ImageLabel", property = "Image" },
|
|
563
|
+
{ className = "ImageButton", property = "Image" },
|
|
564
|
+
{ className = "Animation", property = "AnimationId" },
|
|
565
|
+
{ className = "Sky", property = "SkyboxUp" },
|
|
566
|
+
{ className = "ParticleEmitter", property = "Texture" },
|
|
567
|
+
{ className = "Beam", property = "Texture" },
|
|
568
|
+
{ className = "Trail", property = "Texture" },
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function Perf.audit(params: { [string]: any }): { [string]: any }
|
|
572
|
+
local ContentProvider = game:GetService("ContentProvider")
|
|
573
|
+
|
|
574
|
+
local unset: { { [string]: any } } = {}
|
|
575
|
+
local candidates: { { instance: Instance, id: string, property: string } } = {}
|
|
576
|
+
local disabled: { string } = {}
|
|
577
|
+
local duplicates: { string } = {}
|
|
578
|
+
|
|
579
|
+
local seenNames: { [string]: boolean } = {}
|
|
580
|
+
|
|
581
|
+
for _, descendant in game:GetDescendants() do
|
|
582
|
+
if Scope.isNoisy(descendant) then
|
|
583
|
+
continue
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
for _, field in ASSET_FIELDS do
|
|
587
|
+
if not descendant:IsA(field.className) then
|
|
588
|
+
continue
|
|
589
|
+
end
|
|
590
|
+
local ok, value = pcall(function()
|
|
591
|
+
return (descendant :: any)[field.property]
|
|
592
|
+
end)
|
|
593
|
+
if not ok or typeof(value) ~= "string" then
|
|
594
|
+
continue
|
|
595
|
+
end
|
|
596
|
+
if value == "" then
|
|
597
|
+
table.insert(unset, { path = Paths.of(descendant), property = field.property })
|
|
598
|
+
elseif #candidates < MAX_ASSETS then
|
|
599
|
+
table.insert(candidates, { instance = descendant, id = value, property = field.property })
|
|
600
|
+
end
|
|
601
|
+
break
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
--[[
|
|
605
|
+
`Disabled` exists on BaseScript, not on ModuleScript -- reading it off
|
|
606
|
+
the wrong one throws, which is how this check failed the first time it
|
|
607
|
+
was written. Narrowed to the class that has it.
|
|
608
|
+
]]
|
|
609
|
+
if descendant:IsA("BaseScript") and (descendant :: any).Disabled == true then
|
|
610
|
+
if #disabled < 25 then
|
|
611
|
+
table.insert(disabled, Paths.of(descendant))
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
--[[
|
|
616
|
+
Same-named siblings, which is what breaks `WaitForChild`: it returns
|
|
617
|
+
whichever one the engine reaches first, and that is not stable.
|
|
618
|
+
|
|
619
|
+
Geometry is excluded, and that exclusion is the whole value of the
|
|
620
|
+
check. Measured on a real place: the first version reported 25
|
|
621
|
+
duplicates and every one of them was a wall bar, a bench or a table
|
|
622
|
+
leg in a blockout -- decorative parts nobody ever looks up by name.
|
|
623
|
+
The real findings were buried under them.
|
|
624
|
+
|
|
625
|
+
A duplicated script, RemoteEvent, value or GUI element is a different
|
|
626
|
+
matter: those exist to be found by name, and two of them means code
|
|
627
|
+
somewhere is reaching for whichever the engine happened to index
|
|
628
|
+
first.
|
|
629
|
+
]]
|
|
630
|
+
local parent = descendant.Parent
|
|
631
|
+
if parent ~= nil and not descendant:IsA("BasePart") and not descendant:IsA("Attachment") then
|
|
632
|
+
local key = tostring(parent:GetDebugId(8)) .. "/" .. descendant.Name
|
|
633
|
+
if seenNames[key] and #duplicates < 25 then
|
|
634
|
+
table.insert(duplicates, Paths.of(descendant))
|
|
635
|
+
end
|
|
636
|
+
seenNames[key] = true
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
--[[
|
|
641
|
+
Preloaded in one call rather than one per asset.
|
|
642
|
+
|
|
643
|
+
`PreloadAsync` takes the whole list and fetches in parallel; asking for
|
|
644
|
+
them one at a time would serialise two hundred network round trips into
|
|
645
|
+
something that looks like a hang.
|
|
646
|
+
]]
|
|
647
|
+
local toLoad: { Instance } = {}
|
|
648
|
+
for _, entry in candidates do
|
|
649
|
+
table.insert(toLoad, entry.instance)
|
|
650
|
+
end
|
|
651
|
+
--[[
|
|
652
|
+
Given a deadline, because `PreloadAsync` has none of its own.
|
|
653
|
+
|
|
654
|
+
It blocks until every id in the list resolves, and an id that resolves
|
|
655
|
+
slowly -- or a network that has stopped answering -- blocks it for as
|
|
656
|
+
long as that takes. Measured: an audit against a running playtest sat
|
|
657
|
+
still long enough for the request to time out and the playtest to end
|
|
658
|
+
with it. A tool that can stop Studio responding is worse than one that
|
|
659
|
+
reports less.
|
|
660
|
+
|
|
661
|
+
Run on its own thread and waited on here, so the wait can end without
|
|
662
|
+
the work being cancelled -- there is no way to cancel it, and pretending
|
|
663
|
+
otherwise would leave a thread writing into a finished request.
|
|
664
|
+
]]
|
|
665
|
+
local preloaded = true
|
|
666
|
+
if #toLoad > 0 then
|
|
667
|
+
local done = false
|
|
668
|
+
task.spawn(function()
|
|
669
|
+
pcall(function()
|
|
670
|
+
ContentProvider:PreloadAsync(toLoad)
|
|
671
|
+
end)
|
|
672
|
+
done = true
|
|
673
|
+
end)
|
|
674
|
+
|
|
675
|
+
local waited = 0
|
|
676
|
+
while not done and waited < PRELOAD_SECONDS do
|
|
677
|
+
waited += task.wait(0.05)
|
|
678
|
+
end
|
|
679
|
+
preloaded = done
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
local dead: { { [string]: any } } = {}
|
|
683
|
+
for _, entry in candidates do
|
|
684
|
+
local instance = entry.instance
|
|
685
|
+
local loaded = true
|
|
686
|
+
if instance:IsA("Sound") then
|
|
687
|
+
-- A Sound that fetched has a length; one that did not is still zero.
|
|
688
|
+
loaded = (instance :: Sound).TimeLength > 0
|
|
689
|
+
else
|
|
690
|
+
local ok, value = pcall(function()
|
|
691
|
+
return (instance :: any).IsLoaded
|
|
692
|
+
end)
|
|
693
|
+
if ok and typeof(value) == "boolean" then
|
|
694
|
+
loaded = value
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
--[[
|
|
698
|
+
Nothing is called dead once the fetch has been abandoned.
|
|
699
|
+
|
|
700
|
+
An id that simply had not arrived yet is indistinguishable here from
|
|
701
|
+
one that points at nothing, and "this sound is broken" about a sound
|
|
702
|
+
that is fine is the one answer this check must never give.
|
|
703
|
+
]]
|
|
704
|
+
if not loaded and not preloaded then
|
|
705
|
+
continue
|
|
706
|
+
end
|
|
707
|
+
if not loaded and #dead < 50 then
|
|
708
|
+
table.insert(dead, {
|
|
709
|
+
path = Paths.of(instance),
|
|
710
|
+
property = entry.property,
|
|
711
|
+
id = entry.id,
|
|
712
|
+
})
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
return {
|
|
717
|
+
checked = #candidates,
|
|
718
|
+
-- Reported beside the fetched count, because "3 checked" on a place with
|
|
719
|
+
-- fourteen blank ids reads as if the audit barely ran. A blank id needs
|
|
720
|
+
-- no fetch to be judged; it is still a reference that points nowhere.
|
|
721
|
+
scanned = #candidates + #unset,
|
|
722
|
+
truncated = #candidates >= MAX_ASSETS,
|
|
723
|
+
-- Said out loud, because an empty `dead` list from an abandoned fetch
|
|
724
|
+
-- reads exactly like a clean place.
|
|
725
|
+
incomplete = if not preloaded then true else nil,
|
|
726
|
+
dead = dead,
|
|
727
|
+
deadCount = #dead,
|
|
728
|
+
unset = unset,
|
|
729
|
+
unsetCount = #unset,
|
|
730
|
+
disabledScripts = disabled,
|
|
731
|
+
duplicateNames = duplicates,
|
|
732
|
+
}
|
|
733
|
+
end
|
|
734
|
+
|
|
509
735
|
function Perf.register(host: Plugin?)
|
|
510
736
|
pluginRef = host
|
|
511
737
|
|
|
@@ -649,6 +875,7 @@ function Perf.scene(params: { [string]: any }): { [string]: any }
|
|
|
649
875
|
end
|
|
650
876
|
|
|
651
877
|
Dispatch.registerAll("perf", {
|
|
878
|
+
audit = Perf.audit,
|
|
652
879
|
console = Perf.console,
|
|
653
880
|
snapshot = function()
|
|
654
881
|
return Perf.snapshot()
|