@el4cteo/rbx-studio-mcp 0.7.0 → 0.7.2
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/bridge/api.js +8 -8
- package/dist/bridge/api.js.map +1 -1
- package/dist/bridge/rpc.js +18 -18
- package/dist/bridge/rpc.js.map +1 -1
- package/dist/bridge/server.js +17 -21
- package/dist/bridge/server.js.map +1 -1
- package/dist/lib/opencloud.js +7 -7
- package/dist/lib/opencloud.js.map +1 -1
- package/dist/lib/tool.js +7 -0
- package/dist/lib/tool.js.map +1 -1
- package/dist/tools/input.js +9 -7
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/scripts.js +55 -12
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/world.js +0 -7
- package/dist/tools/world.js.map +1 -1
- package/package.json +4 -3
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Console.luau +21 -21
- package/plugin/src/Emulation.luau +8 -8
- package/plugin/src/ScriptEdit.luau +14 -14
- package/plugin/src/Secret.luau +0 -9
- package/plugin/src/Transport.luau +58 -1
- package/plugin/src/Visuals.luau +8 -8
- package/plugin/src/handlers/Anim.luau +50 -50
- package/plugin/src/handlers/Character.luau +8 -8
- package/plugin/src/handlers/Discover.luau +11 -11
- package/plugin/src/handlers/Input.luau +43 -9
- package/plugin/src/handlers/Instances.luau +5 -5
- package/plugin/src/handlers/Scripts.luau +68 -19
- package/plugin/src/init.server.luau +939 -883
- package/scripts/test-bridge.mjs +40 -0
- package/scripts/test-failover.mjs +107 -99
- package/scripts/test-live.mjs +115 -0
- package/scripts/test-tools.mjs +39 -0
- package/scripts/test-transport.mjs +71 -58
|
@@ -130,22 +130,6 @@ local function flatten(node: Instance, prefix: string, into: { { [string]: any }
|
|
|
130
130
|
end
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
-
--[[
|
|
134
|
-
Which rig an animation was made for, read off the joints it moves.
|
|
135
|
-
|
|
136
|
-
This matters more than it sounds. An R15 animation played on an R6 character
|
|
137
|
-
does nothing at all -- no error, no warning, no movement -- because the joint
|
|
138
|
-
names it addresses ("LeftUpperArm") do not exist on that rig ("Left Arm").
|
|
139
|
-
It is one of the quietest failures on the platform, and the asset id carries
|
|
140
|
-
no hint of which kind it is.
|
|
141
|
-
|
|
142
|
-
The two skeletons are told apart by naming rather than by counting. R6 uses
|
|
143
|
-
six limbs with spaces in their names, unchanged since 2007. R15 splits each
|
|
144
|
-
limb in two and writes them without spaces. Anything that matches neither is
|
|
145
|
-
a custom rig, and is reported as such rather than guessed at -- a wrong
|
|
146
|
-
confident answer here is worse than "unknown", because the whole point of
|
|
147
|
-
the field is to be trusted.
|
|
148
|
-
]]
|
|
149
133
|
--[[
|
|
150
134
|
Which joint hangs off which, for the two rigs Roblox ships.
|
|
151
135
|
|
|
@@ -207,6 +191,22 @@ local R15_PARTS = {
|
|
|
207
191
|
RightLowerArm = true,
|
|
208
192
|
}
|
|
209
193
|
|
|
194
|
+
--[[
|
|
195
|
+
Which rig an animation was made for, read off the joints it moves.
|
|
196
|
+
|
|
197
|
+
This matters more than it sounds. An R15 animation played on an R6 character
|
|
198
|
+
does nothing at all -- no error, no warning, no movement -- because the joint
|
|
199
|
+
names it addresses ("LeftUpperArm") do not exist on that rig ("Left Arm").
|
|
200
|
+
It is one of the quietest failures on the platform, and the asset id carries
|
|
201
|
+
no hint of which kind it is.
|
|
202
|
+
|
|
203
|
+
The two skeletons are told apart by naming rather than by counting. R6 uses
|
|
204
|
+
six limbs with spaces in their names, unchanged since 2007. R15 splits each
|
|
205
|
+
limb in two and writes them without spaces. Anything that matches neither is
|
|
206
|
+
a custom rig, and is reported as such rather than guessed at -- a wrong
|
|
207
|
+
confident answer here is worse than "unknown", because the whole point of
|
|
208
|
+
the field is to be trusted.
|
|
209
|
+
]]
|
|
210
210
|
local function rigOf(joints: { [string]: boolean }): (string, string?)
|
|
211
211
|
local r6, r15 = 0, 0
|
|
212
212
|
for joint in joints do
|
|
@@ -418,21 +418,6 @@ function Anim.read(params: { [string]: any }): { [string]: any }
|
|
|
418
418
|
}
|
|
419
419
|
end
|
|
420
420
|
|
|
421
|
-
--[[
|
|
422
|
-
Builds a KeyframeSequence in memory and registers it for playback.
|
|
423
|
-
|
|
424
|
-
The registration is what makes this more than an instance tree: it returns a
|
|
425
|
-
content id that an Animation can use immediately, in this Studio, with no
|
|
426
|
-
upload and no moderation wait. That is the whole loop an agent needs to try
|
|
427
|
-
an idea -- build, set AnimationId, look -- and it is entirely reversible,
|
|
428
|
-
because nothing left the session.
|
|
429
|
-
|
|
430
|
-
Poses arrive flat, named by joint, and are nested under one root pose here.
|
|
431
|
-
Flat is what a caller can write and nesting is what the engine resolves, and
|
|
432
|
-
the rig's own hierarchy is not knowable from a list of joint names -- so a
|
|
433
|
-
single root is the honest structure: it animates the named joints and claims
|
|
434
|
-
nothing about how they connect.
|
|
435
|
-
]]
|
|
436
421
|
--[[
|
|
437
422
|
The joint tree to nest poses against.
|
|
438
423
|
|
|
@@ -477,6 +462,21 @@ local function parentMap(params: { [string]: any }, joints: { [string]: boolean
|
|
|
477
462
|
return R6_PARENT, "R6"
|
|
478
463
|
end
|
|
479
464
|
|
|
465
|
+
--[[
|
|
466
|
+
Builds a KeyframeSequence in memory and registers it for playback.
|
|
467
|
+
|
|
468
|
+
The registration is what makes this more than an instance tree: it returns a
|
|
469
|
+
content id that an Animation can use immediately, in this Studio, with no
|
|
470
|
+
upload and no moderation wait. That is the whole loop an agent needs to try
|
|
471
|
+
an idea -- build, set AnimationId, look -- and it is entirely reversible,
|
|
472
|
+
because nothing left the session.
|
|
473
|
+
|
|
474
|
+
Poses arrive flat, named by joint, and are nested under one root pose here.
|
|
475
|
+
Flat is what a caller can write and nesting is what the engine resolves, and
|
|
476
|
+
the rig's own hierarchy is not knowable from a list of joint names -- so a
|
|
477
|
+
single root is the honest structure: it animates the named joints and claims
|
|
478
|
+
nothing about how they connect.
|
|
479
|
+
]]
|
|
480
480
|
function Anim.build(params: { [string]: any }): { [string]: any }
|
|
481
481
|
local frames = params.keyframes
|
|
482
482
|
if typeof(frames) ~= "table" or #(frames :: { any }) == 0 then
|
|
@@ -636,25 +636,6 @@ function Anim.build(params: { [string]: any }): { [string]: any }
|
|
|
636
636
|
}
|
|
637
637
|
end
|
|
638
638
|
|
|
639
|
-
--[[
|
|
640
|
-
Plays an animation on a real rig, in edit mode, so it can be looked at.
|
|
641
|
-
|
|
642
|
-
`read` says what an animation contains and `build` makes one; neither answers
|
|
643
|
-
"does it look right", which for a piece of motion is most of the question.
|
|
644
|
-
Measured: an `Animator` loads and plays a track in an ordinary edit session
|
|
645
|
-
-- `Length` comes back populated and `IsPlaying` is true -- so the rig in the
|
|
646
|
-
place can be posed without pressing Play.
|
|
647
|
-
|
|
648
|
-
The frame is the point. Playing an animation and returning immediately shows
|
|
649
|
-
whatever pose the rig happened to be in, so this seeks to a chosen moment and
|
|
650
|
-
holds it there: play, jump to `at`, pause. Then `screenshot` sees that exact
|
|
651
|
-
pose, and asking for three different moments gives three comparable frames.
|
|
652
|
-
|
|
653
|
-
Deliberately leaves the rig posed rather than restoring it. The whole purpose
|
|
654
|
-
is to leave something on screen to photograph, and a tool that tidied up
|
|
655
|
-
before returning would always photograph the idle pose. `op="stop"` puts it
|
|
656
|
-
back.
|
|
657
|
-
]]
|
|
658
639
|
--[[
|
|
659
640
|
Puts a rig back the way it was before anything was played on it.
|
|
660
641
|
|
|
@@ -722,6 +703,25 @@ local function restPose(model: Instance, animator: Animator)
|
|
|
722
703
|
animator:StepAnimations(1 / 60)
|
|
723
704
|
end
|
|
724
705
|
|
|
706
|
+
--[[
|
|
707
|
+
Plays an animation on a real rig, in edit mode, so it can be looked at.
|
|
708
|
+
|
|
709
|
+
`read` says what an animation contains and `build` makes one; neither answers
|
|
710
|
+
"does it look right", which for a piece of motion is most of the question.
|
|
711
|
+
Measured: an `Animator` loads and plays a track in an ordinary edit session
|
|
712
|
+
-- `Length` comes back populated and `IsPlaying` is true -- so the rig in the
|
|
713
|
+
place can be posed without pressing Play.
|
|
714
|
+
|
|
715
|
+
The frame is the point. Playing an animation and returning immediately shows
|
|
716
|
+
whatever pose the rig happened to be in, so this seeks to a chosen moment and
|
|
717
|
+
holds it there: play, jump to `at`, pause. Then `screenshot` sees that exact
|
|
718
|
+
pose, and asking for three different moments gives three comparable frames.
|
|
719
|
+
|
|
720
|
+
Deliberately leaves the rig posed rather than restoring it. The whole purpose
|
|
721
|
+
is to leave something on screen to photograph, and a tool that tidied up
|
|
722
|
+
before returning would always photograph the idle pose. `op="stop"` puts it
|
|
723
|
+
back.
|
|
724
|
+
]]
|
|
725
725
|
function Anim.preview(params: { [string]: any }): { [string]: any }
|
|
726
726
|
local rigPath = params.rig
|
|
727
727
|
if typeof(rigPath) ~= "string" or rigPath == "" then
|
|
@@ -178,14 +178,6 @@ local function takeControl(model: Model): boolean
|
|
|
178
178
|
return ok
|
|
179
179
|
end
|
|
180
180
|
|
|
181
|
-
--[[
|
|
182
|
-
Walks to a point, following a computed path around obstacles.
|
|
183
|
-
|
|
184
|
-
Reports where it ended up rather than only whether the call returned, because
|
|
185
|
-
`MoveTo` succeeds at being asked and says nothing about arriving. A route
|
|
186
|
-
blocked by a wall the agent did not know about looks identical to a
|
|
187
|
-
successful walk unless the final distance is measured.
|
|
188
|
-
]]
|
|
189
181
|
--[[
|
|
190
182
|
Agent settings, shared by the route check and the walk.
|
|
191
183
|
|
|
@@ -336,6 +328,14 @@ function Character.path(params: { [string]: any }): { [string]: any }
|
|
|
336
328
|
}
|
|
337
329
|
end
|
|
338
330
|
|
|
331
|
+
--[[
|
|
332
|
+
Walks to a point, following a computed path around obstacles.
|
|
333
|
+
|
|
334
|
+
Reports where it ended up rather than only whether the call returned, because
|
|
335
|
+
`MoveTo` succeeds at being asked and says nothing about arriving. A route
|
|
336
|
+
blocked by a wall the agent did not know about looks identical to a
|
|
337
|
+
successful walk unless the final distance is measured.
|
|
338
|
+
]]
|
|
339
339
|
function Character.moveTo(params: { [string]: any }): { [string]: any }
|
|
340
340
|
local model, humanoid = humanoidOf(params)
|
|
341
341
|
|
|
@@ -184,10 +184,6 @@ function Discover.tree(params: { [string]: any }): { [string]: any }
|
|
|
184
184
|
}
|
|
185
185
|
end
|
|
186
186
|
|
|
187
|
-
--[[
|
|
188
|
-
Detailed read of specific instances. `properties` is chosen server-side from
|
|
189
|
-
the live API dump, so this handler never needs its own class table.
|
|
190
|
-
]]
|
|
191
187
|
--[[
|
|
192
188
|
The physical facts about a part, which no property panel shows.
|
|
193
189
|
|
|
@@ -323,6 +319,10 @@ local function physicsOf(target: Instance): { [string]: any }?
|
|
|
323
319
|
return facts
|
|
324
320
|
end
|
|
325
321
|
|
|
322
|
+
--[[
|
|
323
|
+
Detailed read of specific instances. `properties` is chosen server-side from
|
|
324
|
+
the live API dump, so this handler never needs its own class table.
|
|
325
|
+
]]
|
|
326
326
|
function Discover.inspect(params: { [string]: any }): { [string]: any }
|
|
327
327
|
local paths = params.paths
|
|
328
328
|
if typeof(paths) ~= "table" or #paths == 0 then
|
|
@@ -415,13 +415,6 @@ function Discover.inspect(params: { [string]: any }): { [string]: any }
|
|
|
415
415
|
return { items = results, failures = failures }
|
|
416
416
|
end
|
|
417
417
|
|
|
418
|
-
--[[
|
|
419
|
-
One search over name, class, property value and tag.
|
|
420
|
-
|
|
421
|
-
Every filter supplied must match (AND), which is what lets a single tool
|
|
422
|
-
answer "every anchored Part under Workspace whose name contains 'door'"
|
|
423
|
-
without the agent chaining three calls and intersecting the results itself.
|
|
424
|
-
]]
|
|
425
418
|
--[[
|
|
426
419
|
Runs a selector through the engine's own matcher.
|
|
427
420
|
|
|
@@ -582,6 +575,13 @@ function Discover.tags(params: { [string]: any }): { [string]: any }
|
|
|
582
575
|
}
|
|
583
576
|
end
|
|
584
577
|
|
|
578
|
+
--[[
|
|
579
|
+
One search over name, class, property value and tag.
|
|
580
|
+
|
|
581
|
+
Every filter supplied must match (AND), which is what lets a single tool
|
|
582
|
+
answer "every anchored Part under Workspace whose name contains 'door'"
|
|
583
|
+
without the agent chaining three calls and intersecting the results itself.
|
|
584
|
+
]]
|
|
585
585
|
function Discover.find(params: { [string]: any }): { [string]: any }
|
|
586
586
|
local root = if params.path then Paths.resolve(params.path) else game
|
|
587
587
|
local limit = tonumber(params.limit) or 100
|
|
@@ -301,18 +301,42 @@ end
|
|
|
301
301
|
|
|
302
302
|
makeCursor()
|
|
303
303
|
|
|
304
|
-
|
|
304
|
+
--[[
|
|
305
|
+
Some keys cannot be sent at all. Roblox keeps them for its own menus --
|
|
306
|
+
Escape, Tab, F9, and in this place's case also One -- and `SendKey` THROWS
|
|
307
|
+
for them ("permanently bound to a CoreGUI core action"). Uncaught, that
|
|
308
|
+
killed this script mid-plan: the rest of the steps never ran, nothing was
|
|
309
|
+
reported, and the tool gave up with NO_ACK after its full timeout while the
|
|
310
|
+
error sat in Studio's Output. Which keys are reserved depends on the place
|
|
311
|
+
(One is the backpack hotbar), so it is caught here rather than listed.
|
|
312
|
+
]]
|
|
313
|
+
local function refusedKey(key, err)
|
|
314
|
+
local message = tostring(err)
|
|
315
|
+
if string.find(message, "CoreGUI", 1, true) then
|
|
316
|
+
return string.format(
|
|
317
|
+
"%s was not sent: Roblox reserves it for its own menus, so no script can press it. "
|
|
318
|
+
.. "Test what the game does on that key by calling its handler another way.",
|
|
319
|
+
key
|
|
320
|
+
)
|
|
321
|
+
end
|
|
322
|
+
return string.format("%s was not sent: %s", key, message)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
local function runStep(step): boolean
|
|
305
326
|
local kind = step.kind
|
|
306
327
|
if kind == "key" then
|
|
307
328
|
local code = Enum.KeyCode[step.key]
|
|
308
|
-
|
|
329
|
+
local down = step.action ~= "release"
|
|
330
|
+
local sent, err = pcall(function()
|
|
331
|
+
virtual:SendKey(down, code, false)
|
|
332
|
+
end)
|
|
333
|
+
if not sent then
|
|
334
|
+
table.insert(notes, refusedKey(step.key, err))
|
|
335
|
+
return false
|
|
336
|
+
end
|
|
337
|
+
if down and step.action ~= "press" then
|
|
338
|
+
task.wait(step.hold or 0.08)
|
|
309
339
|
virtual:SendKey(false, code, false)
|
|
310
|
-
else
|
|
311
|
-
virtual:SendKey(true, code, false)
|
|
312
|
-
if step.action ~= "press" then
|
|
313
|
-
task.wait(step.hold or 0.08)
|
|
314
|
-
virtual:SendKey(false, code, false)
|
|
315
|
-
end
|
|
316
340
|
end
|
|
317
341
|
elseif kind == "move" then
|
|
318
342
|
moveCursor(step.x, step.y, 0.3)
|
|
@@ -364,7 +388,17 @@ for _, step in plan do
|
|
|
364
388
|
virtual:SendTextInput(step.text)
|
|
365
389
|
end
|
|
366
390
|
end
|
|
367
|
-
|
|
391
|
+
return true
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
for index, step in plan do
|
|
395
|
+
-- Guarded so one failing step can never stop the report going back.
|
|
396
|
+
local ok, done = pcall(runStep, step)
|
|
397
|
+
if not ok then
|
|
398
|
+
table.insert(notes, string.format("step %d (%s) failed: %s", index - 1, tostring(step.kind), tostring(done)))
|
|
399
|
+
elseif done then
|
|
400
|
+
table.insert(performed, step.kind)
|
|
401
|
+
end
|
|
368
402
|
if step.after ~= nil and step.after > 0 then
|
|
369
403
|
task.wait(step.after)
|
|
370
404
|
end
|
|
@@ -109,6 +109,11 @@ local function applyTags(target: Instance, tags: { [string]: any })
|
|
|
109
109
|
end
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
+
--[[
|
|
113
|
+
One property that could not be applied yet, kept for a second attempt.
|
|
114
|
+
]]
|
|
115
|
+
type Deferred = { target: Instance, name: string, spec: PropertySpec, className: string }
|
|
116
|
+
|
|
112
117
|
--[[
|
|
113
118
|
Builds one instance and its descendants.
|
|
114
119
|
|
|
@@ -116,11 +121,6 @@ end
|
|
|
116
121
|
model appears in the Explorer complete rather than assembling itself piece by
|
|
117
122
|
piece in front of the user.
|
|
118
123
|
]]
|
|
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
124
|
local function build(
|
|
125
125
|
spec: { [string]: any },
|
|
126
126
|
parent: Instance,
|
|
@@ -56,11 +56,6 @@ local function resolveScript(path: string): LuaSourceContainer
|
|
|
56
56
|
return instance :: LuaSourceContainer
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
--[[
|
|
60
|
-
Reads source, optionally a line window. `startLine`/`endLine` are 1-based and
|
|
61
|
-
inclusive, matching the numbers script_edit takes back, so a read and a write
|
|
62
|
-
need no off-by-one conversion between them.
|
|
63
|
-
]]
|
|
64
59
|
--[[
|
|
65
60
|
A short fingerprint of a script's source, for detecting that it moved.
|
|
66
61
|
|
|
@@ -136,6 +131,11 @@ local function syncedFile(target: Instance): string?
|
|
|
136
131
|
return name
|
|
137
132
|
end
|
|
138
133
|
|
|
134
|
+
--[[
|
|
135
|
+
Reads source, optionally a line window. `startLine`/`endLine` are 1-based and
|
|
136
|
+
inclusive, matching the numbers script_edit takes back, so a read and a write
|
|
137
|
+
need no off-by-one conversion between them.
|
|
138
|
+
]]
|
|
139
139
|
function Scripts.read(params: { [string]: any }): { [string]: any }
|
|
140
140
|
local paths = params.paths
|
|
141
141
|
if typeof(paths) ~= "table" or #paths == 0 then
|
|
@@ -381,12 +381,45 @@ function Scripts.grep(params: { [string]: any }): { [string]: any }
|
|
|
381
381
|
)
|
|
382
382
|
end
|
|
383
383
|
|
|
384
|
+
local function badPattern(reason: any): never
|
|
385
|
+
Dispatch.fail(
|
|
386
|
+
"BAD_PATTERN",
|
|
387
|
+
string.format("%s is not a valid Lua pattern: %s", pattern, tostring(reason)),
|
|
388
|
+
"Lua patterns escape with %, not backslash, and have no alternation. "
|
|
389
|
+
.. "Set `literal` to search for the text exactly as written."
|
|
390
|
+
)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
--[[
|
|
394
|
+
One search over the whole file before splitting it into lines.
|
|
395
|
+
|
|
396
|
+
Most scripts in a place do not contain what is being looked for, and
|
|
397
|
+
splitting each into a line table only to find nothing was most of the cost
|
|
398
|
+
of a place-wide search. A pattern that matches no line cannot match the
|
|
399
|
+
whole file either -- except one anchored with `^` or `$`, which mean line
|
|
400
|
+
ends here but file ends there, so those skip this shortcut.
|
|
401
|
+
]]
|
|
402
|
+
-- Any trailing `$` counts, escaped or not: skipping the shortcut costs speed,
|
|
403
|
+
-- taking it wrongly would hide matches.
|
|
404
|
+
local anchored = not literal and (string.sub(needle, 1, 1) == "^" or string.sub(needle, -1) == "$")
|
|
405
|
+
|
|
384
406
|
local matches: { { [string]: any } } = {}
|
|
385
407
|
local total = 0
|
|
386
408
|
local memo: Paths.NameIndex = {}
|
|
387
409
|
|
|
388
410
|
for _, target in targets do
|
|
389
|
-
local
|
|
411
|
+
local source = ScriptEdit.read(target)
|
|
412
|
+
if not anchored then
|
|
413
|
+
local ok, found = pcall(string.find, if ignoreCase then string.lower(source) else source, needle, 1, literal)
|
|
414
|
+
if not ok then
|
|
415
|
+
badPattern(found)
|
|
416
|
+
end
|
|
417
|
+
if not found then
|
|
418
|
+
continue
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
local lines = TextEdit.toLines(source)
|
|
390
423
|
local path: string? = nil
|
|
391
424
|
|
|
392
425
|
for number, line in lines do
|
|
@@ -395,12 +428,7 @@ function Scripts.grep(params: { [string]: any }): { [string]: any }
|
|
|
395
428
|
-- has to be caught and reported as a pattern problem, not a no-match.
|
|
396
429
|
local ok, from = pcall(string.find, haystack, needle, 1, literal)
|
|
397
430
|
if not ok then
|
|
398
|
-
|
|
399
|
-
"BAD_PATTERN",
|
|
400
|
-
string.format("%s is not a valid Lua pattern: %s", pattern, tostring(from)),
|
|
401
|
-
"Lua patterns escape with %, not backslash, and have no alternation. "
|
|
402
|
-
.. "Set `literal` to search for the text exactly as written."
|
|
403
|
-
)
|
|
431
|
+
badPattern(from)
|
|
404
432
|
end
|
|
405
433
|
if not from then
|
|
406
434
|
continue
|
|
@@ -440,12 +468,6 @@ function Scripts.grep(params: { [string]: any }): { [string]: any }
|
|
|
440
468
|
}
|
|
441
469
|
end
|
|
442
470
|
|
|
443
|
-
--[[
|
|
444
|
-
Creates scripts. Source is assigned directly here rather than through
|
|
445
|
-
`UpdateSourceAsync`: the instance does not exist yet, so nothing can have it
|
|
446
|
-
open in the editor and there is no buffer to conflict with. Every later edit
|
|
447
|
-
goes through the editor path.
|
|
448
|
-
]]
|
|
449
471
|
--[[
|
|
450
472
|
Names the starter container a script was just parented into, or nil.
|
|
451
473
|
|
|
@@ -457,6 +479,14 @@ end
|
|
|
457
479
|
Client over LocalScript" advice writes a double-running script and is given
|
|
458
480
|
no way to find out.
|
|
459
481
|
]]
|
|
482
|
+
--[[
|
|
483
|
+
Assigning `Source` fails at 200,000 characters or more ("Provided string
|
|
484
|
+
length ... is greater than or equal to max length (200000)"), while
|
|
485
|
+
`UpdateSourceAsync` takes far more -- measured writing 279,000. Sources at
|
|
486
|
+
the limit are written through the editor instead of refused.
|
|
487
|
+
]]
|
|
488
|
+
local SOURCE_PROPERTY_LIMIT = 200000
|
|
489
|
+
|
|
460
490
|
local STARTER_CONTAINERS = {
|
|
461
491
|
"StarterGui",
|
|
462
492
|
"StarterPack",
|
|
@@ -473,6 +503,12 @@ local function starterContainer(instance: Instance): string?
|
|
|
473
503
|
return nil
|
|
474
504
|
end
|
|
475
505
|
|
|
506
|
+
--[[
|
|
507
|
+
Creates scripts. Source is assigned directly here rather than through
|
|
508
|
+
`UpdateSourceAsync`: the instance does not exist yet, so nothing can have it
|
|
509
|
+
open in the editor and there is no buffer to conflict with. Every later edit
|
|
510
|
+
goes through the editor path.
|
|
511
|
+
]]
|
|
476
512
|
function Scripts.create(params: { [string]: any }): { [string]: any }
|
|
477
513
|
local requests = params.scripts
|
|
478
514
|
if typeof(requests) ~= "table" or #requests == 0 then
|
|
@@ -503,6 +539,7 @@ function Scripts.create(params: { [string]: any }): { [string]: any }
|
|
|
503
539
|
-- No shared path memo here: each creation changes its parent's children, so a
|
|
504
540
|
-- cached sibling grouping would go stale mid-batch and mis-number the paths.
|
|
505
541
|
local warnings: { string } = {}
|
|
542
|
+
local oversized: { { target: LuaSourceContainer, source: string } } = {}
|
|
506
543
|
|
|
507
544
|
local created, recorded = Undo.record("StudioMCP.ScriptCreate", "MCP create script", function()
|
|
508
545
|
local created: { { [string]: any } } = {}
|
|
@@ -513,7 +550,11 @@ function Scripts.create(params: { [string]: any }): { [string]: any }
|
|
|
513
550
|
|
|
514
551
|
instance.Name = request.name
|
|
515
552
|
if typeof(request.source) == "string" then
|
|
516
|
-
|
|
553
|
+
if #request.source < SOURCE_PROPERTY_LIMIT then
|
|
554
|
+
(instance :: ScriptEdit.SourceContainer).Source = request.source
|
|
555
|
+
else
|
|
556
|
+
table.insert(oversized, { target = instance, source = request.source })
|
|
557
|
+
end
|
|
517
558
|
end
|
|
518
559
|
|
|
519
560
|
if typeof(request.runContext) == "string" and instance:IsA("Script") then
|
|
@@ -605,6 +646,14 @@ function Scripts.create(params: { [string]: any }): { [string]: any }
|
|
|
605
646
|
return created
|
|
606
647
|
end)
|
|
607
648
|
|
|
649
|
+
-- After the recording, because the editor write yields. Undoing the create
|
|
650
|
+
-- still removes these scripts whole, source and all.
|
|
651
|
+
for _, entry in oversized do
|
|
652
|
+
ScriptEdit.write(entry.target, function()
|
|
653
|
+
return entry.source
|
|
654
|
+
end)
|
|
655
|
+
end
|
|
656
|
+
|
|
608
657
|
return {
|
|
609
658
|
items = created,
|
|
610
659
|
undoStep = if recorded then "MCP create script" else nil,
|