@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.
Files changed (48) hide show
  1. package/README.md +4 -4
  2. package/dist/index.js +4 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/tools/anim.js +159 -0
  5. package/dist/tools/anim.js.map +1 -0
  6. package/dist/tools/character.js +95 -5
  7. package/dist/tools/character.js.map +1 -1
  8. package/dist/tools/data.js +173 -0
  9. package/dist/tools/data.js.map +1 -0
  10. package/dist/tools/device.js +77 -7
  11. package/dist/tools/device.js.map +1 -1
  12. package/dist/tools/discover.js +80 -4
  13. package/dist/tools/discover.js.map +1 -1
  14. package/dist/tools/exec.js +48 -1
  15. package/dist/tools/exec.js.map +1 -1
  16. package/dist/tools/input.js +35 -9
  17. package/dist/tools/input.js.map +1 -1
  18. package/dist/tools/perf.js +74 -7
  19. package/dist/tools/perf.js.map +1 -1
  20. package/dist/tools/scripts.js +51 -3
  21. package/dist/tools/scripts.js.map +1 -1
  22. package/dist/tools/world.js +317 -44
  23. package/dist/tools/world.js.map +1 -1
  24. package/package.json +74 -74
  25. package/plugin/src/Commands.luau +622 -562
  26. package/plugin/src/Config.luau +65 -65
  27. package/plugin/src/Console.luau +1909 -1843
  28. package/plugin/src/Emulation.luau +172 -0
  29. package/plugin/src/Phrase.luau +164 -16
  30. package/plugin/src/Png.luau +8 -4
  31. package/plugin/src/Serialize.luau +499 -327
  32. package/plugin/src/Undo.luau +94 -6
  33. package/plugin/src/handlers/Anim.luau +897 -0
  34. package/plugin/src/handlers/Assets.luau +587 -352
  35. package/plugin/src/handlers/Capture.luau +155 -20
  36. package/plugin/src/handlers/Character.luau +823 -361
  37. package/plugin/src/handlers/Data.luau +539 -0
  38. package/plugin/src/handlers/Device.luau +394 -139
  39. package/plugin/src/handlers/Discover.luau +685 -363
  40. package/plugin/src/handlers/Geometry.luau +127 -0
  41. package/plugin/src/handlers/Perf.luau +227 -0
  42. package/plugin/src/handlers/Scripts.luau +673 -539
  43. package/plugin/src/handlers/Session.luau +3 -0
  44. package/plugin/src/handlers/Viewport.luau +268 -0
  45. package/plugin/src/handlers/World.luau +89 -15
  46. package/plugin/src/init.server.luau +879 -832
  47. package/scripts/build-plugin.mjs +20 -0
  48. 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
- Both arguments are required. Called with none it reports "expects 2
337
- arguments" rather than defaulting to the whole image, which is worth
338
- stating because every other read on this object takes none.
339
- ]]
340
- local okPixels, pixels = pcall(function()
341
- return (image :: any):ReadPixelsBuffer(Vector2.zero, size)
342
- end)
343
- if not okPixels then
344
- Dispatch.fail(
345
- "CAPTURE_UNREADABLE",
346
- string.format("The screenshot's pixels could not be read: %s", tostring(pixels))
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
- local rgb, outWidth, outHeight = Png.downscaleToRgb(pixels, sourceWidth, sourceHeight, width)
351
- local black = isFlat(rgb)
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",