@chrrxs/robloxstudio-mcp-inspector 3.1.3 → 3.1.4

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.
@@ -278,6 +278,326 @@ return {
278
278
  </Properties>
279
279
  </Item>
280
280
  <Item class="ModuleScript" referent="3">
281
+ <Properties>
282
+ <string name="Name">CaptureTransfer</string>
283
+ <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
284
+ -- Private ClientBroker transport. Only metadata and <=1 MiB data chunks cross
285
+ -- InvokeClient; callers still receive the original capture-studio response.
286
+ local HttpService = game:GetService("HttpService")
287
+ local READ_ENDPOINT = "/__mcp/capture-studio/read"
288
+ local RELEASE_ENDPOINT = "/__mcp/capture-studio/release"
289
+ local CHUNK_BYTES = 1024 * 1024
290
+ local MAX_TRANSFER_BYTES = 64 * 1024 * 1024
291
+ local MAX_TRANSFERS = 2
292
+ local TRANSFER_TTL_SECONDS = 60
293
+ local transfers = {}
294
+ local function integer(value, min, max)
295
+ local _value = value
296
+ local _condition = type(_value) == "number"
297
+ if _condition then
298
+ _condition = value >= min and value <= max and value == math.floor(value)
299
+ end
300
+ return _condition
301
+ end
302
+ local function validId(value)
303
+ local _value = value
304
+ local _condition = type(_value) == "string"
305
+ if _condition then
306
+ _condition = #value > 0 and #value <= 64
307
+ end
308
+ return _condition
309
+ end
310
+ local function metadata(value)
311
+ local _value = value
312
+ if not (type(_value) == "table") then
313
+ return nil
314
+ end
315
+ local fields = value
316
+ if fields.success ~= true or (fields.encoding ~= "png" and fields.encoding ~= "rgba8") or fields.source ~= "StudioCaptureService" or not integer(fields.width, 1, 65536) or not integer(fields.height, 1, 65536) or not integer(fields.nativeWidth, 1, 65536) or not integer(fields.nativeHeight, 1, 65536) then
317
+ return nil
318
+ end
319
+ return {
320
+ success = true,
321
+ encoding = fields.encoding,
322
+ source = fields.source,
323
+ width = fields.width,
324
+ height = fields.height,
325
+ nativeWidth = fields.nativeWidth,
326
+ nativeHeight = fields.nativeHeight,
327
+ }
328
+ end
329
+ local function fail(message)
330
+ return {
331
+ success = false,
332
+ error = `Capture transfer: {message}`,
333
+ }
334
+ end
335
+ local function discard(id)
336
+ local _id = id
337
+ local transfer = transfers[_id]
338
+ if transfer == nil then
339
+ return nil
340
+ end
341
+ local _id_1 = id
342
+ transfers[_id_1] = nil
343
+ if transfer.expiryThread ~= nil then
344
+ task.cancel(transfer.expiryThread)
345
+ end
346
+ end
347
+ local function prune()
348
+ local now = os.clock()
349
+ for id, transfer in transfers do
350
+ if now >= transfer.expiresAt then
351
+ discard(id)
352
+ end
353
+ end
354
+ end
355
+ local function release(data)
356
+ if not validId(data.transferId) then
357
+ return fail("invalid transfer ID")
358
+ end
359
+ discard(data.transferId)
360
+ -- Idempotent release also negotiates chunk support before capture starts.
361
+ return {
362
+ success = true,
363
+ chunkBytes = CHUNK_BYTES,
364
+ }
365
+ end
366
+ local validSize
367
+ local function begin(data, capture)
368
+ prune()
369
+ local id = data.transferId
370
+ -- An older server cannot consume chunk descriptors. Let it use legacy
371
+ -- capture rather than returning an unsafe inline image or a hard error.
372
+ if id == nil then
373
+ return {
374
+ unavailable = "Client capture requires bounded transfer support",
375
+ }
376
+ end
377
+ if not validId(id) then
378
+ return fail("invalid transfer ID")
379
+ end
380
+ if transfers[id] ~= nil then
381
+ return fail("duplicate transfer ID")
382
+ end
383
+ -- Reserve before capture yields: concurrent captures cannot over-admit or
384
+ -- evict each other's data. Retained strings are capped at 2 * 64 MiB.
385
+ -- ▼ ReadonlyMap.size ▼
386
+ local _size = 0
387
+ for _ in transfers do
388
+ _size += 1
389
+ end
390
+ -- ▲ ReadonlyMap.size ▲
391
+ if _size >= MAX_TRANSFERS then
392
+ return fail("two captures are already in flight; release or await them first")
393
+ end
394
+ local transfer = {
395
+ offset = 0,
396
+ expiresAt = os.clock() + TRANSFER_TTL_SECONDS,
397
+ }
398
+ transfers[id] = transfer
399
+ -- Capture only the ID, not the large payload, in the expiry closure.
400
+ transfer.expiryThread = task.delay(TRANSFER_TTL_SECONDS, function()
401
+ local current = transfers[id]
402
+ if current ~= nil and os.clock() >= current.expiresAt then
403
+ current.expiryThread = nil
404
+ discard(id)
405
+ end
406
+ end)
407
+ local ok, result = pcall(capture)
408
+ if not ok then
409
+ if transfers[id] == transfer then
410
+ discard(id)
411
+ end
412
+ return fail(`capture failed: {tostring(result)}`)
413
+ end
414
+ if transfers[id] ~= transfer or os.clock() >= transfer.expiresAt then
415
+ if transfers[id] == transfer then
416
+ discard(id)
417
+ end
418
+ return fail("capture expired before it completed")
419
+ end
420
+ local info = metadata(result)
421
+ if info == nil then
422
+ discard(id)
423
+ -- Unavailability must reach core unchanged to preserve legacy fallback.
424
+ if type(result) == "table" then
425
+ local fields = result
426
+ local _unavailable = fields.unavailable
427
+ if type(_unavailable) == "string" then
428
+ return {
429
+ unavailable = string.sub(fields.unavailable, 1, 4096),
430
+ }
431
+ end
432
+ local _error = fields.error
433
+ if type(_error) == "string" then
434
+ return fail(string.sub(fields.error, 1, 4096))
435
+ end
436
+ end
437
+ return fail("invalid capture response")
438
+ end
439
+ local fields = result
440
+ local pixels = fields.data
441
+ if not (type(pixels) == "string") or not validSize(#pixels, info) then
442
+ discard(id)
443
+ return fail("invalid or oversized capture data")
444
+ end
445
+ transfer.data = pixels
446
+ return {
447
+ transferId = id,
448
+ totalBytes = #pixels,
449
+ chunkBytes = CHUNK_BYTES,
450
+ metadata = info,
451
+ }
452
+ end
453
+ function validSize(size, info)
454
+ if not integer(size, 4, MAX_TRANSFER_BYTES) or size % 4 ~= 0 then
455
+ return false
456
+ end
457
+ return info.encoding ~= "rgba8" or size == math.ceil(info.width * info.height * 4 / 3) * 4
458
+ end
459
+ local function read(data)
460
+ prune()
461
+ local id = data.transferId
462
+ if not validId(id) then
463
+ return fail("invalid transfer ID")
464
+ end
465
+ local transfer = transfers[id]
466
+ if transfer == nil then
467
+ return fail("unknown or expired transfer")
468
+ end
469
+ local pixels = transfer.data
470
+ local offset = data.offset
471
+ local length = data.length
472
+ if pixels == nil or not integer(offset, 0, #pixels - 1) or not integer(length, 1, CHUNK_BYTES) or offset ~= transfer.offset or length ~= math.min(CHUNK_BYTES, #pixels - offset) then
473
+ discard(id)
474
+ return fail("invalid chunk offset or length")
475
+ end
476
+ local _arg0 = offset + 1
477
+ local _arg1 = offset + length
478
+ local chunk = string.sub(pixels, _arg0, _arg1)
479
+ transfer.offset += length
480
+ -- Last read releases immediately, even if the receiver disappears before
481
+ -- its explicit release. Explicit release is idempotent for error paths.
482
+ if transfer.offset == #pixels then
483
+ discard(id)
484
+ end
485
+ return {
486
+ transferId = id,
487
+ offset = offset,
488
+ data = chunk,
489
+ }
490
+ end
491
+ local function receive(invoke, data)
492
+ local id = HttpService:GenerateGUID(false)
493
+ local ok, result = pcall(function()
494
+ -- Older clients return inline data and may disconnect on a large capture.
495
+ -- Probe a harmless release of our fresh ID before requesting any pixels.
496
+ local probe = invoke(RELEASE_ENDPOINT, {
497
+ transferId = id,
498
+ })
499
+ if not (type(probe) == "table") then
500
+ return {
501
+ unavailable = "Client capture lacks bounded transfer support",
502
+ }
503
+ end
504
+ local capability = probe
505
+ if capability.success ~= true or capability.chunkBytes ~= CHUNK_BYTES then
506
+ return {
507
+ unavailable = "Client capture lacks bounded transfer support",
508
+ }
509
+ end
510
+ local response = invoke("/api/capture-studio", {
511
+ encoding = data.encoding,
512
+ transferId = id,
513
+ })
514
+ if not (type(response) == "table") then
515
+ error("invalid transfer response")
516
+ end
517
+ local fields = response
518
+ local _unavailable = fields.unavailable
519
+ if type(_unavailable) == "string" then
520
+ return {
521
+ unavailable = string.sub(fields.unavailable, 1, 4096),
522
+ }
523
+ end
524
+ local _error = fields.error
525
+ if type(_error) == "string" then
526
+ error(string.sub(fields.error, 1, 4096))
527
+ end
528
+ local info = metadata(fields.metadata)
529
+ if info == nil or fields.transferId ~= id or fields.chunkBytes ~= CHUNK_BYTES or not validSize(fields.totalBytes, info) then
530
+ error("invalid transfer metadata or size")
531
+ end
532
+ local size = fields.totalBytes
533
+ local chunks = {}
534
+ local offset = 0
535
+ while offset < size do
536
+ local length = math.min(CHUNK_BYTES, size - offset)
537
+ local response = invoke(READ_ENDPOINT, {
538
+ transferId = id,
539
+ offset = offset,
540
+ length = length,
541
+ })
542
+ if not (type(response) == "table") then
543
+ error("invalid chunk response")
544
+ end
545
+ local chunk = response
546
+ local _error_1 = chunk.error
547
+ if type(_error_1) == "string" then
548
+ error(string.sub(chunk.error, 1, 4096))
549
+ end
550
+ local _condition = chunk.transferId ~= id or chunk.offset ~= offset
551
+ if not _condition then
552
+ local _data = chunk.data
553
+ _condition = not (type(_data) == "string")
554
+ if not _condition then
555
+ _condition = #chunk.data ~= length
556
+ end
557
+ end
558
+ if _condition then
559
+ error("chunk ID, offset or length mismatch")
560
+ end
561
+ local _data = chunk.data
562
+ table.insert(chunks, _data)
563
+ offset += length
564
+ end
565
+ return {
566
+ success = true,
567
+ encoding = info.encoding,
568
+ source = info.source,
569
+ width = info.width,
570
+ height = info.height,
571
+ nativeWidth = info.nativeWidth,
572
+ nativeHeight = info.nativeHeight,
573
+ data = table.concat(chunks, ""),
574
+ }
575
+ end)
576
+ -- The final read already frees successful transfers. Cleanup is best-effort:
577
+ -- it must not mask an unavailable signal from an older client or a completed
578
+ -- image. Failed partial transfers also have the bounded expiry above.
579
+ pcall(function()
580
+ return invoke(RELEASE_ENDPOINT, {
581
+ transferId = id,
582
+ })
583
+ end)
584
+ if not ok then
585
+ return fail(tostring(result))
586
+ end
587
+ return result
588
+ end
589
+ return {
590
+ READ_ENDPOINT = READ_ENDPOINT,
591
+ RELEASE_ENDPOINT = RELEASE_ENDPOINT,
592
+ begin = begin,
593
+ read = read,
594
+ release = release,
595
+ receive = receive,
596
+ }
597
+ ]]></string>
598
+ </Properties>
599
+ </Item>
600
+ <Item class="ModuleScript" referent="4">
281
601
  <Properties>
282
602
  <string name="Name">ClientBroker</string>
283
603
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -291,6 +611,7 @@ local RuntimeLogBuffer = TS.import(script, script.Parent, "RuntimeLogBuffer")
291
611
  local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandlers")
292
612
  local SceneAnalysisHandlers = TS.import(script, script.Parent, "handlers", "SceneAnalysisHandlers")
293
613
  local CaptureHandlers = TS.import(script, script.Parent, "handlers", "CaptureHandlers")
614
+ local CaptureTransfer = TS.import(script, script.Parent, "CaptureTransfer")
294
615
  local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandlers")
295
616
  local MetadataHandlers = TS.import(script, script.Parent, "handlers", "MetadataHandlers")
296
617
  local EvalRuntimeHandlers = TS.import(script, script.Parent, "handlers", "EvalRuntimeHandlers")
@@ -327,6 +648,7 @@ local CLIENT_BROKER_ALLOWED_ENDPOINTS = {
327
648
  ["/api/multiplayer-test-state"] = true,
328
649
  ["/api/multiplayer-test-leave-client"] = true,
329
650
  ["/api/capture-begin"] = true,
651
+ ["/api/capture-studio"] = true,
330
652
  ["/api/simulate-mouse-input"] = true,
331
653
  ["/api/simulate-keyboard-input"] = true,
332
654
  ["/api/focus-viewport"] = true,
@@ -528,6 +850,17 @@ local function setupClientBroker(attempt)
528
850
  if payload and payload.endpoint == "/api/capture-begin" then
529
851
  return CaptureHandlers.captureBegin()
530
852
  end
853
+ if payload and payload.endpoint == "/api/capture-studio" then
854
+ return CaptureTransfer.begin(payload.data or {}, function()
855
+ return CaptureHandlers.captureStudio(payload.data or {})
856
+ end)
857
+ end
858
+ if payload and payload.endpoint == CaptureTransfer.READ_ENDPOINT then
859
+ return CaptureTransfer.read(payload.data or {})
860
+ end
861
+ if payload and payload.endpoint == CaptureTransfer.RELEASE_ENDPOINT then
862
+ return CaptureTransfer.release(payload.data or {})
863
+ end
531
864
  if payload and payload.endpoint == "/api/simulate-mouse-input" then
532
865
  return InputHandlers.simulateMouseInput(payload.data or {})
533
866
  end
@@ -840,6 +1173,14 @@ local function dispatchClientRequest(peerId, target, endpoint, data)
840
1173
  error = `Client-proxy does not forward {endpoint}. Allowed: {table.concat(allowed, ", ")}.`,
841
1174
  }
842
1175
  end
1176
+ if endpoint == "/api/capture-studio" then
1177
+ return CaptureTransfer.receive(function(captureEndpoint, captureData)
1178
+ return entry.remote:InvokeClient(entry.player, {
1179
+ endpoint = captureEndpoint,
1180
+ data = captureData,
1181
+ })
1182
+ end, data or {})
1183
+ end
843
1184
  local envelope = {
844
1185
  endpoint = endpoint,
845
1186
  data = data,
@@ -920,7 +1261,7 @@ return {
920
1261
  ]]></string>
921
1262
  </Properties>
922
1263
  </Item>
923
- <Item class="ModuleScript" referent="4">
1264
+ <Item class="ModuleScript" referent="5">
924
1265
  <Properties>
925
1266
  <string name="Name">Communication</string>
926
1267
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -988,6 +1329,7 @@ local routeMap = {
988
1329
  ["/api/insert-asset"] = AssetHandlers.insertAsset,
989
1330
  ["/api/preview-asset"] = AssetHandlers.previewAsset,
990
1331
  ["/api/capture-screenshot"] = CaptureHandlers.captureScreenshot,
1332
+ ["/api/capture-studio"] = CaptureHandlers.captureStudio,
991
1333
  ["/api/capture-begin"] = CaptureHandlers.captureBegin,
992
1334
  ["/api/capture-read"] = CaptureHandlers.captureRead,
993
1335
  ["/api/simulate-mouse-input"] = InputHandlers.simulateMouseInput,
@@ -1276,7 +1618,7 @@ return {
1276
1618
  ]]></string>
1277
1619
  </Properties>
1278
1620
  </Item>
1279
- <Item class="ModuleScript" referent="5">
1621
+ <Item class="ModuleScript" referent="6">
1280
1622
  <Properties>
1281
1623
  <string name="Name">CooperativeJobRunner</string>
1282
1624
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -1351,7 +1693,7 @@ return {
1351
1693
  ]]></string>
1352
1694
  </Properties>
1353
1695
  </Item>
1354
- <Item class="ModuleScript" referent="6">
1696
+ <Item class="ModuleScript" referent="7">
1355
1697
  <Properties>
1356
1698
  <string name="Name">EvalBridges</string>
1357
1699
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -1472,9 +1814,9 @@ local function computeBridgeStamp()
1472
1814
  for i = 1, #combined do
1473
1815
  h = (h * 33 + (string.byte(combined, i))) % 2147483647
1474
1816
  end
1475
- -- "3.1.3" is replaced with the package version at package time
1817
+ -- "3.1.4" is replaced with the package version at package time
1476
1818
  -- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
1477
- return `{tostring(h)}-3.1.3`
1819
+ return `{tostring(h)}-3.1.4`
1478
1820
  end
1479
1821
  local BRIDGE_STAMP = computeBridgeStamp()
1480
1822
  local function setSource(scriptInst, source)
@@ -1627,11 +1969,11 @@ return {
1627
1969
  ]]></string>
1628
1970
  </Properties>
1629
1971
  </Item>
1630
- <Item class="Folder" referent="7">
1972
+ <Item class="Folder" referent="8">
1631
1973
  <Properties>
1632
1974
  <string name="Name">handlers</string>
1633
1975
  </Properties>
1634
- <Item class="ModuleScript" referent="8">
1976
+ <Item class="ModuleScript" referent="9">
1635
1977
  <Properties>
1636
1978
  <string name="Name">AssetHandlers</string>
1637
1979
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -2072,7 +2414,7 @@ return {
2072
2414
  ]]></string>
2073
2415
  </Properties>
2074
2416
  </Item>
2075
- <Item class="ModuleScript" referent="9">
2417
+ <Item class="ModuleScript" referent="10">
2076
2418
  <Properties>
2077
2419
  <string name="Name">BreakpointHandlers</string>
2078
2420
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -2579,7 +2921,7 @@ return {
2579
2921
  ]]></string>
2580
2922
  </Properties>
2581
2923
  </Item>
2582
- <Item class="ModuleScript" referent="10">
2924
+ <Item class="ModuleScript" referent="11">
2583
2925
  <Properties>
2584
2926
  <string name="Name">CaptureHandlers</string>
2585
2927
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -2587,11 +2929,27 @@ local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
2587
2929
  local RenderMonitor = TS.import(script, script.Parent.Parent, "RenderMonitor")
2588
2930
  local CaptureService = game:GetService("CaptureService")
2589
2931
  local AssetService = game:GetService("AssetService")
2932
+ local Workspace = game:GetService("Workspace")
2590
2933
  local MAX_TILE_SIZE = 1024
2591
2934
  local MAX_RAW_PIXEL_BYTES = 36 * 1024 * 1024
2592
2935
  local MAX_CREATED_IMAGE_DIM = 2048
2593
2936
  local BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
2594
2937
  local PAD_BYTE = (string.byte("="))
2938
+ -- StudioCaptureService (Studio-only, PluginSecurity) is the fast path: it hands
2939
+ -- back the framebuffer directly, so it needs neither CaptureService's
2940
+ -- asynchronous callback nor an EditableImage round-trip — and it captures at
2941
+ -- ViewportSize, which is exactly the coordinate space simulate_mouse_input
2942
+ -- expects. It is gated behind Studio FFlags and is missing from @rbxts/types,
2943
+ -- hence the local structural declarations below.
2944
+ -- Unchecked cast, single reason: these enums exist in Studio but are missing
2945
+ -- from @rbxts/types, so the global Enum table has to be read dynamically.
2946
+ local ENUM_TABLE = Enum
2947
+ local STUDIO_CAPTURE_FORMATS = ENUM_TABLE.StudioCaptureScreenshotFormat
2948
+ -- Measured on a 1980x1032 viewport: RGBA8 completes in ~0.16s and PNG in ~1s.
2949
+ -- A capture that is still Pending well past that never completes (it happens
2950
+ -- while a playtest owns the renderer), so we stop waiting and let the caller
2951
+ -- fall back instead of stalling the tool call.
2952
+ local STUDIO_CAPTURE_TIMEOUT = 3
2595
2953
  local B64 = {}
2596
2954
  for i = 0, 63 do
2597
2955
  B64[i + 1] = (string.byte(BASE64_CHARS, i + 1))
@@ -2789,6 +3147,124 @@ local function readContentToBase64(contentId)
2789
3147
  nativeHeight = nativeH,
2790
3148
  }
2791
3149
  end
3150
+ local cachedStudioService
3151
+ local function getStudioCaptureService()
3152
+ if cachedStudioService ~= nil then
3153
+ return cachedStudioService
3154
+ end
3155
+ -- Unchecked cast, single reason: StudioCaptureService is absent from
3156
+ -- @rbxts/types, so GetService cannot be called through the typed overload.
3157
+ local dynamicGame = game
3158
+ local ok, service = pcall(function()
3159
+ return dynamicGame:GetService("StudioCaptureService")
3160
+ end)
3161
+ if not ok or service == nil then
3162
+ return nil
3163
+ end
3164
+ cachedStudioService = service
3165
+ return cachedStudioService
3166
+ end
3167
+ -- Captures through StudioCaptureService. Returns undefined when the service
3168
+ -- cannot capture right now (missing FFlag, permission not granted, or this
3169
+ -- DataModel is not the active one) so the caller can fall back to the
3170
+ -- CaptureService + EditableImage path.
3171
+ local function doStudioCapture(wantPng)
3172
+ if STUDIO_CAPTURE_FORMATS == nil then
3173
+ return nil
3174
+ end
3175
+ local service = getStudioCaptureService()
3176
+ if service == nil then
3177
+ return nil
3178
+ end
3179
+ local canOk, can = pcall(function()
3180
+ return service:CanCaptureScreenshot()
3181
+ end)
3182
+ if not canOk or can ~= true then
3183
+ return nil
3184
+ end
3185
+ local camera = Workspace.CurrentCamera
3186
+ if camera == nil then
3187
+ return nil
3188
+ end
3189
+ local viewport = camera.ViewportSize
3190
+ local nativeW = math.max(1, math.floor(viewport.X))
3191
+ local nativeH = math.max(1, math.floor(viewport.Y))
3192
+ local w = nativeW
3193
+ local h = nativeH
3194
+ -- Raw RGBA rides back base64-encoded, so an oversized viewport is
3195
+ -- downscaled by the engine during the capture itself — no second pass.
3196
+ if not wantPng and nativeW * nativeH * 4 > MAX_RAW_PIXEL_BYTES then
3197
+ local scale = math.sqrt(MAX_RAW_PIXEL_BYTES / (nativeW * nativeH * 4))
3198
+ w = math.max(1, math.floor(nativeW * scale))
3199
+ h = math.max(1, math.floor(nativeH * scale))
3200
+ end
3201
+ -- CaptureSize is a framebuffer crop, not the logical viewport size. At
3202
+ -- fractional display scaling it cuts off the right/bottom of the frame.
3203
+ local options = {
3204
+ OutputSize = Vector2.new(w, h),
3205
+ ResampleMode = Enum.ResamplerMode.Default,
3206
+ Format = if wantPng then STUDIO_CAPTURE_FORMATS.PNG else STUDIO_CAPTURE_FORMATS.RGBA8,
3207
+ }
3208
+ local captureOk, captureResult = pcall(function()
3209
+ return service:CaptureScreenshot(options)
3210
+ end)
3211
+ if not captureOk then
3212
+ return {
3213
+ error = `StudioCaptureService:CaptureScreenshot failed: {tostring(captureResult)}`,
3214
+ }
3215
+ end
3216
+ if captureResult == nil then
3217
+ return nil
3218
+ end
3219
+ local capture = captureResult
3220
+ local startTime = tick()
3221
+ while capture.BufferStatus.Name == "Pending" or capture.BufferStatus.Name == "NotStarted" do
3222
+ if tick() - startTime > STUDIO_CAPTURE_TIMEOUT then
3223
+ return nil
3224
+ end
3225
+ task.wait(0.02)
3226
+ end
3227
+ if capture.BufferStatus.Name ~= "Ready" then
3228
+ local errorsOk, errors = pcall(function()
3229
+ return capture:GetErrors()
3230
+ end)
3231
+ local detail = if errorsOk then game:GetService("HttpService"):JSONEncode(errors) else "unavailable"
3232
+ return {
3233
+ error = `StudioCaptureService capture failed (status {capture.BufferStatus.Name}): {detail}`,
3234
+ }
3235
+ end
3236
+ local bufferOk, captureBuffer = pcall(function()
3237
+ return capture:GetBuffer()
3238
+ end)
3239
+ if not bufferOk then
3240
+ return {
3241
+ error = `StudioCaptureService:GetBuffer failed: {tostring(captureBuffer)}`,
3242
+ }
3243
+ end
3244
+ local resolution = capture.Resolution
3245
+ return {
3246
+ success = true,
3247
+ encoding = if wantPng then "png" else "rgba8",
3248
+ source = "StudioCaptureService",
3249
+ width = math.max(1, math.floor(resolution.X)),
3250
+ height = math.max(1, math.floor(resolution.Y)),
3251
+ nativeWidth = nativeW,
3252
+ nativeHeight = nativeH,
3253
+ data = encodeBase64(captureBuffer),
3254
+ }
3255
+ end
3256
+ -- Studio-only capture endpoint. Reports `unavailable` (instead of an error) so
3257
+ -- the server can fall back to the legacy CaptureService path.
3258
+ local function captureStudio(requestData)
3259
+ local wantPng = requestData.encoding == "png"
3260
+ local result = doStudioCapture(wantPng)
3261
+ if result == nil then
3262
+ return {
3263
+ unavailable = "StudioCaptureService cannot capture this DataModel right now",
3264
+ }
3265
+ end
3266
+ return result
3267
+ end
2792
3268
  -- Edit-mode single shot: capture and read back in the same (edit) context.
2793
3269
  local function captureScreenshotData()
2794
3270
  local cap = doCaptureScreenshot()
@@ -2817,13 +3293,14 @@ end
2817
3293
  return {
2818
3294
  captureScreenshotData = captureScreenshotData,
2819
3295
  captureScreenshot = captureScreenshot,
3296
+ captureStudio = captureStudio,
2820
3297
  captureBegin = captureBegin,
2821
3298
  captureRead = captureRead,
2822
3299
  }
2823
3300
  ]]></string>
2824
3301
  </Properties>
2825
3302
  </Item>
2826
- <Item class="ModuleScript" referent="11">
3303
+ <Item class="ModuleScript" referent="12">
2827
3304
  <Properties>
2828
3305
  <string name="Name">EvalRuntimeHandlers</string>
2829
3306
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -2977,7 +3454,7 @@ return {
2977
3454
  ]]></string>
2978
3455
  </Properties>
2979
3456
  </Item>
2980
- <Item class="ModuleScript" referent="12">
3457
+ <Item class="ModuleScript" referent="13">
2981
3458
  <Properties>
2982
3459
  <string name="Name">GenerateModelHandlers</string>
2983
3460
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3182,7 +3659,7 @@ return {
3182
3659
  ]]></string>
3183
3660
  </Properties>
3184
3661
  </Item>
3185
- <Item class="ModuleScript" referent="13">
3662
+ <Item class="ModuleScript" referent="14">
3186
3663
  <Properties>
3187
3664
  <string name="Name">InputHandlers</string>
3188
3665
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3382,7 +3859,7 @@ return {
3382
3859
  ]]></string>
3383
3860
  </Properties>
3384
3861
  </Item>
3385
- <Item class="ModuleScript" referent="14">
3862
+ <Item class="ModuleScript" referent="15">
3386
3863
  <Properties>
3387
3864
  <string name="Name">LogHandlers</string>
3388
3865
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3404,7 +3881,7 @@ return {
3404
3881
  ]]></string>
3405
3882
  </Properties>
3406
3883
  </Item>
3407
- <Item class="ModuleScript" referent="15">
3884
+ <Item class="ModuleScript" referent="16">
3408
3885
  <Properties>
3409
3886
  <string name="Name">MemoryHandlers</string>
3410
3887
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3472,7 +3949,7 @@ return {
3472
3949
  ]]></string>
3473
3950
  </Properties>
3474
3951
  </Item>
3475
- <Item class="ModuleScript" referent="16">
3952
+ <Item class="ModuleScript" referent="17">
3476
3953
  <Properties>
3477
3954
  <string name="Name">MetadataHandlers</string>
3478
3955
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3829,7 +4306,7 @@ return {
3829
4306
  ]]></string>
3830
4307
  </Properties>
3831
4308
  </Item>
3832
- <Item class="ModuleScript" referent="17">
4309
+ <Item class="ModuleScript" referent="18">
3833
4310
  <Properties>
3834
4311
  <string name="Name">MicroProfilerHandlers</string>
3835
4312
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -5582,7 +6059,7 @@ return {
5582
6059
  ]]></string>
5583
6060
  </Properties>
5584
6061
  </Item>
5585
- <Item class="ModuleScript" referent="18">
6062
+ <Item class="ModuleScript" referent="19">
5586
6063
  <Properties>
5587
6064
  <string name="Name">PropertyHandlers</string>
5588
6065
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -5667,6 +6144,7 @@ local function setProperties(requestData)
5667
6144
  -- execute_luau edits as well, even when every assignment in this call failed.
5668
6145
  finishRecording(recordingId, true)
5669
6146
  return {
6147
+ success = failureCount == 0,
5670
6148
  instancePath = instancePath,
5671
6149
  summary = {
5672
6150
  total = successCount + failureCount,
@@ -5682,7 +6160,7 @@ return {
5682
6160
  ]]></string>
5683
6161
  </Properties>
5684
6162
  </Item>
5685
- <Item class="ModuleScript" referent="19">
6163
+ <Item class="ModuleScript" referent="20">
5686
6164
  <Properties>
5687
6165
  <string name="Name">QueryHandlers</string>
5688
6166
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -6337,7 +6815,7 @@ return {
6337
6815
  ]]></string>
6338
6816
  </Properties>
6339
6817
  </Item>
6340
- <Item class="ModuleScript" referent="20">
6818
+ <Item class="ModuleScript" referent="21">
6341
6819
  <Properties>
6342
6820
  <string name="Name">SceneAnalysisHandlers</string>
6343
6821
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -6586,7 +7064,7 @@ return {
6586
7064
  ]]></string>
6587
7065
  </Properties>
6588
7066
  </Item>
6589
- <Item class="ModuleScript" referent="21">
7067
+ <Item class="ModuleScript" referent="22">
6590
7068
  <Properties>
6591
7069
  <string name="Name">ScriptHandlers</string>
6592
7070
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -7258,7 +7736,7 @@ return {
7258
7736
  ]]></string>
7259
7737
  </Properties>
7260
7738
  </Item>
7261
- <Item class="ModuleScript" referent="22">
7739
+ <Item class="ModuleScript" referent="23">
7262
7740
  <Properties>
7263
7741
  <string name="Name">ScriptProfilerHandlers</string>
7264
7742
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -7721,7 +8199,7 @@ return {
7721
8199
  ]]></string>
7722
8200
  </Properties>
7723
8201
  </Item>
7724
- <Item class="ModuleScript" referent="23">
8202
+ <Item class="ModuleScript" referent="24">
7725
8203
  <Properties>
7726
8204
  <string name="Name">SerializationHandlers</string>
7727
8205
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -7907,7 +8385,7 @@ return {
7907
8385
  ]]></string>
7908
8386
  </Properties>
7909
8387
  </Item>
7910
- <Item class="ModuleScript" referent="24">
8388
+ <Item class="ModuleScript" referent="25">
7911
8389
  <Properties>
7912
8390
  <string name="Name">TestHandlers</string>
7913
8391
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -8298,7 +8776,7 @@ return {
8298
8776
  </Properties>
8299
8777
  </Item>
8300
8778
  </Item>
8301
- <Item class="ModuleScript" referent="25">
8779
+ <Item class="ModuleScript" referent="26">
8302
8780
  <Properties>
8303
8781
  <string name="Name">HttpDiagnostics</string>
8304
8782
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -8379,7 +8857,7 @@ return {
8379
8857
  ]]></string>
8380
8858
  </Properties>
8381
8859
  </Item>
8382
- <Item class="ModuleScript" referent="26">
8860
+ <Item class="ModuleScript" referent="27">
8383
8861
  <Properties>
8384
8862
  <string name="Name">LuauExec</string>
8385
8863
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -8813,7 +9291,7 @@ return {
8813
9291
  ]]></string>
8814
9292
  </Properties>
8815
9293
  </Item>
8816
- <Item class="ModuleScript" referent="27">
9294
+ <Item class="ModuleScript" referent="28">
8817
9295
  <Properties>
8818
9296
  <string name="Name">PeerRole</string>
8819
9297
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -8834,7 +9312,7 @@ return {
8834
9312
  ]]></string>
8835
9313
  </Properties>
8836
9314
  </Item>
8837
- <Item class="ModuleScript" referent="28">
9315
+ <Item class="ModuleScript" referent="29">
8838
9316
  <Properties>
8839
9317
  <string name="Name">PluginSession</string>
8840
9318
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9023,7 +9501,7 @@ return {
9023
9501
  ]]></string>
9024
9502
  </Properties>
9025
9503
  </Item>
9026
- <Item class="ModuleScript" referent="29">
9504
+ <Item class="ModuleScript" referent="30">
9027
9505
  <Properties>
9028
9506
  <string name="Name">Recording</string>
9029
9507
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9053,7 +9531,7 @@ return {
9053
9531
  ]]></string>
9054
9532
  </Properties>
9055
9533
  </Item>
9056
- <Item class="ModuleScript" referent="30">
9534
+ <Item class="ModuleScript" referent="31">
9057
9535
  <Properties>
9058
9536
  <string name="Name">RenderMonitor</string>
9059
9537
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9121,7 +9599,7 @@ return {
9121
9599
  ]]></string>
9122
9600
  </Properties>
9123
9601
  </Item>
9124
- <Item class="ModuleScript" referent="31">
9602
+ <Item class="ModuleScript" referent="32">
9125
9603
  <Properties>
9126
9604
  <string name="Name">RuntimeLogBuffer</string>
9127
9605
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9357,7 +9835,7 @@ return {
9357
9835
  ]]></string>
9358
9836
  </Properties>
9359
9837
  </Item>
9360
- <Item class="ModuleScript" referent="32">
9838
+ <Item class="ModuleScript" referent="33">
9361
9839
  <Properties>
9362
9840
  <string name="Name">ScriptSearch</string>
9363
9841
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9684,7 +10162,7 @@ return {
9684
10162
  ]]></string>
9685
10163
  </Properties>
9686
10164
  </Item>
9687
- <Item class="ModuleScript" referent="33">
10165
+ <Item class="ModuleScript" referent="34">
9688
10166
  <Properties>
9689
10167
  <string name="Name">ServerUrlSettings</string>
9690
10168
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9823,11 +10301,11 @@ return {
9823
10301
  ]]></string>
9824
10302
  </Properties>
9825
10303
  </Item>
9826
- <Item class="ModuleScript" referent="34">
10304
+ <Item class="ModuleScript" referent="35">
9827
10305
  <Properties>
9828
10306
  <string name="Name">State</string>
9829
10307
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
9830
- local CURRENT_VERSION = "3.1.3"
10308
+ local CURRENT_VERSION = "3.1.4"
9831
10309
  local PLUGIN_VARIANT = "inspector"
9832
10310
  local BASE_PORT = 58741
9833
10311
  local function createConnection(port)
@@ -9857,7 +10335,7 @@ return {
9857
10335
  ]]></string>
9858
10336
  </Properties>
9859
10337
  </Item>
9860
- <Item class="ModuleScript" referent="35">
10338
+ <Item class="ModuleScript" referent="36">
9861
10339
  <Properties>
9862
10340
  <string name="Name">StopPlayMonitor</string>
9863
10341
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -10090,7 +10568,7 @@ return {
10090
10568
  ]]></string>
10091
10569
  </Properties>
10092
10570
  </Item>
10093
- <Item class="ModuleScript" referent="36">
10571
+ <Item class="ModuleScript" referent="37">
10094
10572
  <Properties>
10095
10573
  <string name="Name">StudioWebSocket</string>
10096
10574
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -11041,7 +11519,7 @@ return {
11041
11519
  ]]></string>
11042
11520
  </Properties>
11043
11521
  </Item>
11044
- <Item class="ModuleScript" referent="37">
11522
+ <Item class="ModuleScript" referent="38">
11045
11523
  <Properties>
11046
11524
  <string name="Name">TopologyId</string>
11047
11525
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -11086,7 +11564,7 @@ return {
11086
11564
  ]]></string>
11087
11565
  </Properties>
11088
11566
  </Item>
11089
- <Item class="ModuleScript" referent="38">
11567
+ <Item class="ModuleScript" referent="39">
11090
11568
  <Properties>
11091
11569
  <string name="Name">UI</string>
11092
11570
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -11638,7 +12116,7 @@ return {
11638
12116
  ]]></string>
11639
12117
  </Properties>
11640
12118
  </Item>
11641
- <Item class="ModuleScript" referent="39">
12119
+ <Item class="ModuleScript" referent="40">
11642
12120
  <Properties>
11643
12121
  <string name="Name">Utils</string>
11644
12122
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -12479,11 +12957,11 @@ return {
12479
12957
  </Properties>
12480
12958
  </Item>
12481
12959
  </Item>
12482
- <Item class="Folder" referent="44">
12960
+ <Item class="Folder" referent="45">
12483
12961
  <Properties>
12484
12962
  <string name="Name">include</string>
12485
12963
  </Properties>
12486
- <Item class="ModuleScript" referent="40">
12964
+ <Item class="ModuleScript" referent="41">
12487
12965
  <Properties>
12488
12966
  <string name="Name">LibMP</string>
12489
12967
  <string name="Source"><![CDATA[-- =============================================================================
@@ -168867,7 +169345,7 @@ return LibMP
168867
169345
  ]]></string>
168868
169346
  </Properties>
168869
169347
  </Item>
168870
- <Item class="ModuleScript" referent="41">
169348
+ <Item class="ModuleScript" referent="42">
168871
169349
  <Properties>
168872
169350
  <string name="Name">Promise</string>
168873
169351
  <string name="Source"><![CDATA[--[[
@@ -170941,7 +171419,7 @@ return Promise
170941
171419
  ]]></string>
170942
171420
  </Properties>
170943
171421
  </Item>
170944
- <Item class="ModuleScript" referent="42">
171422
+ <Item class="ModuleScript" referent="43">
170945
171423
  <Properties>
170946
171424
  <string name="Name">RuntimeLib</string>
170947
171425
  <string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
@@ -171208,15 +171686,15 @@ return TS
171208
171686
  </Properties>
171209
171687
  </Item>
171210
171688
  </Item>
171211
- <Item class="Folder" referent="45">
171689
+ <Item class="Folder" referent="46">
171212
171690
  <Properties>
171213
171691
  <string name="Name">node_modules</string>
171214
171692
  </Properties>
171215
- <Item class="Folder" referent="46">
171693
+ <Item class="Folder" referent="47">
171216
171694
  <Properties>
171217
171695
  <string name="Name">@rbxts</string>
171218
171696
  </Properties>
171219
- <Item class="ModuleScript" referent="43">
171697
+ <Item class="ModuleScript" referent="44">
171220
171698
  <Properties>
171221
171699
  <string name="Name">services</string>
171222
171700
  <string name="Source"><![CDATA[return setmetatable({}, {