@el4cteo/rbx-studio-mcp 0.6.0 → 0.6.5
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 +4 -4
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/tools/anim.js +159 -0
- package/dist/tools/anim.js.map +1 -0
- package/dist/tools/character.js +95 -5
- package/dist/tools/character.js.map +1 -1
- package/dist/tools/data.js +173 -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 +48 -1
- 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 +51 -3
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/world.js +317 -44
- package/dist/tools/world.js.map +1 -1
- package/package.json +74 -74
- package/plugin/src/Commands.luau +622 -562
- 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 +164 -16
- package/plugin/src/Png.luau +8 -4
- package/plugin/src/Serialize.luau +499 -327
- package/plugin/src/Undo.luau +94 -6
- package/plugin/src/handlers/Anim.luau +897 -0
- package/plugin/src/handlers/Assets.luau +587 -352
- 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 +127 -0
- 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/Viewport.luau +268 -0
- package/plugin/src/handlers/World.luau +89 -15
- package/plugin/src/init.server.luau +879 -832
- package/scripts/build-plugin.mjs +20 -0
- package/scripts/check-plugin.mjs +171 -124
|
@@ -221,10 +221,19 @@ local function takeScreenshot(): string
|
|
|
221
221
|
)
|
|
222
222
|
end
|
|
223
223
|
|
|
224
|
+
--[[
|
|
225
|
+
A frame at a time to begin with, then a slower poll.
|
|
226
|
+
|
|
227
|
+
The callback normally fires within a frame or two, and a flat 0.05s wait
|
|
228
|
+
billed that to the agent as up to 50ms of doing nothing -- on a call
|
|
229
|
+
whose whole point is that it is taken often. `task.wait()` resumes on the
|
|
230
|
+
next heartbeat, so the common case now costs a frame; after a quarter of
|
|
231
|
+
a second the shot is clearly not coming back promptly and the poll drops
|
|
232
|
+
to the cheap interval for the rest of the timeout.
|
|
233
|
+
]]
|
|
224
234
|
local waited = 0
|
|
225
235
|
while contentId == nil and waited < CAPTURE_TIMEOUT do
|
|
226
|
-
task.wait(0.05)
|
|
227
|
-
waited += 0.05
|
|
236
|
+
waited += if waited < 0.25 then task.wait() else task.wait(0.05)
|
|
228
237
|
end
|
|
229
238
|
|
|
230
239
|
if contentId == nil then
|
|
@@ -310,6 +319,125 @@ function Capture.playtestId(_params: { [string]: any }): { [string]: any }
|
|
|
310
319
|
return { contentId = tostring(result.contentId), device = Emulation.deviceId() }
|
|
311
320
|
end
|
|
312
321
|
|
|
322
|
+
--[[
|
|
323
|
+
Widest image the engine will create for us.
|
|
324
|
+
|
|
325
|
+
`AssetService:CreateEditableImage` caps at 1024 on either axis, and the
|
|
326
|
+
`width` parameter goes to 1600. Above the cap there is no destination to draw
|
|
327
|
+
into, so those calls fall back to the Luau loop rather than failing -- a
|
|
328
|
+
slower big screenshot, not a missing one.
|
|
329
|
+
]]
|
|
330
|
+
local ENGINE_SCALE_LIMIT = 1024
|
|
331
|
+
|
|
332
|
+
--[[
|
|
333
|
+
Drops alpha. RGBA in, RGB out, no scaling.
|
|
334
|
+
|
|
335
|
+
A screenshot has no alpha worth keeping and carrying it would add a quarter
|
|
336
|
+
to every byte that follows, so the channel goes here rather than on the wire.
|
|
337
|
+
|
|
338
|
+
Copied a word at a time rather than a byte at a time. Each 32-bit read takes
|
|
339
|
+
a whole pixel and each write lands three bytes further on, so the alpha byte
|
|
340
|
+
spills one place into the next pixel's slot -- where the next iteration
|
|
341
|
+
overwrites it. Only the final pixel has nothing following it, which is what
|
|
342
|
+
the one byte of slack on the buffer is for; the result is then trimmed to
|
|
343
|
+
the exact length so nothing downstream has to know about it.
|
|
344
|
+
|
|
345
|
+
Worth the trick: measured on a 799x359 frame, 16ms a byte at a time against
|
|
346
|
+
6-10ms this way, for output verified identical. That is most of what the
|
|
347
|
+
engine-side downscale costs, on a call agents make constantly.
|
|
348
|
+
]]
|
|
349
|
+
local function stripAlpha(rgba: buffer, width: number, height: number): buffer
|
|
350
|
+
local pixels = width * height
|
|
351
|
+
local length = pixels * 3
|
|
352
|
+
|
|
353
|
+
local wide = buffer.create(length + 1)
|
|
354
|
+
for index = 0, pixels - 1 do
|
|
355
|
+
buffer.writeu32(wide, index * 3, buffer.readu32(rgba, index * 4))
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
local out = buffer.create(length)
|
|
359
|
+
buffer.copy(out, 0, wide, 0, length)
|
|
360
|
+
return out
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
--[[
|
|
364
|
+
Downscales on the engine's side instead of in Luau.
|
|
365
|
+
|
|
366
|
+
This is about the picture, not the clock. `SamplingMode` defaults to bilinear,
|
|
367
|
+
so a 2x reduction averages the pixels it merges; the Luau loop it replaces
|
|
368
|
+
took every other row and column and dropped the rest, which is how one-pixel
|
|
369
|
+
GUI text and thin edges vanished from screenshots that existed to answer
|
|
370
|
+
"does this look right".
|
|
371
|
+
|
|
372
|
+
It was written expecting to be faster too, and it is not -- that guess is
|
|
373
|
+
recorded here because the measurement is the useful part. On a 1370x616 frame
|
|
374
|
+
scaled to 799: the old full-resolution read costs about 1ms, not the large
|
|
375
|
+
number the 4.4MB of RGBA suggests, and the Luau downscale that follows runs
|
|
376
|
+
in 19-22ms all in. `DrawImageTransformed` alone is 16-17ms of engine time.
|
|
377
|
+
With `stripAlpha` doing the rest a word at a time, this path lands around
|
|
378
|
+
23ms -- a couple of milliseconds behind, on a call whose round trip is
|
|
379
|
+
measured in hundreds. Paying that for legible text is the trade; pretending
|
|
380
|
+
it was free would have been the mistake.
|
|
381
|
+
|
|
382
|
+
Returns nil when the engine cannot do it -- an older Studio without
|
|
383
|
+
`CreateEditableImage`, or a target past ENGINE_SCALE_LIMIT -- and the caller
|
|
384
|
+
falls back.
|
|
385
|
+
]]
|
|
386
|
+
local function resample(
|
|
387
|
+
source: any,
|
|
388
|
+
sourceWidth: number,
|
|
389
|
+
sourceHeight: number,
|
|
390
|
+
targetWidth: number
|
|
391
|
+
): (buffer?, number, number)
|
|
392
|
+
local scale = math.min(1, targetWidth / sourceWidth)
|
|
393
|
+
local outWidth = math.max(1, math.floor(sourceWidth * scale))
|
|
394
|
+
local outHeight = math.max(1, math.floor(sourceHeight * scale))
|
|
395
|
+
|
|
396
|
+
if outWidth > ENGINE_SCALE_LIMIT or outHeight > ENGINE_SCALE_LIMIT then
|
|
397
|
+
return nil, 0, 0
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
local ok, rgb = pcall(function()
|
|
401
|
+
local canvas = (AssetService :: any):CreateEditableImage({
|
|
402
|
+
Size = Vector2.new(outWidth, outHeight),
|
|
403
|
+
})
|
|
404
|
+
--[[
|
|
405
|
+
Position is the centre, not the corner: `DrawImageTransformed` places
|
|
406
|
+
the source's PIVOT at the position given, and the pivot defaults to
|
|
407
|
+
the middle of the source image. Passing Vector2.zero here puts three
|
|
408
|
+
quarters of the frame off-canvas.
|
|
409
|
+
]]
|
|
410
|
+
local okDraw = pcall(function()
|
|
411
|
+
canvas:DrawImageTransformed(
|
|
412
|
+
Vector2.new(outWidth / 2, outHeight / 2),
|
|
413
|
+
Vector2.new(scale, scale),
|
|
414
|
+
0,
|
|
415
|
+
source,
|
|
416
|
+
{
|
|
417
|
+
CombineType = Enum.ImageCombineType.Overwrite,
|
|
418
|
+
SamplingMode = Enum.ResamplerMode.Default,
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
end)
|
|
422
|
+
if not okDraw then
|
|
423
|
+
canvas:Destroy()
|
|
424
|
+
error("draw failed", 0)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
local pixels = canvas:ReadPixelsBuffer(Vector2.zero, canvas.Size)
|
|
428
|
+
-- Freed here rather than left to the collector: an EditableImage holds
|
|
429
|
+
-- its pixels outside Luau's heap, and a screenshot every few seconds
|
|
430
|
+
-- would otherwise pile them up for as long as Studio stays open.
|
|
431
|
+
canvas:Destroy()
|
|
432
|
+
return stripAlpha(pixels, outWidth, outHeight)
|
|
433
|
+
end)
|
|
434
|
+
|
|
435
|
+
if not ok then
|
|
436
|
+
return nil, 0, 0
|
|
437
|
+
end
|
|
438
|
+
return rgb :: buffer, outWidth, outHeight
|
|
439
|
+
end
|
|
440
|
+
|
|
313
441
|
--[[
|
|
314
442
|
Turns a content id into pixels on the wire.
|
|
315
443
|
|
|
@@ -332,23 +460,30 @@ local function encode(contentId: string, width: number, context: string): { [str
|
|
|
332
460
|
local sourceWidth = math.floor(size.X)
|
|
333
461
|
local sourceHeight = math.floor(size.Y)
|
|
334
462
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
463
|
+
local rgb, outWidth, outHeight = resample(image, sourceWidth, sourceHeight, width)
|
|
464
|
+
|
|
465
|
+
if rgb == nil then
|
|
466
|
+
--[[
|
|
467
|
+
Both arguments are required. Called with none it reports "expects 2
|
|
468
|
+
arguments" rather than defaulting to the whole image, which is worth
|
|
469
|
+
stating because every other read on this object takes none.
|
|
470
|
+
]]
|
|
471
|
+
local okPixels, pixels = pcall(function()
|
|
472
|
+
return (image :: any):ReadPixelsBuffer(Vector2.zero, size)
|
|
473
|
+
end)
|
|
474
|
+
if not okPixels then
|
|
475
|
+
Dispatch.fail(
|
|
476
|
+
"CAPTURE_UNREADABLE",
|
|
477
|
+
string.format("The screenshot's pixels could not be read: %s", tostring(pixels))
|
|
478
|
+
)
|
|
479
|
+
end
|
|
480
|
+
rgb, outWidth, outHeight = Png.downscaleToRgb(pixels, sourceWidth, sourceHeight, width)
|
|
348
481
|
end
|
|
349
482
|
|
|
350
|
-
|
|
351
|
-
|
|
483
|
+
-- On the scaled image, not the source: the question is whether anything was
|
|
484
|
+
-- drawn, which survives the downscale, and it is a quarter of the pixels to
|
|
485
|
+
-- walk.
|
|
486
|
+
local black = isFlat(rgb :: buffer)
|
|
352
487
|
|
|
353
488
|
--[[
|
|
354
489
|
Raw pixels, compressed by the engine, assembled into a PNG by Node.
|
|
@@ -370,7 +505,7 @@ local function encode(contentId: string, width: number, context: string): { [str
|
|
|
370
505
|
]]
|
|
371
506
|
local okPacked, packed = pcall(function()
|
|
372
507
|
local compressed = (EncodingService :: any):CompressBuffer(
|
|
373
|
-
rgb,
|
|
508
|
+
rgb :: buffer,
|
|
374
509
|
(Enum :: any).CompressionAlgorithm.Zstd,
|
|
375
510
|
COMPRESSION_LEVEL
|
|
376
511
|
)
|
|
@@ -385,7 +520,7 @@ local function encode(contentId: string, width: number, context: string): { [str
|
|
|
385
520
|
height = outHeight,
|
|
386
521
|
sourceWidth = sourceWidth,
|
|
387
522
|
sourceHeight = sourceHeight,
|
|
388
|
-
rawBytes = buffer.len(rgb),
|
|
523
|
+
rawBytes = buffer.len(rgb :: buffer),
|
|
389
524
|
bytes = #(packed :: string),
|
|
390
525
|
context = context,
|
|
391
526
|
black = black,
|
|
@@ -393,7 +528,7 @@ local function encode(contentId: string, width: number, context: string): { [str
|
|
|
393
528
|
}
|
|
394
529
|
end
|
|
395
530
|
|
|
396
|
-
local png = Png.encode(rgb, outWidth, outHeight)
|
|
531
|
+
local png = Png.encode(rgb :: buffer, outWidth, outHeight)
|
|
397
532
|
|
|
398
533
|
return {
|
|
399
534
|
encoding = "png",
|