@chrrxs/robloxstudio-mcp-inspector 3.1.2 → 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.2" 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.2`
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
@@ -7985,6 +8463,16 @@ local function startPlaytest(requestData)
7985
8463
  error = "start_playtest is single-player only. Use multiplayer_test_start for multi-client StudioTestService sessions.",
7986
8464
  }
7987
8465
  end
8466
+ -- Peer teardown and ExecutePlayModeAsync completion can precede native edit
8467
+ -- readiness. Do not acknowledge a new start that the engine cannot execute.
8468
+ if not StudioTestService.EditModeActive then
8469
+ return {
8470
+ success = false,
8471
+ error = "Studio is not ready to start a playtest.",
8472
+ message = "Wait for Studio to finish its current playtest transition before starting another.",
8473
+ editModeReady = false,
8474
+ }
8475
+ end
7988
8476
  -- Self-heal: if testRunning is stuck true but Studio reports no active
7989
8477
  -- playtest, the previous start_playtest's task.spawn was orphaned
7990
8478
  -- (plugin reload mid-test, Studio entered some inconsistent state, etc).
@@ -8064,22 +8552,24 @@ local function stopPlaytest(_requestData)
8064
8552
  }
8065
8553
  end
8066
8554
  StopPlayMonitor.clearPending(stopRequest.requestId)
8067
- -- Request was consumed (EndTest called). ExecutePlayModeAsync in our
8068
- -- startPlaytest task.spawn is still unwinding though — testRunning stays
8069
- -- true until that yield completes and the post-block runs. Wait so
8070
- -- back-to-back stop -> start sequences don't race against the prior
8071
- -- teardown and get "A test is already running". 10s covers play-DM
8072
- -- teardown on heavier places; if it still hasn't cleared we return
8073
- -- anyway so users aren't stuck — but note that in the response so the
8074
- -- caller knows a subsequent start may need a moment.
8555
+ -- EndTest consumption, task completion, and native edit readiness are distinct
8556
+ -- milestones. In particular, testRunning can clear before Studio accepts another
8557
+ -- ExecutePlayModeAsync call (and is never set for manually started playtests).
8558
+ -- Only report completion after both tracked execution and native teardown settle.
8075
8559
  local deadline = tick() + 10
8076
- while testRunning and tick() < deadline do
8560
+ while (testRunning or not StudioTestService.EditModeActive) and tick() < deadline do
8077
8561
  task.wait(0.1)
8078
8562
  end
8079
- if testRunning then
8563
+ local editModeReady = StudioTestService.EditModeActive
8564
+ if testRunning or not editModeReady then
8080
8565
  return {
8081
- success = true,
8082
- message = "Playtest stop signal sent; teardown still in progress.",
8566
+ success = false,
8567
+ error = "Playtest teardown did not complete.",
8568
+ message = "Stop signal was accepted, but Studio teardown did not settle before timeout.",
8569
+ stopSignalAccepted = true,
8570
+ editModeReady = editModeReady,
8571
+ playtestTaskPending = testRunning,
8572
+ timedOut = true,
8083
8573
  }
8084
8574
  end
8085
8575
  return {
@@ -8286,7 +8776,7 @@ return {
8286
8776
  </Properties>
8287
8777
  </Item>
8288
8778
  </Item>
8289
- <Item class="ModuleScript" referent="25">
8779
+ <Item class="ModuleScript" referent="26">
8290
8780
  <Properties>
8291
8781
  <string name="Name">HttpDiagnostics</string>
8292
8782
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -8367,7 +8857,7 @@ return {
8367
8857
  ]]></string>
8368
8858
  </Properties>
8369
8859
  </Item>
8370
- <Item class="ModuleScript" referent="26">
8860
+ <Item class="ModuleScript" referent="27">
8371
8861
  <Properties>
8372
8862
  <string name="Name">LuauExec</string>
8373
8863
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -8801,7 +9291,7 @@ return {
8801
9291
  ]]></string>
8802
9292
  </Properties>
8803
9293
  </Item>
8804
- <Item class="ModuleScript" referent="27">
9294
+ <Item class="ModuleScript" referent="28">
8805
9295
  <Properties>
8806
9296
  <string name="Name">PeerRole</string>
8807
9297
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -8822,7 +9312,7 @@ return {
8822
9312
  ]]></string>
8823
9313
  </Properties>
8824
9314
  </Item>
8825
- <Item class="ModuleScript" referent="28">
9315
+ <Item class="ModuleScript" referent="29">
8826
9316
  <Properties>
8827
9317
  <string name="Name">PluginSession</string>
8828
9318
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9011,7 +9501,7 @@ return {
9011
9501
  ]]></string>
9012
9502
  </Properties>
9013
9503
  </Item>
9014
- <Item class="ModuleScript" referent="29">
9504
+ <Item class="ModuleScript" referent="30">
9015
9505
  <Properties>
9016
9506
  <string name="Name">Recording</string>
9017
9507
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9041,7 +9531,7 @@ return {
9041
9531
  ]]></string>
9042
9532
  </Properties>
9043
9533
  </Item>
9044
- <Item class="ModuleScript" referent="30">
9534
+ <Item class="ModuleScript" referent="31">
9045
9535
  <Properties>
9046
9536
  <string name="Name">RenderMonitor</string>
9047
9537
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9109,7 +9599,7 @@ return {
9109
9599
  ]]></string>
9110
9600
  </Properties>
9111
9601
  </Item>
9112
- <Item class="ModuleScript" referent="31">
9602
+ <Item class="ModuleScript" referent="32">
9113
9603
  <Properties>
9114
9604
  <string name="Name">RuntimeLogBuffer</string>
9115
9605
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9345,7 +9835,7 @@ return {
9345
9835
  ]]></string>
9346
9836
  </Properties>
9347
9837
  </Item>
9348
- <Item class="ModuleScript" referent="32">
9838
+ <Item class="ModuleScript" referent="33">
9349
9839
  <Properties>
9350
9840
  <string name="Name">ScriptSearch</string>
9351
9841
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9672,7 +10162,7 @@ return {
9672
10162
  ]]></string>
9673
10163
  </Properties>
9674
10164
  </Item>
9675
- <Item class="ModuleScript" referent="33">
10165
+ <Item class="ModuleScript" referent="34">
9676
10166
  <Properties>
9677
10167
  <string name="Name">ServerUrlSettings</string>
9678
10168
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9811,11 +10301,11 @@ return {
9811
10301
  ]]></string>
9812
10302
  </Properties>
9813
10303
  </Item>
9814
- <Item class="ModuleScript" referent="34">
10304
+ <Item class="ModuleScript" referent="35">
9815
10305
  <Properties>
9816
10306
  <string name="Name">State</string>
9817
10307
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
9818
- local CURRENT_VERSION = "3.1.2"
10308
+ local CURRENT_VERSION = "3.1.4"
9819
10309
  local PLUGIN_VARIANT = "inspector"
9820
10310
  local BASE_PORT = 58741
9821
10311
  local function createConnection(port)
@@ -9845,7 +10335,7 @@ return {
9845
10335
  ]]></string>
9846
10336
  </Properties>
9847
10337
  </Item>
9848
- <Item class="ModuleScript" referent="35">
10338
+ <Item class="ModuleScript" referent="36">
9849
10339
  <Properties>
9850
10340
  <string name="Name">StopPlayMonitor</string>
9851
10341
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -10078,7 +10568,7 @@ return {
10078
10568
  ]]></string>
10079
10569
  </Properties>
10080
10570
  </Item>
10081
- <Item class="ModuleScript" referent="36">
10571
+ <Item class="ModuleScript" referent="37">
10082
10572
  <Properties>
10083
10573
  <string name="Name">StudioWebSocket</string>
10084
10574
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -11029,7 +11519,7 @@ return {
11029
11519
  ]]></string>
11030
11520
  </Properties>
11031
11521
  </Item>
11032
- <Item class="ModuleScript" referent="37">
11522
+ <Item class="ModuleScript" referent="38">
11033
11523
  <Properties>
11034
11524
  <string name="Name">TopologyId</string>
11035
11525
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -11074,7 +11564,7 @@ return {
11074
11564
  ]]></string>
11075
11565
  </Properties>
11076
11566
  </Item>
11077
- <Item class="ModuleScript" referent="38">
11567
+ <Item class="ModuleScript" referent="39">
11078
11568
  <Properties>
11079
11569
  <string name="Name">UI</string>
11080
11570
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -11626,7 +12116,7 @@ return {
11626
12116
  ]]></string>
11627
12117
  </Properties>
11628
12118
  </Item>
11629
- <Item class="ModuleScript" referent="39">
12119
+ <Item class="ModuleScript" referent="40">
11630
12120
  <Properties>
11631
12121
  <string name="Name">Utils</string>
11632
12122
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -12467,11 +12957,11 @@ return {
12467
12957
  </Properties>
12468
12958
  </Item>
12469
12959
  </Item>
12470
- <Item class="Folder" referent="44">
12960
+ <Item class="Folder" referent="45">
12471
12961
  <Properties>
12472
12962
  <string name="Name">include</string>
12473
12963
  </Properties>
12474
- <Item class="ModuleScript" referent="40">
12964
+ <Item class="ModuleScript" referent="41">
12475
12965
  <Properties>
12476
12966
  <string name="Name">LibMP</string>
12477
12967
  <string name="Source"><![CDATA[-- =============================================================================
@@ -168855,7 +169345,7 @@ return LibMP
168855
169345
  ]]></string>
168856
169346
  </Properties>
168857
169347
  </Item>
168858
- <Item class="ModuleScript" referent="41">
169348
+ <Item class="ModuleScript" referent="42">
168859
169349
  <Properties>
168860
169350
  <string name="Name">Promise</string>
168861
169351
  <string name="Source"><![CDATA[--[[
@@ -170929,7 +171419,7 @@ return Promise
170929
171419
  ]]></string>
170930
171420
  </Properties>
170931
171421
  </Item>
170932
- <Item class="ModuleScript" referent="42">
171422
+ <Item class="ModuleScript" referent="43">
170933
171423
  <Properties>
170934
171424
  <string name="Name">RuntimeLib</string>
170935
171425
  <string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
@@ -171196,15 +171686,15 @@ return TS
171196
171686
  </Properties>
171197
171687
  </Item>
171198
171688
  </Item>
171199
- <Item class="Folder" referent="45">
171689
+ <Item class="Folder" referent="46">
171200
171690
  <Properties>
171201
171691
  <string name="Name">node_modules</string>
171202
171692
  </Properties>
171203
- <Item class="Folder" referent="46">
171693
+ <Item class="Folder" referent="47">
171204
171694
  <Properties>
171205
171695
  <string name="Name">@rbxts</string>
171206
171696
  </Properties>
171207
- <Item class="ModuleScript" referent="43">
171697
+ <Item class="ModuleScript" referent="44">
171208
171698
  <Properties>
171209
171699
  <string name="Name">services</string>
171210
171700
  <string name="Source"><![CDATA[return setmetatable({}, {