@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
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
]]
|
|
14
14
|
|
|
15
15
|
local AssetService = game:GetService("AssetService")
|
|
16
|
+
local InsertService = game:GetService("InsertService")
|
|
16
17
|
|
|
17
18
|
local Dispatch = require(script.Parent.Parent.Dispatch)
|
|
18
19
|
local Paths = require(script.Parent.Parent.Paths)
|
|
@@ -101,6 +102,54 @@ function Assets.insert(params: { [string]: any }): { [string]: any }
|
|
|
101
102
|
end
|
|
102
103
|
end
|
|
103
104
|
|
|
105
|
+
--[[
|
|
106
|
+
Scripts stripped BEFORE anything is parented.
|
|
107
|
+
|
|
108
|
+
The order is the whole point. A free model's scripts run the moment the
|
|
109
|
+
place is played, and one that is parented first and cleaned up a
|
|
110
|
+
fraction of a second later has already been given a foothold -- a
|
|
111
|
+
`Script` with `RunContext = Legacy` will not execute in edit mode, but
|
|
112
|
+
`Plugin` run context does, and nothing here should depend on knowing
|
|
113
|
+
which one an unknown model used.
|
|
114
|
+
|
|
115
|
+
This is the most-requested missing piece in Studio's own Toolbox: there
|
|
116
|
+
is no way to insert a model without its code, so people take the
|
|
117
|
+
geometry they want and the scripts they did not ask for together.
|
|
118
|
+
]]
|
|
119
|
+
local stripped = 0
|
|
120
|
+
if params.stripScripts == true then
|
|
121
|
+
for _, child in children do
|
|
122
|
+
if child:IsA("LuaSourceContainer") then
|
|
123
|
+
child:Destroy()
|
|
124
|
+
stripped += 1
|
|
125
|
+
else
|
|
126
|
+
for _, descendant in child:GetDescendants() do
|
|
127
|
+
if descendant:IsA("LuaSourceContainer") then
|
|
128
|
+
descendant:Destroy()
|
|
129
|
+
stripped += 1
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
-- A model that was nothing BUT scripts is now empty, and parenting
|
|
135
|
+
-- nothing silently is worse than saying so.
|
|
136
|
+
local remaining: { Instance } = {}
|
|
137
|
+
for _, child in children do
|
|
138
|
+
if child.Parent ~= nil or child:IsDescendantOf(game) or child.Name ~= "" then
|
|
139
|
+
if not child:IsA("LuaSourceContainer") then
|
|
140
|
+
table.insert(remaining, child)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
children = remaining
|
|
145
|
+
if #children == 0 then
|
|
146
|
+
Dispatch.fail(
|
|
147
|
+
"EMPTY_ASSET",
|
|
148
|
+
string.format("Asset %d was nothing but scripts, and stripScripts removed them all.", assetId)
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
104
153
|
local inserted: { string } = {}
|
|
105
154
|
local _, undoable = Undo.record("MCPInsertAsset", "MCP insert asset", function()
|
|
106
155
|
for _, child in children do
|
|
@@ -132,8 +181,9 @@ function Assets.insert(params: { [string]: any }): { [string]: any }
|
|
|
132
181
|
return {
|
|
133
182
|
inserted = inserted,
|
|
134
183
|
assetId = assetId,
|
|
135
|
-
scriptCount = scriptCount,
|
|
136
|
-
scripts = if scriptCount > 0 then scriptNames else nil,
|
|
184
|
+
scriptCount = if params.stripScripts == true then 0 else scriptCount,
|
|
185
|
+
scripts = if scriptCount > 0 and params.stripScripts ~= true then scriptNames else nil,
|
|
186
|
+
strippedScripts = if params.stripScripts == true then stripped else nil,
|
|
137
187
|
undoable = undoable,
|
|
138
188
|
}
|
|
139
189
|
end
|
|
@@ -342,8 +392,242 @@ function Assets.bake(params: { [string]: any }): { [string]: any }
|
|
|
342
392
|
}
|
|
343
393
|
end
|
|
344
394
|
|
|
395
|
+
--[[
|
|
396
|
+
The audio library, which the Creator Store index answers badly.
|
|
397
|
+
|
|
398
|
+
`assets op="search" category="audio"` used to go through the same public
|
|
399
|
+
index the model search uses, and it came back with a name, a creator and a
|
|
400
|
+
vote ratio -- none of which is how anyone chooses a sound. The engine has a
|
|
401
|
+
purpose-built search for this, and it returns the fields that actually
|
|
402
|
+
decide: how long the clip is, whether it is music or a sound effect, who
|
|
403
|
+
performed it, and whether Roblox endorses it.
|
|
404
|
+
|
|
405
|
+
Measured against a live session: "footstep" returns 30 results carrying
|
|
406
|
+
Title, Artist, Duration, AudioType, Tags and IsEndorsed. Duration alone is
|
|
407
|
+
the difference between a footstep and a three-minute track that happens to
|
|
408
|
+
mention footsteps in its description -- and the old path could not tell them
|
|
409
|
+
apart.
|
|
410
|
+
]]
|
|
411
|
+
function Assets.audio(params: { [string]: any }): { [string]: any }
|
|
412
|
+
local keyword = params.keyword
|
|
413
|
+
if typeof(keyword) ~= "string" or keyword == "" then
|
|
414
|
+
Dispatch.fail("BAD_PARAMS", "audio search needs a `keyword`.")
|
|
415
|
+
end
|
|
416
|
+
local limit = math.clamp(tonumber(params.limit) or 10, 1, 50)
|
|
417
|
+
|
|
418
|
+
local okParams, search = pcall(function()
|
|
419
|
+
local p = Instance.new("AudioSearchParams")
|
|
420
|
+
p.SearchKeyword = keyword
|
|
421
|
+
--[[
|
|
422
|
+
Both bounds are optional and both are worth passing through. A search
|
|
423
|
+
for a footstep wants something under two seconds; a search for
|
|
424
|
+
background music wants the opposite, and without a length filter the
|
|
425
|
+
two queries return the same list.
|
|
426
|
+
]]
|
|
427
|
+
local minimum = tonumber(params.minDuration)
|
|
428
|
+
local maximum = tonumber(params.maxDuration)
|
|
429
|
+
if minimum ~= nil then
|
|
430
|
+
(p :: any).MinDuration = math.max(0, minimum)
|
|
431
|
+
end
|
|
432
|
+
if maximum ~= nil then
|
|
433
|
+
(p :: any).MaxDuration = math.max(0, maximum)
|
|
434
|
+
end
|
|
435
|
+
--[[
|
|
436
|
+
Always set, never left to the engine.
|
|
437
|
+
|
|
438
|
+
`AudioSubType` defaults to Music, and that default is a trap rather
|
|
439
|
+
than a preference: measured, a search for "footstep" with the default
|
|
440
|
+
returns "Silent Footsteps II" at 153 seconds, "Footsteps in the Dark"
|
|
441
|
+
at 268, and thirty more like them -- ambient tracks whose titles
|
|
442
|
+
mention footsteps. The same search as SoundEffect returns
|
|
443
|
+
"JjBg_Grass_Footstep_6" at one second.
|
|
444
|
+
|
|
445
|
+
Nothing about the empty result said "you searched the music library".
|
|
446
|
+
So the default here is SoundEffect, which is what someone typing a
|
|
447
|
+
noise into a game engine means, and Music is something you ask for.
|
|
448
|
+
]]
|
|
449
|
+
local subType = if typeof(params.audioType) == "string" and params.audioType ~= ""
|
|
450
|
+
then params.audioType
|
|
451
|
+
else "SoundEffect"
|
|
452
|
+
;(p :: any).AudioSubType = (Enum :: any).AudioSubType[subType]
|
|
453
|
+
return p
|
|
454
|
+
end)
|
|
455
|
+
if not okParams then
|
|
456
|
+
Dispatch.fail(
|
|
457
|
+
"BAD_PARAMS",
|
|
458
|
+
string.format("Those search parameters were refused: %s", tostring(search)),
|
|
459
|
+
'audioType must be one of the Enum.AudioSubType names, e.g. "Music" or "SoundEffect".'
|
|
460
|
+
)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
local okSearch, pages = pcall(function()
|
|
464
|
+
return (AssetService :: any):SearchAudioAsync(search)
|
|
465
|
+
end)
|
|
466
|
+
if not okSearch then
|
|
467
|
+
Dispatch.fail(
|
|
468
|
+
"AUDIO_SEARCH_FAILED",
|
|
469
|
+
string.format("The audio search failed: %s", tostring(pages))
|
|
470
|
+
)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
local items: { { [string]: any } } = {}
|
|
474
|
+
local page = pages:GetCurrentPage()
|
|
475
|
+
for _, entry in page do
|
|
476
|
+
if #items >= limit then
|
|
477
|
+
break
|
|
478
|
+
end
|
|
479
|
+
local row = entry :: { [string]: any }
|
|
480
|
+
table.insert(items, {
|
|
481
|
+
assetId = tostring(row.Id),
|
|
482
|
+
title = tostring(row.Title),
|
|
483
|
+
artist = if row.Artist ~= nil and tostring(row.Artist) ~= "" then tostring(row.Artist) else nil,
|
|
484
|
+
-- Seconds, rounded. The API returns a float and nobody picking a
|
|
485
|
+
-- sound effect cares about the third decimal place.
|
|
486
|
+
duration = math.round(tonumber(row.Duration) or 0),
|
|
487
|
+
audioType = (tostring(row.AudioType):gsub("Enum%.AudioSubType%.", "")),
|
|
488
|
+
endorsed = row.IsEndorsed == true,
|
|
489
|
+
})
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
items = items,
|
|
494
|
+
count = #items,
|
|
495
|
+
keyword = keyword,
|
|
496
|
+
audioType = if typeof(params.audioType) == "string" and params.audioType ~= ""
|
|
497
|
+
then params.audioType
|
|
498
|
+
else "SoundEffect",
|
|
499
|
+
}
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
--[[
|
|
503
|
+
Looks inside a published asset without putting it in the place.
|
|
504
|
+
|
|
505
|
+
`insert` already reports what came in, and reporting it afterwards is one
|
|
506
|
+
Ctrl+Z too late to be reassuring: the scripts are in the data model by then,
|
|
507
|
+
and the model may have parented things outside itself on the way in.
|
|
508
|
+
`LoadAssetAsync` builds the asset in memory instead, which is where it can be
|
|
509
|
+
read and then dropped.
|
|
510
|
+
|
|
511
|
+
So this is the safe half of the existing script warning. The Creator Store
|
|
512
|
+
index says whether an asset has scripts at all; this says which ones, what
|
|
513
|
+
class they are, and what else is in there -- before anything is committed.
|
|
514
|
+
|
|
515
|
+
The loaded container is destroyed on every path. It is never parented, so it
|
|
516
|
+
is invisible to the data model, to undo history and to the user.
|
|
517
|
+
]]
|
|
518
|
+
function Assets.peek(params: { [string]: any }): { [string]: any }
|
|
519
|
+
local assetId = tonumber(params.assetId)
|
|
520
|
+
if assetId == nil or assetId <= 0 then
|
|
521
|
+
Dispatch.fail("BAD_PARAMS", "peek needs a numeric `assetId`.")
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
--[[
|
|
525
|
+
Loaded the way `insert` loads, not the way it is tempting to.
|
|
526
|
+
|
|
527
|
+
`AssetService:LoadAssetAsync` enforces ownership: it opens assets the
|
|
528
|
+
signed-in user owns or has been given, and refuses everything else with
|
|
529
|
+
"User is not authorized to access Asset". `InsertService:LoadAsset` does
|
|
530
|
+
not -- it is what the Toolbox itself uses, and it reaches any public
|
|
531
|
+
model.
|
|
532
|
+
|
|
533
|
+
Using the first one made this tool useless at the one job it exists for.
|
|
534
|
+
Measured: a free door from the Creator Store, two scripts inside it,
|
|
535
|
+
refused by `peek` and inserted by `insert` moments later. The SAFE
|
|
536
|
+
preview was weaker than the unsafe insert, so the only way to see what
|
|
537
|
+
was in a model was to put it in the place first -- which is precisely
|
|
538
|
+
what `peek` was written to avoid.
|
|
539
|
+
|
|
540
|
+
Nothing is parented here, so reaching further costs nothing: the model is
|
|
541
|
+
read in memory and destroyed before this function returns.
|
|
542
|
+
]]
|
|
543
|
+
--[[
|
|
544
|
+
`game:GetObjects`, the same loader `insert` uses -- see the note there.
|
|
545
|
+
|
|
546
|
+
Two wrong loaders were tried before this one, and both failed the same
|
|
547
|
+
way: `AssetService:LoadAssetAsync` and `InsertService:LoadAsset` each
|
|
548
|
+
enforce ownership and refuse any Creator Store model with "User is not
|
|
549
|
+
authorized to access Asset". That made the SAFE preview weaker than the
|
|
550
|
+
unsafe insert -- measured, a free door with two scripts in it was refused
|
|
551
|
+
by `peek` and inserted by `insert` moments later, so the only way to see
|
|
552
|
+
inside a model was to put it in the place first.
|
|
553
|
+
|
|
554
|
+
`GetObjects` is the plugin-security path Studio's own toolbox uses and it
|
|
555
|
+
reaches everything `insert` reaches, which is the only correct answer
|
|
556
|
+
here: a preview that cannot see what the insert would bring in is not a
|
|
557
|
+
preview of anything.
|
|
558
|
+
|
|
559
|
+
It returns top-level instances rather than a wrapper model, so they are
|
|
560
|
+
walked directly.
|
|
561
|
+
]]
|
|
562
|
+
local ok, loaded = pcall(function()
|
|
563
|
+
return game:GetObjects("rbxassetid://" .. assetId)
|
|
564
|
+
end)
|
|
565
|
+
if not ok then
|
|
566
|
+
Dispatch.fail(
|
|
567
|
+
"ASSET_UNAVAILABLE",
|
|
568
|
+
string.format("Could not load asset %d: %s", assetId, tostring(loaded)),
|
|
569
|
+
"The asset may be private, deleted, or not a model."
|
|
570
|
+
)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
-- An array of roots, not one wrapper: `GetObjects` hands back whatever the
|
|
574
|
+
-- asset holds at its top level.
|
|
575
|
+
local roots = loaded :: { Instance }
|
|
576
|
+
local classes: { [string]: number } = {}
|
|
577
|
+
local scripts: { string } = {}
|
|
578
|
+
local total = 0
|
|
579
|
+
|
|
580
|
+
for _, root in roots do
|
|
581
|
+
total += 1
|
|
582
|
+
classes[root.ClassName] = (classes[root.ClassName] or 0) + 1
|
|
583
|
+
if root:IsA("LuaSourceContainer") and #scripts < 25 then
|
|
584
|
+
table.insert(scripts, string.format("%s (%s)", root.Name, root.ClassName))
|
|
585
|
+
end
|
|
586
|
+
for _, descendant in root:GetDescendants() do
|
|
587
|
+
total += 1
|
|
588
|
+
classes[descendant.ClassName] = (classes[descendant.ClassName] or 0) + 1
|
|
589
|
+
if descendant:IsA("LuaSourceContainer") and #scripts < 25 then
|
|
590
|
+
table.insert(scripts, string.format("%s (%s)", descendant:GetFullName(), descendant.ClassName))
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
local breakdown: { { [string]: any } } = {}
|
|
596
|
+
for className, count in classes do
|
|
597
|
+
table.insert(breakdown, { className = className, count = count })
|
|
598
|
+
end
|
|
599
|
+
table.sort(breakdown, function(a, b)
|
|
600
|
+
if a.count == b.count then
|
|
601
|
+
return a.className < b.className
|
|
602
|
+
end
|
|
603
|
+
return a.count > b.count
|
|
604
|
+
end)
|
|
605
|
+
|
|
606
|
+
local names: { string } = {}
|
|
607
|
+
for _, root in roots do
|
|
608
|
+
table.insert(names, string.format("%s (%s)", root.Name, root.ClassName))
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
-- Destroyed before returning, not after: the reply is built from plain
|
|
612
|
+
-- strings and numbers, so nothing in it outlives the instances it came from.
|
|
613
|
+
for _, root in roots do
|
|
614
|
+
root:Destroy()
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
return {
|
|
618
|
+
assetId = assetId,
|
|
619
|
+
roots = names,
|
|
620
|
+
descendants = total,
|
|
621
|
+
classes = breakdown,
|
|
622
|
+
scripts = scripts,
|
|
623
|
+
scriptCount = #scripts,
|
|
624
|
+
}
|
|
625
|
+
end
|
|
626
|
+
|
|
345
627
|
function Assets.register()
|
|
346
628
|
Dispatch.registerAll("assets", {
|
|
629
|
+
audio = Assets.audio,
|
|
630
|
+
peek = Assets.peek,
|
|
347
631
|
insert = Assets.insert,
|
|
348
632
|
bake = Assets.bake,
|
|
349
633
|
})
|