@chrrxs/robloxstudio-mcp-inspector 3.1.3 → 3.1.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/dist/index.js +1156 -303
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +1497 -175
package/dist/index.js
CHANGED
|
@@ -1945,6 +1945,15 @@ import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
|
1945
1945
|
|
|
1946
1946
|
// ../core/dist/bridge-service.js
|
|
1947
1947
|
import { createHash, randomUUID } from "crypto";
|
|
1948
|
+
var MultiplayerGroupInUseError = class extends Error {
|
|
1949
|
+
groupId;
|
|
1950
|
+
code = "multiplayer_group_in_use";
|
|
1951
|
+
constructor(groupId) {
|
|
1952
|
+
super(`Multiplayer Group "${groupId}" still has connected runtime peers and cannot be removed.`);
|
|
1953
|
+
this.groupId = groupId;
|
|
1954
|
+
this.name = "MultiplayerGroupInUseError";
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1948
1957
|
function isExecutionOutcome(value) {
|
|
1949
1958
|
return value === "success" || value === "error" || value === "not_executed" || value === "unknown";
|
|
1950
1959
|
}
|
|
@@ -2233,6 +2242,11 @@ var BridgeService = class {
|
|
|
2233
2242
|
const group = this.multiplayerGroupsById.get(groupId);
|
|
2234
2243
|
if (!group)
|
|
2235
2244
|
return void 0;
|
|
2245
|
+
for (const peer of this.peersById.values()) {
|
|
2246
|
+
if (peer.multiplayerGroupId === groupId && isRuntimeRole(peer.role)) {
|
|
2247
|
+
throw new MultiplayerGroupInUseError(groupId);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2236
2250
|
this.multiplayerGroupsById.delete(groupId);
|
|
2237
2251
|
for (const peer of this.peersById.values()) {
|
|
2238
2252
|
if (peer.multiplayerGroupId === groupId)
|
|
@@ -2855,7 +2869,7 @@ var BridgeService = class {
|
|
|
2855
2869
|
operation.status.executionStartedAt = now;
|
|
2856
2870
|
else {
|
|
2857
2871
|
operation.status.executionOutcome = outcome ?? "unknown";
|
|
2858
|
-
if (outcome
|
|
2872
|
+
if (outcome === "success" || outcome === "error")
|
|
2859
2873
|
operation.status.executionCompletedAt = now;
|
|
2860
2874
|
}
|
|
2861
2875
|
operation.updatedAt = now;
|
|
@@ -2871,7 +2885,7 @@ var BridgeService = class {
|
|
|
2871
2885
|
requestId,
|
|
2872
2886
|
targetPeerId: operation.status.targetPeerId,
|
|
2873
2887
|
...observations(operation.status),
|
|
2874
|
-
executionCompletedAt: executionOutcome === "
|
|
2888
|
+
executionCompletedAt: executionOutcome === "success" || executionOutcome === "error" ? operation.status.executionCompletedAt ?? Date.now() : void 0,
|
|
2875
2889
|
stage: "response_delivery",
|
|
2876
2890
|
outcome: executionOutcome === "not_executed" ? "not_executed" : "unknown",
|
|
2877
2891
|
executionOutcome
|
|
@@ -2899,13 +2913,15 @@ var BridgeService = class {
|
|
|
2899
2913
|
operation.status.state = "settled";
|
|
2900
2914
|
const localRejection = error instanceof RequestFailure && error.details.transportStage === "server_send";
|
|
2901
2915
|
const responseOutcome = handlerOutcome(response);
|
|
2902
|
-
const completedOutcome = !hasError && responseOutcome === "error" ? "error" : executionOutcome ?? (localRejection ? "not_executed" : hasError ? operation.status.executionOutcome === "success" ? "success" : "error" : responseOutcome);
|
|
2916
|
+
const completedOutcome = executionOutcome === "unknown" || executionOutcome === "not_executed" ? executionOutcome : !hasError && responseOutcome === "error" ? "error" : executionOutcome ?? (localRejection ? "not_executed" : hasError ? operation.status.executionOutcome === "success" ? "success" : "error" : responseOutcome);
|
|
2903
2917
|
operation.status.executionOutcome = completedOutcome;
|
|
2904
|
-
operation.status.outcome = hasError || completedOutcome === "error" || completedOutcome === "not_executed" ? "error" : "success";
|
|
2918
|
+
operation.status.outcome = hasError || responseOutcome === "error" || completedOutcome === "error" || completedOutcome === "not_executed" ? "error" : "success";
|
|
2905
2919
|
if (!localRejection) {
|
|
2906
2920
|
operation.status.stage = "response_delivery";
|
|
2907
|
-
if (completedOutcome
|
|
2921
|
+
if (completedOutcome === "success" || completedOutcome === "error")
|
|
2908
2922
|
operation.status.executionCompletedAt ??= now;
|
|
2923
|
+
else
|
|
2924
|
+
delete operation.status.executionCompletedAt;
|
|
2909
2925
|
}
|
|
2910
2926
|
operation.status.settledAt = now;
|
|
2911
2927
|
operation.updatedAt = now;
|
|
@@ -3308,6 +3324,10 @@ execute_luau runs through the Studio plugin. eval_server_runtime and eval_client
|
|
|
3308
3324
|
|
|
3309
3325
|
Read output with get_runtime_logs. Reuse nextCursor as cursor for one Instance, or nextCursorByInstance as cursor_by_instance for a Multiplayer Group, instead of requesting the full process streams again.
|
|
3310
3326
|
|
|
3327
|
+
- Each Peer VM retains approximately 64 KiB of message text, rather than a fixed message count. Reads do not consume or clear the buffer; incoming messages evict old entries.
|
|
3328
|
+
- totalDropped sums lifetime eviction counts from successfully read Peers. It counts messages evicted, not messages missed by this caller, and may decrease when Peers fail, disconnect, or reload.
|
|
3329
|
+
- For successfully read Peers, nextCursor advances past all captured entries, including those omitted by filter or tail. It is a continuation watermark, not pagination through omitted results; continuing from it will not return those entries.
|
|
3330
|
+
|
|
3311
3331
|
## Simulation and input
|
|
3312
3332
|
|
|
3313
3333
|
- Inspect current settings with get_simulation_state before changing them.
|
|
@@ -3936,6 +3956,40 @@ var StudioLaunchPreDispatchError = class extends Error {
|
|
|
3936
3956
|
};
|
|
3937
3957
|
}
|
|
3938
3958
|
};
|
|
3959
|
+
var StudioCloseVerificationError = class extends Error {
|
|
3960
|
+
observation;
|
|
3961
|
+
constructor(message, observation) {
|
|
3962
|
+
super(message);
|
|
3963
|
+
this.observation = observation;
|
|
3964
|
+
}
|
|
3965
|
+
};
|
|
3966
|
+
async function beforeCloseDeadline(operation, deadline, processId) {
|
|
3967
|
+
const { promise, reject } = Promise.withResolvers();
|
|
3968
|
+
const timeoutError = () => new StudioCloseVerificationError(`Timed out verifying Studio process ${processId} termination; process state is unknown.`, { status: "unknown", observedAt: Date.now(), error: "Close operation deadline exceeded." });
|
|
3969
|
+
if (Date.now() >= deadline)
|
|
3970
|
+
throw timeoutError();
|
|
3971
|
+
const timer = setTimeout(() => reject(timeoutError()), deadline - Date.now());
|
|
3972
|
+
try {
|
|
3973
|
+
return await Promise.race([operation(), promise]);
|
|
3974
|
+
} finally {
|
|
3975
|
+
clearTimeout(timer);
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
function observePosixProcess(processId) {
|
|
3979
|
+
try {
|
|
3980
|
+
process.kill(processId, 0);
|
|
3981
|
+
return { status: "running", observedAt: Date.now() };
|
|
3982
|
+
} catch (error) {
|
|
3983
|
+
if (error instanceof Error && "code" in error && error.code === "ESRCH") {
|
|
3984
|
+
return { status: "not_running", observedAt: Date.now(), reason: "missing" };
|
|
3985
|
+
}
|
|
3986
|
+
return {
|
|
3987
|
+
status: "unknown",
|
|
3988
|
+
observedAt: Date.now(),
|
|
3989
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3990
|
+
};
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3939
3993
|
function run(command, args, options = {}) {
|
|
3940
3994
|
return execFileSync2(command, args, {
|
|
3941
3995
|
encoding: "utf8",
|
|
@@ -3957,9 +4011,10 @@ async function runAsync(command, args, options = {}) {
|
|
|
3957
4011
|
function isWsl() {
|
|
3958
4012
|
return getStudioPlatformCapabilities().isWsl;
|
|
3959
4013
|
}
|
|
3960
|
-
async function powershellAsync(script) {
|
|
4014
|
+
async function powershellAsync(script, timeoutMs = 15e3) {
|
|
3961
4015
|
return runAsync("powershell.exe", ["-NoProfile", "-Command", script], {
|
|
3962
|
-
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
|
|
4016
|
+
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
|
|
4017
|
+
timeout: timeoutMs
|
|
3963
4018
|
});
|
|
3964
4019
|
}
|
|
3965
4020
|
function windowsLocalAppData() {
|
|
@@ -4094,8 +4149,15 @@ function quoteWindowsCommandLineArg(value) {
|
|
|
4094
4149
|
}
|
|
4095
4150
|
return `${quoted}${"\\".repeat(backslashes * 2)}"`;
|
|
4096
4151
|
}
|
|
4097
|
-
function
|
|
4152
|
+
function parseStudioTestWorkerJobName(name) {
|
|
4153
|
+
if (name !== void 0 && !/^Local\\RsmcpStudioWorker-[a-f0-9]{32}$/u.test(name)) {
|
|
4154
|
+
throw new Error("Invalid Studio test worker job name.");
|
|
4155
|
+
}
|
|
4156
|
+
return name;
|
|
4157
|
+
}
|
|
4158
|
+
function buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment, windowsWorkingDirectory, testWorkerJobName = process.env.RSMCP_STUDIO_TEST_WORKER_JOB) {
|
|
4098
4159
|
const environmentPatch = parseStudioProcessEnvironmentPatch(processEnvironment);
|
|
4160
|
+
parseStudioTestWorkerJobName(testWorkerJobName);
|
|
4099
4161
|
const commandLine = [windowsExe, ...args].map(quoteWindowsCommandLineArg).join(" ");
|
|
4100
4162
|
return [
|
|
4101
4163
|
"$ErrorActionPreference = 'Stop'",
|
|
@@ -4208,6 +4270,15 @@ public sealed class McpSuspendedStudio : IDisposable
|
|
|
4208
4270
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
4209
4271
|
private static extern IntPtr CreateJobObjectW(IntPtr jobAttributes, string name);
|
|
4210
4272
|
|
|
4273
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
4274
|
+
private static extern IntPtr OpenJobObjectW(uint access, bool inheritHandle, string name);
|
|
4275
|
+
|
|
4276
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
4277
|
+
private static extern bool GetHandleInformation(IntPtr handle, out uint flags);
|
|
4278
|
+
|
|
4279
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
4280
|
+
private static extern bool QueryInformationJobObject(IntPtr job, int informationClass, IntPtr information, uint informationLength, out uint returned);
|
|
4281
|
+
|
|
4211
4282
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
4212
4283
|
private static extern bool SetInformationJobObject(
|
|
4213
4284
|
IntPtr job,
|
|
@@ -4295,20 +4366,60 @@ public sealed class McpSuspendedStudio : IDisposable
|
|
|
4295
4366
|
throw new InvalidOperationException("Unexpected Studio process wait result: " + wait);
|
|
4296
4367
|
}
|
|
4297
4368
|
|
|
4298
|
-
|
|
4369
|
+
private static Win32Exception TestWorkerError(string operation)
|
|
4370
|
+
{
|
|
4371
|
+
int error = Marshal.GetLastWin32Error();
|
|
4372
|
+
return new Win32Exception(error, operation + " (Win32 " + error + "): " + new Win32Exception(error).Message);
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
private static IntPtr OpenTestWorkerJob(string name)
|
|
4376
|
+
{
|
|
4377
|
+
if (name == null) return IntPtr.Zero;
|
|
4378
|
+
if (!System.Text.RegularExpressions.Regex.IsMatch(name, @"\\ALocal\\\\RsmcpStudioWorker-[a-f0-9]{32}\\z"))
|
|
4379
|
+
throw new ArgumentException("Invalid Studio test worker job name");
|
|
4380
|
+
IntPtr handle = OpenJobObjectW(5, false, name);
|
|
4381
|
+
if (handle == IntPtr.Zero)
|
|
4382
|
+
throw TestWorkerError("Open test worker job failed");
|
|
4383
|
+
try
|
|
4384
|
+
{
|
|
4385
|
+
uint flags;
|
|
4386
|
+
if (!GetHandleInformation(handle, out flags))
|
|
4387
|
+
throw TestWorkerError("Get test worker job handle flags failed");
|
|
4388
|
+
if ((flags & 1) != 0)
|
|
4389
|
+
throw new InvalidOperationException("Test worker job handle must not be inherited");
|
|
4390
|
+
int length = Marshal.SizeOf(typeof(ExtendedLimitInformation));
|
|
4391
|
+
IntPtr buffer = Marshal.AllocHGlobal(length);
|
|
4392
|
+
try
|
|
4393
|
+
{
|
|
4394
|
+
uint returned;
|
|
4395
|
+
if (!QueryInformationJobObject(handle, JOB_OBJECT_EXTENDED_LIMIT_INFORMATION, buffer, (uint)length, out returned))
|
|
4396
|
+
throw TestWorkerError("Query test worker job limits failed");
|
|
4397
|
+
var information = (ExtendedLimitInformation)Marshal.PtrToStructure(buffer, typeof(ExtendedLimitInformation));
|
|
4398
|
+
if (returned != length || information.BasicLimitInformation.LimitFlags != JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)
|
|
4399
|
+
throw new InvalidOperationException("Test worker job must retain kill-on-close without breakaway");
|
|
4400
|
+
}
|
|
4401
|
+
finally { Marshal.FreeHGlobal(buffer); }
|
|
4402
|
+
return handle;
|
|
4403
|
+
}
|
|
4404
|
+
catch { CloseHandle(handle); throw; }
|
|
4405
|
+
}
|
|
4406
|
+
|
|
4407
|
+
public static McpSuspendedStudio Start(string application, string commandLine, string currentDirectory, string testWorkerJobName)
|
|
4299
4408
|
{
|
|
4300
4409
|
if (String.IsNullOrEmpty(currentDirectory))
|
|
4301
4410
|
currentDirectory = null;
|
|
4302
4411
|
IntPtr jobHandle = CreateJobObjectW(IntPtr.Zero, null);
|
|
4303
4412
|
if (jobHandle == IntPtr.Zero)
|
|
4304
4413
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateJobObjectW failed");
|
|
4414
|
+
IntPtr workerJob = IntPtr.Zero;
|
|
4305
4415
|
try
|
|
4306
4416
|
{
|
|
4307
4417
|
ConfigureKillOnClose(jobHandle, true);
|
|
4418
|
+
workerJob = OpenTestWorkerJob(testWorkerJobName);
|
|
4308
4419
|
var startup = new StartupInfo();
|
|
4309
4420
|
startup.cb = Marshal.SizeOf(startup);
|
|
4310
4421
|
ProcessInformation created;
|
|
4311
|
-
uint flags = CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB;
|
|
4422
|
+
uint flags = CREATE_SUSPENDED | (workerJob == IntPtr.Zero ? CREATE_BREAKAWAY_FROM_JOB : 0);
|
|
4312
4423
|
bool started = CreateProcessW(application, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, false, flags, IntPtr.Zero, currentDirectory, ref startup, out created);
|
|
4313
4424
|
if (!started && Marshal.GetLastWin32Error() == 5)
|
|
4314
4425
|
started = CreateProcessW(application, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, false, CREATE_SUSPENDED, IntPtr.Zero, currentDirectory, ref startup, out created);
|
|
@@ -4316,6 +4427,10 @@ public sealed class McpSuspendedStudio : IDisposable
|
|
|
4316
4427
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateProcessW failed");
|
|
4317
4428
|
try
|
|
4318
4429
|
{
|
|
4430
|
+
// Worker lifetime encloses the private launch-authorization job.
|
|
4431
|
+
// COMPLETE may release authorization, never test-worker ownership.
|
|
4432
|
+
if (workerJob != IntPtr.Zero && !AssignProcessToJobObject(workerJob, created.hProcess))
|
|
4433
|
+
throw TestWorkerError("Assign Studio to test worker job failed");
|
|
4319
4434
|
if (!AssignProcessToJobObject(jobHandle, created.hProcess))
|
|
4320
4435
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "AssignProcessToJobObject failed");
|
|
4321
4436
|
return new McpSuspendedStudio(created, jobHandle);
|
|
@@ -4339,6 +4454,10 @@ public sealed class McpSuspendedStudio : IDisposable
|
|
|
4339
4454
|
CloseHandle(jobHandle);
|
|
4340
4455
|
throw;
|
|
4341
4456
|
}
|
|
4457
|
+
finally
|
|
4458
|
+
{
|
|
4459
|
+
if (workerJob != IntPtr.Zero) CloseHandle(workerJob);
|
|
4460
|
+
}
|
|
4342
4461
|
}
|
|
4343
4462
|
|
|
4344
4463
|
public void Resume()
|
|
@@ -4388,7 +4507,7 @@ public sealed class McpSuspendedStudio : IDisposable
|
|
|
4388
4507
|
}
|
|
4389
4508
|
}
|
|
4390
4509
|
'@`,
|
|
4391
|
-
`$launch = [McpSuspendedStudio]::Start(${powershellStringLiteral(windowsExe)}, ${powershellStringLiteral(commandLine)}, ${windowsWorkingDirectory === void 0 ? "$null" : powershellStringLiteral(windowsWorkingDirectory)})`,
|
|
4510
|
+
`$launch = [McpSuspendedStudio]::Start(${powershellStringLiteral(windowsExe)}, ${powershellStringLiteral(commandLine)}, ${windowsWorkingDirectory === void 0 ? "$null" : powershellStringLiteral(windowsWorkingDirectory)}, ${testWorkerJobName === void 0 ? "$null" : powershellStringLiteral(testWorkerJobName)})`,
|
|
4392
4511
|
"try {",
|
|
4393
4512
|
"[Console]::Out.WriteLine((ConvertTo-Json @{ pid = $launch.ProcessId; started = [string]$launch.StartedAtFileTime } -Compress)); [Console]::Out.Flush()",
|
|
4394
4513
|
"$accepted = $false",
|
|
@@ -4427,13 +4546,14 @@ function buildWindowsStudioStopScript(processId, startedAt) {
|
|
|
4427
4546
|
"} finally { if ($null -ne $studio) { $studio.Dispose() } }"
|
|
4428
4547
|
].join("; ");
|
|
4429
4548
|
}
|
|
4430
|
-
async function stopWindowsStudio(processId, startedAt) {
|
|
4431
|
-
await powershellAsync(buildWindowsStudioStopScript(processId, startedAt));
|
|
4549
|
+
async function stopWindowsStudio(processId, startedAt, timeoutMs = 15e3) {
|
|
4550
|
+
await powershellAsync(buildWindowsStudioStopScript(processId, startedAt), timeoutMs);
|
|
4432
4551
|
}
|
|
4433
4552
|
async function spawnWindowsStudio(exe, args, processEnvironment, studioWorkingDirectory) {
|
|
4434
4553
|
const windowsExe = await toStudioLaunchArgAsync(exe);
|
|
4435
4554
|
const windowsWorkingDirectory = studioWorkingDirectory ? await toStudioLaunchArgAsync(studioWorkingDirectory) : void 0;
|
|
4436
|
-
const
|
|
4555
|
+
const testWorkerJobName = process.env.RSMCP_STUDIO_TEST_WORKER_JOB;
|
|
4556
|
+
const script = buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment, windowsWorkingDirectory, testWorkerJobName);
|
|
4437
4557
|
const launcher = spawn("powershell.exe", ["-NoProfile", "-Command", script], {
|
|
4438
4558
|
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
|
|
4439
4559
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -4740,6 +4860,24 @@ function prepareStudioLaunchOptions(options) {
|
|
|
4740
4860
|
localPlaceFile: createBaseplatePlaceFile()
|
|
4741
4861
|
};
|
|
4742
4862
|
}
|
|
4863
|
+
function resolveStudioExeFromVersions(root) {
|
|
4864
|
+
if (!existsSync2(root)) {
|
|
4865
|
+
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4866
|
+
}
|
|
4867
|
+
const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
4868
|
+
if (candidates.length === 0) {
|
|
4869
|
+
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4870
|
+
}
|
|
4871
|
+
const exe = candidates[0];
|
|
4872
|
+
const installationDir = path2.dirname(exe);
|
|
4873
|
+
const hasIncompleteDownload = readdirSync(installationDir).some((name) => /\.crdownload$/i.test(name));
|
|
4874
|
+
const settings = statSync(path2.join(installationDir, "AppSettings.xml"), { throwIfNoEntry: false });
|
|
4875
|
+
if (hasIncompleteDownload || !settings?.isFile() || settings.size === 0) {
|
|
4876
|
+
const reason = hasIncompleteDownload ? "an unfinished .crdownload download is present" : "AppSettings.xml is missing, empty, or not a regular file";
|
|
4877
|
+
throw new Error(`Roblox Studio installation/update is incomplete at ${installationDir}: ${reason}. Complete the installation/update or reinstall Roblox Studio under the owning Windows account before launching.`);
|
|
4878
|
+
}
|
|
4879
|
+
return exe;
|
|
4880
|
+
}
|
|
4743
4881
|
function resolveStudioExe() {
|
|
4744
4882
|
if (process.env.ROBLOX_STUDIO_EXE)
|
|
4745
4883
|
return process.env.ROBLOX_STUDIO_EXE;
|
|
@@ -4751,14 +4889,7 @@ function resolveStudioExe() {
|
|
|
4751
4889
|
}
|
|
4752
4890
|
const localAppData = windowsLocalAppData();
|
|
4753
4891
|
const root = localAppData ? path2.join(toWslPath(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
|
|
4754
|
-
|
|
4755
|
-
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4756
|
-
}
|
|
4757
|
-
const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
4758
|
-
if (candidates.length === 0) {
|
|
4759
|
-
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4760
|
-
}
|
|
4761
|
-
return candidates[0];
|
|
4892
|
+
return resolveStudioExeFromVersions(root);
|
|
4762
4893
|
}
|
|
4763
4894
|
async function resolveStudioExeAsync() {
|
|
4764
4895
|
if (process.env.ROBLOX_STUDIO_EXE)
|
|
@@ -4771,14 +4902,7 @@ async function resolveStudioExeAsync() {
|
|
|
4771
4902
|
}
|
|
4772
4903
|
const localAppData = await windowsLocalAppDataAsync();
|
|
4773
4904
|
const root = localAppData ? path2.join(await toWslPathAsync(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
|
|
4774
|
-
|
|
4775
|
-
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4776
|
-
}
|
|
4777
|
-
const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
4778
|
-
if (candidates.length === 0) {
|
|
4779
|
-
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4780
|
-
}
|
|
4781
|
-
return candidates[0];
|
|
4905
|
+
return resolveStudioExeFromVersions(root);
|
|
4782
4906
|
}
|
|
4783
4907
|
var WINDOWS_STUDIO_PROCESS_QUERY = [
|
|
4784
4908
|
"$ErrorActionPreference = 'Stop'",
|
|
@@ -4795,13 +4919,13 @@ function parseWindowsStudioProcesses(output) {
|
|
|
4795
4919
|
}
|
|
4796
4920
|
return processes;
|
|
4797
4921
|
}
|
|
4798
|
-
async function observeStudioProcesses() {
|
|
4922
|
+
async function observeStudioProcesses(timeoutMs = 15e3) {
|
|
4799
4923
|
const observedAt = Date.now();
|
|
4800
4924
|
try {
|
|
4801
4925
|
if (process.platform === "darwin") {
|
|
4802
4926
|
let out2 = "";
|
|
4803
4927
|
try {
|
|
4804
|
-
out2 = await runAsync("pgrep", ["-fl", "RobloxStudio"]);
|
|
4928
|
+
out2 = await runAsync("pgrep", ["-fl", "RobloxStudio"], { timeout: timeoutMs });
|
|
4805
4929
|
} catch (error) {
|
|
4806
4930
|
const code = error.code;
|
|
4807
4931
|
if (Number(code) === 1)
|
|
@@ -4817,7 +4941,7 @@ async function observeStudioProcesses() {
|
|
|
4817
4941
|
if (process.platform !== "win32" && !isWsl()) {
|
|
4818
4942
|
return { status: "ok", observedAt, processes: [] };
|
|
4819
4943
|
}
|
|
4820
|
-
const out = await powershellAsync(WINDOWS_STUDIO_PROCESS_QUERY);
|
|
4944
|
+
const out = await powershellAsync(WINDOWS_STUDIO_PROCESS_QUERY, timeoutMs);
|
|
4821
4945
|
return { status: "ok", observedAt, processes: parseWindowsStudioProcesses(out) };
|
|
4822
4946
|
} catch (error) {
|
|
4823
4947
|
return {
|
|
@@ -4901,6 +5025,7 @@ var StudioInstanceManager = class {
|
|
|
4901
5025
|
confirmedExitGraceMs;
|
|
4902
5026
|
snapshotCacheMs;
|
|
4903
5027
|
launchCompletionTimeoutMs;
|
|
5028
|
+
closeTimeoutMs;
|
|
4904
5029
|
coordinatorTimer;
|
|
4905
5030
|
coordinatorRefresh;
|
|
4906
5031
|
cachedSnapshot;
|
|
@@ -4915,6 +5040,7 @@ var StudioInstanceManager = class {
|
|
|
4915
5040
|
this.confirmedExitGraceMs = options.confirmedExitGraceMs ?? 5e3;
|
|
4916
5041
|
this.snapshotCacheMs = options.snapshotCacheMs ?? 0;
|
|
4917
5042
|
this.launchCompletionTimeoutMs = options.launchCompletionTimeoutMs ?? LAUNCH_COMPLETION_TIMEOUT_MS;
|
|
5043
|
+
this.closeTimeoutMs = options.closeTimeoutMs ?? 1e4;
|
|
4918
5044
|
}
|
|
4919
5045
|
getLifecycleCapabilities() {
|
|
4920
5046
|
const platform = this.platformCapabilities ?? getStudioPlatformCapabilities();
|
|
@@ -5312,58 +5438,57 @@ var StudioInstanceManager = class {
|
|
|
5312
5438
|
instanceId: record.instanceId
|
|
5313
5439
|
};
|
|
5314
5440
|
}
|
|
5441
|
+
const deadline = Date.now() + this.closeTimeoutMs;
|
|
5315
5442
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
5316
5443
|
if (!processId) {
|
|
5317
5444
|
throw new Error(`Cannot close ${record.instanceId ?? "Studio launch"} because its process id was not detected.`);
|
|
5318
5445
|
}
|
|
5319
5446
|
const control = record.recordId ? this.launchControls.get(record.recordId) : void 0;
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
this.
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
source: record.source,
|
|
5353
|
-
reason: observation.reason === "identity_mismatch" ? "identity_mismatch" : "pid_not_running",
|
|
5354
|
-
action: "marked_closed_and_cleaned_baseplate"
|
|
5355
|
-
});
|
|
5356
|
-
return {
|
|
5357
|
-
status: "already_closed",
|
|
5358
|
-
launchId: record.recordId,
|
|
5359
|
-
instanceId: record.instanceId
|
|
5360
|
-
};
|
|
5447
|
+
const abort = record.processAuthorizationState !== "released" && control ? () => control.abort() : void 0;
|
|
5448
|
+
if (!abort) {
|
|
5449
|
+
let snapshot;
|
|
5450
|
+
try {
|
|
5451
|
+
snapshot = await beforeCloseDeadline(() => this.readProcessSnapshot(deadline - Date.now()), deadline, processId);
|
|
5452
|
+
} catch (error) {
|
|
5453
|
+
if (error instanceof StudioCloseVerificationError) {
|
|
5454
|
+
await this.applyProcessObservation(record, error.observation);
|
|
5455
|
+
}
|
|
5456
|
+
throw error;
|
|
5457
|
+
}
|
|
5458
|
+
const observation = this.observeRecord(record, snapshot);
|
|
5459
|
+
if (observation.status === "unknown") {
|
|
5460
|
+
await this.applyProcessObservation(record, observation);
|
|
5461
|
+
throw new Error(`Cannot verify the managed Studio process because process observation failed: ${observation.error}`);
|
|
5462
|
+
}
|
|
5463
|
+
if (observation.status === "not_running") {
|
|
5464
|
+
await this.markProcessExited(record, void 0, observation.reason === "identity_mismatch" ? "Studio process identity changed; the retained PID was not reused." : record.failureReason);
|
|
5465
|
+
await this.registry.logEvent({
|
|
5466
|
+
event: "registry_close_already_stopped",
|
|
5467
|
+
recordId: record.recordId,
|
|
5468
|
+
instanceId: record.instanceId,
|
|
5469
|
+
source: record.source,
|
|
5470
|
+
reason: observation.reason === "identity_mismatch" ? "identity_mismatch" : "pid_not_running",
|
|
5471
|
+
action: "marked_closed_and_cleaned_baseplate"
|
|
5472
|
+
});
|
|
5473
|
+
return {
|
|
5474
|
+
status: "already_closed",
|
|
5475
|
+
launchId: record.recordId,
|
|
5476
|
+
instanceId: record.instanceId
|
|
5477
|
+
};
|
|
5478
|
+
}
|
|
5361
5479
|
}
|
|
5362
5480
|
try {
|
|
5363
|
-
await this.closeProcess(processId, record.nativeProcessStartedAt);
|
|
5481
|
+
await this.closeProcess(processId, record.nativeProcessStartedAt, abort, deadline);
|
|
5364
5482
|
} catch (error) {
|
|
5365
|
-
|
|
5483
|
+
if (error instanceof StudioCloseVerificationError) {
|
|
5484
|
+
await this.applyProcessObservation(record, error.observation);
|
|
5485
|
+
throw error;
|
|
5486
|
+
}
|
|
5487
|
+
if (abort)
|
|
5488
|
+
throw error;
|
|
5489
|
+
const retry = await beforeCloseDeadline(() => this.readProcessSnapshot(deadline - Date.now()), deadline, processId);
|
|
5366
5490
|
const retryObservation = this.observeRecord(record, retry);
|
|
5491
|
+
await this.applyProcessObservation(record, retryObservation);
|
|
5367
5492
|
if (retryObservation.status === "running" || retryObservation.status === "unknown")
|
|
5368
5493
|
throw error;
|
|
5369
5494
|
await this.registry.logEvent({
|
|
@@ -5400,7 +5525,8 @@ var StudioInstanceManager = class {
|
|
|
5400
5525
|
};
|
|
5401
5526
|
}
|
|
5402
5527
|
async closeConnectedInstance(instance) {
|
|
5403
|
-
const
|
|
5528
|
+
const deadline = Date.now() + this.closeTimeoutMs;
|
|
5529
|
+
const snapshot = await beforeCloseDeadline(() => this.readProcessSnapshot(deadline - Date.now()), deadline, 0);
|
|
5404
5530
|
if (snapshot.status === "error") {
|
|
5405
5531
|
throw new Error(`Could not enumerate Studio processes: ${snapshot.error}`);
|
|
5406
5532
|
}
|
|
@@ -5408,26 +5534,64 @@ var StudioInstanceManager = class {
|
|
|
5408
5534
|
if (!process2) {
|
|
5409
5535
|
throw new Error(`Could not find a Studio process for connected instance "${instance.instanceId}".`);
|
|
5410
5536
|
}
|
|
5411
|
-
await this.closeProcess(process2.Id, process2.StartTimeUtcFileTime);
|
|
5537
|
+
await this.closeProcess(process2.Id, process2.StartTimeUtcFileTime, void 0, deadline);
|
|
5412
5538
|
}
|
|
5413
|
-
async closeProcess(processId, startedAt) {
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5539
|
+
async closeProcess(processId, startedAt, stop, deadline = Date.now() + this.closeTimeoutMs) {
|
|
5540
|
+
this.cachedSnapshot = void 0;
|
|
5541
|
+
await beforeCloseDeadline(async () => {
|
|
5542
|
+
if (stop) {
|
|
5543
|
+
await stop();
|
|
5544
|
+
} else if (this.processAdapter.stopProcess) {
|
|
5545
|
+
await this.processAdapter.stopProcess(processId, startedAt);
|
|
5546
|
+
} else if (process.platform === "win32" || isWsl()) {
|
|
5547
|
+
if (startedAt === void 0) {
|
|
5548
|
+
throw new Error("Cannot stop a Windows Studio process without its creation-time identity.");
|
|
5549
|
+
}
|
|
5550
|
+
await stopWindowsStudio(processId, startedAt, Math.max(1, deadline - Date.now()));
|
|
5551
|
+
} else {
|
|
5552
|
+
try {
|
|
5553
|
+
process.kill(processId, "SIGTERM");
|
|
5554
|
+
} catch (error) {
|
|
5555
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH")
|
|
5556
|
+
throw error;
|
|
5557
|
+
}
|
|
5421
5558
|
}
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5559
|
+
}, deadline, processId);
|
|
5560
|
+
while (true) {
|
|
5561
|
+
const observation = await beforeCloseDeadline(() => this.observeClosingProcess(processId, startedAt, deadline), deadline, processId);
|
|
5562
|
+
if (observation.status === "not_running")
|
|
5563
|
+
return;
|
|
5564
|
+
if (observation.status === "unknown") {
|
|
5565
|
+
throw new StudioCloseVerificationError(`Could not verify Studio process ${processId} termination: ${observation.error}`, observation);
|
|
5566
|
+
}
|
|
5567
|
+
const remaining = deadline - Date.now();
|
|
5568
|
+
if (remaining <= 0) {
|
|
5569
|
+
throw new StudioCloseVerificationError(`Timed out closing Studio process ${processId}: the process is still running. Close can be retried.`, observation);
|
|
5570
|
+
}
|
|
5571
|
+
await delay2(Math.min(100, remaining));
|
|
5572
|
+
if (Date.now() >= deadline) {
|
|
5573
|
+
throw new StudioCloseVerificationError(`Timed out closing Studio process ${processId}: the process is still running. Close can be retried.`, observation);
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5576
|
+
}
|
|
5577
|
+
async observeClosingProcess(processId, startedAt, deadline) {
|
|
5578
|
+
if (process.platform === "darwin" && !this.processAdapter.observeStudioProcesses && !this.processAdapter.listStudioProcesses) {
|
|
5579
|
+
return observePosixProcess(processId);
|
|
5580
|
+
}
|
|
5581
|
+
const snapshot = await this.readProcessSnapshot(Math.max(1, deadline - Date.now()));
|
|
5582
|
+
if (snapshot.status === "error") {
|
|
5583
|
+
return { status: "unknown", observedAt: snapshot.observedAt, error: snapshot.error };
|
|
5584
|
+
}
|
|
5585
|
+
const target = snapshot.processes.find((candidate) => candidate.Id === processId);
|
|
5586
|
+
if (!target)
|
|
5587
|
+
return { status: "not_running", observedAt: snapshot.observedAt, reason: "missing" };
|
|
5588
|
+
if (startedAt !== void 0 && target.StartTimeUtcFileTime !== startedAt) {
|
|
5589
|
+
if (target.StartTimeUtcFileTime === void 0) {
|
|
5590
|
+
return { status: "unknown", observedAt: snapshot.observedAt, error: "Process creation-time identity is unavailable." };
|
|
5429
5591
|
}
|
|
5592
|
+
return { status: "not_running", observedAt: snapshot.observedAt, reason: "identity_mismatch" };
|
|
5430
5593
|
}
|
|
5594
|
+
return { status: "running", observedAt: snapshot.observedAt };
|
|
5431
5595
|
}
|
|
5432
5596
|
findProcessForConnectedInstance(instance, processes) {
|
|
5433
5597
|
if (processes.length === 0)
|
|
@@ -5457,24 +5621,7 @@ var StudioInstanceManager = class {
|
|
|
5457
5621
|
return this.snapshotInFlight;
|
|
5458
5622
|
this.snapshotInFlight = (async () => {
|
|
5459
5623
|
try {
|
|
5460
|
-
|
|
5461
|
-
if (this.processAdapter.observeStudioProcesses) {
|
|
5462
|
-
snapshot = await this.processAdapter.observeStudioProcesses();
|
|
5463
|
-
} else if (this.processAdapter.listStudioProcesses) {
|
|
5464
|
-
const observedAt = Date.now();
|
|
5465
|
-
const processes = await this.processAdapter.listStudioProcesses();
|
|
5466
|
-
snapshot = { status: "ok", observedAt, processes };
|
|
5467
|
-
} else {
|
|
5468
|
-
snapshot = await observeStudioProcesses();
|
|
5469
|
-
}
|
|
5470
|
-
this.cachedSnapshot = snapshot;
|
|
5471
|
-
return snapshot;
|
|
5472
|
-
} catch (error) {
|
|
5473
|
-
const snapshot = {
|
|
5474
|
-
status: "error",
|
|
5475
|
-
observedAt: Date.now(),
|
|
5476
|
-
error: error instanceof Error ? error.message : String(error)
|
|
5477
|
-
};
|
|
5624
|
+
const snapshot = await this.readProcessSnapshot();
|
|
5478
5625
|
this.cachedSnapshot = snapshot;
|
|
5479
5626
|
return snapshot;
|
|
5480
5627
|
} finally {
|
|
@@ -5483,6 +5630,25 @@ var StudioInstanceManager = class {
|
|
|
5483
5630
|
})();
|
|
5484
5631
|
return this.snapshotInFlight;
|
|
5485
5632
|
}
|
|
5633
|
+
async readProcessSnapshot(timeoutMs = 15e3) {
|
|
5634
|
+
try {
|
|
5635
|
+
if (this.processAdapter.observeStudioProcesses) {
|
|
5636
|
+
return await this.processAdapter.observeStudioProcesses();
|
|
5637
|
+
}
|
|
5638
|
+
if (this.processAdapter.listStudioProcesses) {
|
|
5639
|
+
const observedAt = Date.now();
|
|
5640
|
+
const processes = await this.processAdapter.listStudioProcesses();
|
|
5641
|
+
return { status: "ok", observedAt, processes };
|
|
5642
|
+
}
|
|
5643
|
+
return await observeStudioProcesses(Math.max(1, timeoutMs));
|
|
5644
|
+
} catch (error) {
|
|
5645
|
+
return {
|
|
5646
|
+
status: "error",
|
|
5647
|
+
observedAt: Date.now(),
|
|
5648
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5649
|
+
};
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5486
5652
|
async getCurrentBootId() {
|
|
5487
5653
|
return this.processAdapter.currentBootId ? await this.processAdapter.currentBootId() : currentBootIdAsync();
|
|
5488
5654
|
}
|
|
@@ -5548,8 +5714,16 @@ var StudioInstanceManager = class {
|
|
|
5548
5714
|
if (!processId)
|
|
5549
5715
|
return { status: "running", observedAt: snapshot.observedAt };
|
|
5550
5716
|
const studioProcess = snapshot.processes.find((candidate) => candidate.Id === processId);
|
|
5551
|
-
if (!studioProcess)
|
|
5717
|
+
if (!studioProcess) {
|
|
5718
|
+
if (process.platform === "darwin" && !this.processAdapter.observeStudioProcesses && !this.processAdapter.listStudioProcesses) {
|
|
5719
|
+
const observation = observePosixProcess(processId);
|
|
5720
|
+
return observation.status === "running" ? { status: "unknown", observedAt: observation.observedAt, error: `Studio PID ${processId} is still alive but missing from process enumeration.` } : observation;
|
|
5721
|
+
}
|
|
5552
5722
|
return { status: "not_running", observedAt: snapshot.observedAt, reason: "missing" };
|
|
5723
|
+
}
|
|
5724
|
+
if (record.nativeProcessStartedAt !== void 0 && studioProcess.StartTimeUtcFileTime === void 0) {
|
|
5725
|
+
return { status: "unknown", observedAt: snapshot.observedAt, error: "Process creation-time identity is unavailable." };
|
|
5726
|
+
}
|
|
5553
5727
|
return this.verifyProcessForRecord(record, studioProcess) ? { status: "running", observedAt: snapshot.observedAt } : { status: "not_running", observedAt: snapshot.observedAt, reason: "identity_mismatch" };
|
|
5554
5728
|
}
|
|
5555
5729
|
verifyProcessForRecord(record, studioProcess) {
|
|
@@ -5838,20 +6012,20 @@ var StudioInstanceManager = class {
|
|
|
5838
6012
|
};
|
|
5839
6013
|
|
|
5840
6014
|
// ../core/dist/mcp-runtime.js
|
|
5841
|
-
var
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
6015
|
+
var INTERNAL_ENVELOPE_KEYS = {
|
|
6016
|
+
bundleModifiedAt: true,
|
|
6017
|
+
bundlePath: true,
|
|
6018
|
+
bundleSha256: true,
|
|
6019
|
+
connectedAt: true,
|
|
6020
|
+
debug: true,
|
|
6021
|
+
diagnostics: true,
|
|
6022
|
+
internal: true,
|
|
6023
|
+
lastActivity: true,
|
|
6024
|
+
transportPeerId: true,
|
|
6025
|
+
pluginVariant: true,
|
|
6026
|
+
pluginVersion: true,
|
|
6027
|
+
serverVersion: true
|
|
6028
|
+
};
|
|
5855
6029
|
var TEXT_RESULT_TOOLS = /* @__PURE__ */ new Set(["get_roblox_docs"]);
|
|
5856
6030
|
var SIDE_EFFECTING_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
5857
6031
|
"capture_micro_profiler",
|
|
@@ -5900,18 +6074,15 @@ function compactPublicValue(value) {
|
|
|
5900
6074
|
return value.map(compactPublicValue);
|
|
5901
6075
|
if (!value || typeof value !== "object")
|
|
5902
6076
|
return value;
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
compact[key] = compactPublicValue(child);
|
|
5908
|
-
}
|
|
5909
|
-
return compact;
|
|
6077
|
+
return Object.fromEntries(Object.entries(value).filter(([, child]) => child !== void 0).map(([key, child]) => [key, compactPublicValue(child)]));
|
|
6078
|
+
}
|
|
6079
|
+
function compactPublicEnvelope(value) {
|
|
6080
|
+
return Object.fromEntries(Object.entries(value).filter(([key, child]) => child !== void 0 && !Object.hasOwn(INTERNAL_ENVELOPE_KEYS, key)).map(([key, child]) => [key, compactPublicValue(child)]));
|
|
5910
6081
|
}
|
|
5911
6082
|
function asStructuredObject(value) {
|
|
5912
6083
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5913
6084
|
return void 0;
|
|
5914
|
-
return
|
|
6085
|
+
return compactPublicEnvelope(value);
|
|
5915
6086
|
}
|
|
5916
6087
|
function parseJsonObject(text) {
|
|
5917
6088
|
try {
|
|
@@ -5936,6 +6107,7 @@ function normalizeToolResult(raw, era) {
|
|
|
5936
6107
|
...result.isError ? { isError: true } : {}
|
|
5937
6108
|
};
|
|
5938
6109
|
}
|
|
6110
|
+
const isError = result.isError || structured.success === false || typeof structured.error === "string" && structured.error.length > 0;
|
|
5939
6111
|
const content = era === "modern" ? originalContent.filter((_, index) => index !== jsonTextIndex) : [
|
|
5940
6112
|
{ type: "text", text: JSON.stringify(structured) },
|
|
5941
6113
|
...originalContent.filter((_, index) => index !== jsonTextIndex)
|
|
@@ -5943,7 +6115,7 @@ function normalizeToolResult(raw, era) {
|
|
|
5943
6115
|
return {
|
|
5944
6116
|
content,
|
|
5945
6117
|
structuredContent: structured,
|
|
5946
|
-
...
|
|
6118
|
+
...isError ? { isError: true } : {}
|
|
5947
6119
|
};
|
|
5948
6120
|
}
|
|
5949
6121
|
function publicRoutingError(error) {
|
|
@@ -5966,8 +6138,11 @@ function publicRoutingError(error) {
|
|
|
5966
6138
|
};
|
|
5967
6139
|
}
|
|
5968
6140
|
function publicToolErrorBody(name, error) {
|
|
6141
|
+
if (error instanceof MultiplayerGroupInUseError) {
|
|
6142
|
+
return { error: error.code, message: error.message, multiplayer_group_id: error.groupId };
|
|
6143
|
+
}
|
|
5969
6144
|
if (error instanceof StudioLaunchPreDispatchError) {
|
|
5970
|
-
return
|
|
6145
|
+
return compactPublicEnvelope(error.toResponseBody());
|
|
5971
6146
|
}
|
|
5972
6147
|
if (error instanceof RoutingFailure)
|
|
5973
6148
|
return publicRoutingError(error);
|
|
@@ -6441,6 +6616,47 @@ var WebSocketStudioTransport = class {
|
|
|
6441
6616
|
};
|
|
6442
6617
|
|
|
6443
6618
|
// ../core/dist/http-server.js
|
|
6619
|
+
var TOOL_PROXY_ENDPOINTS = {
|
|
6620
|
+
get_place_info: ["/api/place-info"],
|
|
6621
|
+
search_objects: ["/api/search-objects"],
|
|
6622
|
+
get_instance_properties: ["/api/instance-properties"],
|
|
6623
|
+
get_project_structure: ["/api/project-structure"],
|
|
6624
|
+
set_properties: ["/api/set-properties"],
|
|
6625
|
+
get_script_source: ["/api/get-script-source"],
|
|
6626
|
+
set_script_source: ["/api/set-script-source"],
|
|
6627
|
+
edit_script_lines: ["/api/edit-script-lines"],
|
|
6628
|
+
insert_script_lines: ["/api/insert-script-lines"],
|
|
6629
|
+
delete_script_lines: ["/api/delete-script-lines"],
|
|
6630
|
+
get_attributes: ["/api/get-attributes"],
|
|
6631
|
+
selection: ["/api/get-selection", "/api/set-selection", "/api/focus-viewport"],
|
|
6632
|
+
execute_luau: ["/api/execute-luau"],
|
|
6633
|
+
eval_server_runtime: ["/api/eval-runtime"],
|
|
6634
|
+
eval_client_runtime: ["/api/eval-runtime"],
|
|
6635
|
+
grep_scripts: ["/api/grep-scripts"],
|
|
6636
|
+
solo_playtest: ["/api/start-playtest", "/api/stop-playtest", "/api/multiplayer-test-state"],
|
|
6637
|
+
multiplayer_playtest: [
|
|
6638
|
+
"/api/multiplayer-test-start",
|
|
6639
|
+
"/api/multiplayer-test-add-players",
|
|
6640
|
+
"/api/multiplayer-test-leave-client",
|
|
6641
|
+
"/api/multiplayer-test-end",
|
|
6642
|
+
"/api/multiplayer-test-state"
|
|
6643
|
+
],
|
|
6644
|
+
get_runtime_logs: ["/api/get-runtime-logs"],
|
|
6645
|
+
capture_script_profiler: ["/api/capture-script-profiler"],
|
|
6646
|
+
capture_micro_profiler: ["/api/capture-micro-profiler"],
|
|
6647
|
+
breakpoints: ["/api/breakpoints"],
|
|
6648
|
+
insert_asset: ["/api/insert-asset"],
|
|
6649
|
+
generate_model: ["/api/generate-model"],
|
|
6650
|
+
preview_asset: ["/api/preview-asset"],
|
|
6651
|
+
capture_screenshot: ["/api/capture-studio", "/api/capture-screenshot", "/api/capture-begin", "/api/capture-read", "/api/capture-markers"],
|
|
6652
|
+
simulate_mouse_input: ["/api/simulate-mouse-input"],
|
|
6653
|
+
simulate_keyboard_input: ["/api/simulate-keyboard-input"],
|
|
6654
|
+
get_memory_breakdown: ["/api/get-memory-breakdown"],
|
|
6655
|
+
get_scene_analysis: ["/api/get-scene-analysis"],
|
|
6656
|
+
export_rbxm: ["/api/export-rbxm"],
|
|
6657
|
+
import_rbxm: ["/api/import-rbxm"],
|
|
6658
|
+
find_and_replace_in_scripts: ["/api/find-and-replace-in-scripts"]
|
|
6659
|
+
};
|
|
6444
6660
|
function toPassivePeer(peer) {
|
|
6445
6661
|
return {
|
|
6446
6662
|
instanceId: peer.instanceId,
|
|
@@ -6627,6 +6843,7 @@ ${body}`);
|
|
|
6627
6843
|
}
|
|
6628
6844
|
function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
6629
6845
|
const app = express();
|
|
6846
|
+
const allowedProxyEndpoints = allowedTools ? new Set(Object.entries(TOOL_PROXY_ENDPOINTS).filter(([toolName]) => allowedTools.has(toolName)).flatMap(([, endpoints]) => endpoints)) : void 0;
|
|
6630
6847
|
const studioLifecycleCallable = !allowedTools || allowedTools.has("manage_instance");
|
|
6631
6848
|
const studioLifecycleCapabilities = studioLifecycleCallable ? tools.getStudioLifecycleCapabilities() : void 0;
|
|
6632
6849
|
let mcpServerActive = false;
|
|
@@ -6773,8 +6990,8 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
6773
6990
|
});
|
|
6774
6991
|
const authToken = security?.authToken;
|
|
6775
6992
|
const authRequired = (requestPath) => {
|
|
6776
|
-
const
|
|
6777
|
-
return
|
|
6993
|
+
const path7 = requestPath.toLowerCase().replace(/\/+$/, "");
|
|
6994
|
+
return path7 === "/mcp" || path7.startsWith("/mcp/") || path7 === "/proxy" || path7 === "/topology" || path7 === "/request-status" || path7 === "/unregister-instance-id" || path7 === "/create-multiplayer-group" || path7 === "/remove-multiplayer-group";
|
|
6778
6995
|
};
|
|
6779
6996
|
app.use((req, res, next) => {
|
|
6780
6997
|
if (!authToken || !authRequired(req.path)) {
|
|
@@ -7034,14 +7251,27 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
7034
7251
|
const group = await bridge.createMultiplayerGroupEverywhere(groupId, controllerInstanceId);
|
|
7035
7252
|
res.json({ success: true, group });
|
|
7036
7253
|
});
|
|
7037
|
-
app.post("/remove-multiplayer-group", async (req, res) => {
|
|
7254
|
+
app.post("/remove-multiplayer-group", async (req, res, next) => {
|
|
7038
7255
|
const { groupId } = req.body;
|
|
7039
7256
|
if (typeof groupId !== "string" || groupId.length === 0) {
|
|
7040
7257
|
res.status(400).json({ error: "groupId is required" });
|
|
7041
7258
|
return;
|
|
7042
7259
|
}
|
|
7043
|
-
|
|
7044
|
-
|
|
7260
|
+
try {
|
|
7261
|
+
const removed = await bridge.removeMultiplayerGroupEverywhere(groupId);
|
|
7262
|
+
res.json({ success: true, removed });
|
|
7263
|
+
} catch (error) {
|
|
7264
|
+
if (error instanceof MultiplayerGroupInUseError) {
|
|
7265
|
+
res.status(409).json({
|
|
7266
|
+
success: false,
|
|
7267
|
+
error: error.code,
|
|
7268
|
+
message: error.message,
|
|
7269
|
+
groupId: error.groupId
|
|
7270
|
+
});
|
|
7271
|
+
return;
|
|
7272
|
+
}
|
|
7273
|
+
next(error);
|
|
7274
|
+
}
|
|
7045
7275
|
});
|
|
7046
7276
|
app.get("/status", (req, res) => {
|
|
7047
7277
|
const peers = bridge.getPublicPeers().map(toPassivePeer);
|
|
@@ -7088,6 +7318,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
7088
7318
|
res.status(400).json({ error: "endpoint and targetPeerId are required" });
|
|
7089
7319
|
return;
|
|
7090
7320
|
}
|
|
7321
|
+
if (allowedProxyEndpoints && !allowedProxyEndpoints.has(endpoint)) {
|
|
7322
|
+
res.status(403).json({ error: "forbidden_endpoint" });
|
|
7323
|
+
return;
|
|
7324
|
+
}
|
|
7091
7325
|
if (timeoutMs !== void 0 && (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3e5)) {
|
|
7092
7326
|
res.status(400).json({ error: "timeoutMs must be an integer between 1 and 300000" });
|
|
7093
7327
|
return;
|
|
@@ -7750,12 +7984,10 @@ var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
|
7750
7984
|
var MAX_EDITABLE_IMAGE_DIMENSION = 1024;
|
|
7751
7985
|
var MAX_PNG_DIMENSION = 16 * 1024;
|
|
7752
7986
|
var MAX_PNG_PIXELS = 8 * 1024 * 1024;
|
|
7987
|
+
var MAX_SCREENSHOT_PNG_PIXELS = 32 * 1024 * 1024;
|
|
7753
7988
|
var MAX_PNG_INPUT_BYTES = 32 * 1024 * 1024;
|
|
7754
7989
|
var MAX_PNG_BASE64_CHARACTERS = 4 * Math.ceil(MAX_PNG_INPUT_BYTES / 3);
|
|
7755
|
-
var MAX_PNG_CHUNK_BYTES = 32 * 1024 * 1024;
|
|
7756
|
-
var MAX_PNG_COMPRESSED_BYTES = 32 * 1024 * 1024;
|
|
7757
7990
|
var MAX_PNG_CHUNKS = 65536;
|
|
7758
|
-
var MAX_PNG_INFLATED_BYTES = MAX_PNG_PIXELS * 4 + MAX_PNG_DIMENSION;
|
|
7759
7991
|
function paethPredictor(a, b, c) {
|
|
7760
7992
|
const p = a + b - c;
|
|
7761
7993
|
const pa = Math.abs(p - a);
|
|
@@ -7778,12 +8010,12 @@ function bytesPerPixel(colorType) {
|
|
|
7778
8010
|
return 2;
|
|
7779
8011
|
throw new Error(`Unsupported PNG color type ${colorType}. Supported color types: 0, 2, 4, 6.`);
|
|
7780
8012
|
}
|
|
7781
|
-
function validatePngInputSize(byteLength) {
|
|
8013
|
+
function validatePngInputSize(byteLength, maxInputBytes = MAX_PNG_INPUT_BYTES) {
|
|
7782
8014
|
if (!Number.isSafeInteger(byteLength) || byteLength < 0) {
|
|
7783
8015
|
throw new Error(`Invalid PNG input length ${byteLength}.`);
|
|
7784
8016
|
}
|
|
7785
|
-
if (byteLength >
|
|
7786
|
-
throw new Error(`PNG input length ${byteLength} exceeds the ${
|
|
8017
|
+
if (byteLength > maxInputBytes) {
|
|
8018
|
+
throw new Error(`PNG input length ${byteLength} exceeds the ${maxInputBytes}-byte limit.`);
|
|
7787
8019
|
}
|
|
7788
8020
|
}
|
|
7789
8021
|
function getBase64DecodedLength(encoded) {
|
|
@@ -7845,7 +8077,7 @@ function readPngFileWithinLimit(resolved, imagePath) {
|
|
|
7845
8077
|
fs2.closeSync(descriptor);
|
|
7846
8078
|
}
|
|
7847
8079
|
}
|
|
7848
|
-
function validateSourceDimensions(width, height) {
|
|
8080
|
+
function validateSourceDimensions(width, height, maxPixels) {
|
|
7849
8081
|
if (width <= 0 || height <= 0) {
|
|
7850
8082
|
throw new Error("PNG IHDR dimensions must be positive.");
|
|
7851
8083
|
}
|
|
@@ -7853,78 +8085,65 @@ function validateSourceDimensions(width, height) {
|
|
|
7853
8085
|
throw new Error(`PNG dimensions ${width}x${height} exceed the ${MAX_PNG_DIMENSION}-pixel dimension limit.`);
|
|
7854
8086
|
}
|
|
7855
8087
|
const pixelCount = width * height;
|
|
7856
|
-
if (!Number.isSafeInteger(pixelCount) || pixelCount >
|
|
7857
|
-
throw new Error(`PNG pixel count ${pixelCount} exceeds the ${
|
|
8088
|
+
if (!Number.isSafeInteger(pixelCount) || pixelCount > maxPixels) {
|
|
8089
|
+
throw new Error(`PNG pixel count ${pixelCount} exceeds the ${maxPixels}-pixel limit.`);
|
|
7858
8090
|
}
|
|
7859
8091
|
}
|
|
7860
|
-
function getExpectedScanlineLength(width, height, bpp) {
|
|
8092
|
+
function getExpectedScanlineLength(width, height, bpp, maxPixels) {
|
|
8093
|
+
const maxInflatedBytes = maxPixels * 4 + MAX_PNG_DIMENSION;
|
|
7861
8094
|
const stride = width * bpp;
|
|
7862
8095
|
const expected = height * (stride + 1);
|
|
7863
|
-
if (!Number.isSafeInteger(stride) || !Number.isSafeInteger(expected) || expected >
|
|
7864
|
-
throw new Error(`PNG scanline data length ${expected} exceeds the ${
|
|
8096
|
+
if (!Number.isSafeInteger(stride) || !Number.isSafeInteger(expected) || expected > maxInflatedBytes) {
|
|
8097
|
+
throw new Error(`PNG scanline data length ${expected} exceeds the ${maxInflatedBytes}-byte limit.`);
|
|
7865
8098
|
}
|
|
7866
8099
|
return expected;
|
|
7867
8100
|
}
|
|
7868
|
-
function
|
|
7869
|
-
const
|
|
7870
|
-
if (
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
const upLeft = y > 0 && x >= bpp ? unfiltered[prevRowOffset + x - bpp] : 0;
|
|
7887
|
-
let decoded;
|
|
7888
|
-
if (filter === 0) {
|
|
7889
|
-
decoded = value;
|
|
7890
|
-
} else if (filter === 1) {
|
|
7891
|
-
decoded = value + left;
|
|
7892
|
-
} else if (filter === 2) {
|
|
7893
|
-
decoded = value + up;
|
|
7894
|
-
} else if (filter === 3) {
|
|
7895
|
-
decoded = value + Math.floor((left + up) / 2);
|
|
7896
|
-
} else if (filter === 4) {
|
|
7897
|
-
decoded = value + paethPredictor(left, up, upLeft);
|
|
7898
|
-
} else {
|
|
7899
|
-
throw new Error(`Unsupported PNG filter type ${filter}.`);
|
|
7900
|
-
}
|
|
7901
|
-
unfiltered[rowOffset + x] = decoded & 255;
|
|
8101
|
+
function unfilterPngRow(raw, rowOffset, stride, bpp) {
|
|
8102
|
+
const filter = raw[rowOffset - 1];
|
|
8103
|
+
if (filter === 0)
|
|
8104
|
+
return;
|
|
8105
|
+
const prevRowOffset = rowOffset - stride - 1;
|
|
8106
|
+
for (let x = 0; x < stride; x++) {
|
|
8107
|
+
const left = x >= bpp ? raw[rowOffset + x - bpp] : 0;
|
|
8108
|
+
const up = prevRowOffset > 0 ? raw[prevRowOffset + x] : 0;
|
|
8109
|
+
const upLeft = prevRowOffset > 0 && x >= bpp ? raw[prevRowOffset + x - bpp] : 0;
|
|
8110
|
+
let predictor;
|
|
8111
|
+
if (filter === 1) {
|
|
8112
|
+
predictor = left;
|
|
8113
|
+
} else if (filter === 2) {
|
|
8114
|
+
predictor = up;
|
|
8115
|
+
} else if (filter === 3) {
|
|
8116
|
+
predictor = Math.floor((left + up) / 2);
|
|
8117
|
+
} else {
|
|
8118
|
+
predictor = paethPredictor(left, up, upLeft);
|
|
7902
8119
|
}
|
|
8120
|
+
raw[rowOffset + x] = raw[rowOffset + x] + predictor & 255;
|
|
7903
8121
|
}
|
|
7904
|
-
|
|
7905
|
-
|
|
8122
|
+
}
|
|
8123
|
+
function convertScanlinesToRgba({ raw, width, height, colorType, bpp }) {
|
|
8124
|
+
const stride = width * bpp;
|
|
7906
8125
|
const rgba = Buffer.alloc(width * height * 4);
|
|
7907
|
-
let si = 0;
|
|
7908
8126
|
let di = 0;
|
|
7909
|
-
for (let
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
rgba
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
}
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
8127
|
+
for (let rowOffset = 1; rowOffset < raw.length; rowOffset += stride + 1) {
|
|
8128
|
+
unfilterPngRow(raw, rowOffset, stride, bpp);
|
|
8129
|
+
if (colorType === 6) {
|
|
8130
|
+
raw.copy(rgba, di, rowOffset, rowOffset + stride);
|
|
8131
|
+
di += stride;
|
|
8132
|
+
continue;
|
|
8133
|
+
}
|
|
8134
|
+
for (let si = rowOffset; si < rowOffset + stride; si += bpp) {
|
|
8135
|
+
if (colorType === 2) {
|
|
8136
|
+
rgba[di++] = raw[si];
|
|
8137
|
+
rgba[di++] = raw[si + 1];
|
|
8138
|
+
rgba[di++] = raw[si + 2];
|
|
8139
|
+
rgba[di++] = 255;
|
|
8140
|
+
} else {
|
|
8141
|
+
const gray = raw[si];
|
|
8142
|
+
rgba[di++] = gray;
|
|
8143
|
+
rgba[di++] = gray;
|
|
8144
|
+
rgba[di++] = gray;
|
|
8145
|
+
rgba[di++] = colorType === 4 ? raw[si + 1] : 255;
|
|
8146
|
+
}
|
|
7928
8147
|
}
|
|
7929
8148
|
}
|
|
7930
8149
|
return rgba;
|
|
@@ -7951,8 +8170,9 @@ function resizeRgbaNearest(image) {
|
|
|
7951
8170
|
}
|
|
7952
8171
|
return { width, height, rgba };
|
|
7953
8172
|
}
|
|
7954
|
-
function
|
|
7955
|
-
|
|
8173
|
+
function inflatePng(data, maxPixels) {
|
|
8174
|
+
const maxInputBytes = maxPixels * 4;
|
|
8175
|
+
validatePngInputSize(data.length, maxInputBytes);
|
|
7956
8176
|
if (data.length < PNG_SIGNATURE.length || !data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) {
|
|
7957
8177
|
throw new Error("Unsupported image format. generate_model currently supports PNG images.");
|
|
7958
8178
|
}
|
|
@@ -7973,8 +8193,8 @@ function decodePngToRgba(data) {
|
|
|
7973
8193
|
}
|
|
7974
8194
|
const length = data.readUInt32BE(offset);
|
|
7975
8195
|
const type = data.toString("ascii", offset + 4, offset + 8);
|
|
7976
|
-
if (length >
|
|
7977
|
-
throw new Error(`PNG chunk ${type} length ${length} exceeds the ${
|
|
8196
|
+
if (length > maxInputBytes) {
|
|
8197
|
+
throw new Error(`PNG chunk ${type} length ${length} exceeds the ${maxInputBytes}-byte chunk limit.`);
|
|
7978
8198
|
}
|
|
7979
8199
|
if (++chunkCount > MAX_PNG_CHUNKS) {
|
|
7980
8200
|
throw new Error(`PNG contains more than the ${MAX_PNG_CHUNKS}-chunk limit.`);
|
|
@@ -8010,12 +8230,12 @@ function decodePngToRgba(data) {
|
|
|
8010
8230
|
if (interlace !== 0)
|
|
8011
8231
|
throw new Error("Interlaced PNG images are not supported.");
|
|
8012
8232
|
bpp = bytesPerPixel(colorType);
|
|
8013
|
-
validateSourceDimensions(width, height);
|
|
8014
|
-
expectedScanlineLength = getExpectedScanlineLength(width, height, bpp);
|
|
8233
|
+
validateSourceDimensions(width, height, maxPixels);
|
|
8234
|
+
expectedScanlineLength = getExpectedScanlineLength(width, height, bpp, maxPixels);
|
|
8015
8235
|
sawIhdr = true;
|
|
8016
8236
|
} else if (type === "IDAT") {
|
|
8017
|
-
if (length >
|
|
8018
|
-
throw new Error(`PNG image data exceeds the ${
|
|
8237
|
+
if (length > maxInputBytes - idatLength) {
|
|
8238
|
+
throw new Error(`PNG image data exceeds the ${maxInputBytes}-byte compressed-data limit.`);
|
|
8019
8239
|
}
|
|
8020
8240
|
idatLength += length;
|
|
8021
8241
|
idatChunks.push(chunk);
|
|
@@ -8043,8 +8263,43 @@ function decodePngToRgba(data) {
|
|
|
8043
8263
|
const detail = error instanceof Error ? error.message : String(error);
|
|
8044
8264
|
throw new Error(`Invalid PNG compressed data: ${detail}`);
|
|
8045
8265
|
}
|
|
8046
|
-
|
|
8047
|
-
|
|
8266
|
+
if (raw.length !== expectedScanlineLength) {
|
|
8267
|
+
throw new Error(`PNG data ended early. Expected ${expectedScanlineLength} bytes after inflate, got ${raw.length}.`);
|
|
8268
|
+
}
|
|
8269
|
+
const rowLength = width * bpp + 1;
|
|
8270
|
+
for (let rowOffset = 0; rowOffset < raw.length; rowOffset += rowLength) {
|
|
8271
|
+
if (raw[rowOffset] > 4) {
|
|
8272
|
+
throw new Error(`Unsupported PNG filter type ${raw[rowOffset]}.`);
|
|
8273
|
+
}
|
|
8274
|
+
}
|
|
8275
|
+
return { width, height, colorType, bpp, raw };
|
|
8276
|
+
}
|
|
8277
|
+
function isUniformPng(data) {
|
|
8278
|
+
const { raw, width, colorType, bpp } = inflatePng(data, MAX_SCREENSHOT_PNG_PIXELS);
|
|
8279
|
+
const stride = width * bpp;
|
|
8280
|
+
unfilterPngRow(raw, 1, stride, bpp);
|
|
8281
|
+
const red = raw[1];
|
|
8282
|
+
const grayscale = colorType === 0 || colorType === 4;
|
|
8283
|
+
const green = grayscale ? red : raw[2];
|
|
8284
|
+
const blue = grayscale ? red : raw[3];
|
|
8285
|
+
for (let rowOffset = 1; rowOffset < raw.length; rowOffset += stride + 1) {
|
|
8286
|
+
if (rowOffset !== 1)
|
|
8287
|
+
unfilterPngRow(raw, rowOffset, stride, bpp);
|
|
8288
|
+
for (let offset = rowOffset; offset < rowOffset + stride; offset += bpp) {
|
|
8289
|
+
if (raw[offset] !== red || !grayscale && (raw[offset + 1] !== green || raw[offset + 2] !== blue)) {
|
|
8290
|
+
return false;
|
|
8291
|
+
}
|
|
8292
|
+
}
|
|
8293
|
+
}
|
|
8294
|
+
return true;
|
|
8295
|
+
}
|
|
8296
|
+
function decodePngToRgba(data) {
|
|
8297
|
+
const png = inflatePng(data, MAX_PNG_PIXELS);
|
|
8298
|
+
return resizeRgbaNearest({
|
|
8299
|
+
width: png.width,
|
|
8300
|
+
height: png.height,
|
|
8301
|
+
rgba: convertScanlinesToRgba(png)
|
|
8302
|
+
});
|
|
8048
8303
|
}
|
|
8049
8304
|
function decodePngBase64ToRgba(encoded) {
|
|
8050
8305
|
const expectedLength = getBase64DecodedLength(encoded);
|
|
@@ -10028,9 +10283,308 @@ function rgbaToPng(rgba, width, height) {
|
|
|
10028
10283
|
]);
|
|
10029
10284
|
}
|
|
10030
10285
|
|
|
10031
|
-
// ../core/dist/
|
|
10286
|
+
// ../core/dist/host-capture.js
|
|
10287
|
+
import { spawn as spawn2 } from "child_process";
|
|
10032
10288
|
import * as fs3 from "fs";
|
|
10289
|
+
import * as os3 from "os";
|
|
10033
10290
|
import * as path5 from "path";
|
|
10291
|
+
var HOST_CAPTURE_TIMEOUT_MS = 2e4;
|
|
10292
|
+
var MARKER_MIN_RB = 200;
|
|
10293
|
+
var MARKER_MAX_G = 60;
|
|
10294
|
+
var MARKER_SCALE_TOLERANCE = 0.03;
|
|
10295
|
+
function isHostCaptureDisabled(env = process.env) {
|
|
10296
|
+
const value = (env.ROBLOX_STUDIO_HOST_CAPTURE ?? "").trim().toLowerCase();
|
|
10297
|
+
return value === "0" || value === "false" || value === "off";
|
|
10298
|
+
}
|
|
10299
|
+
function isHostCaptureSupported(platform = process.platform) {
|
|
10300
|
+
return platform === "win32";
|
|
10301
|
+
}
|
|
10302
|
+
function hostCaptureUnsupportedReason(platform = process.platform) {
|
|
10303
|
+
return `host window capture is only implemented on Windows (this is ${platform})`;
|
|
10304
|
+
}
|
|
10305
|
+
function isUniformFrame(rgba, width, height) {
|
|
10306
|
+
const pixels = width * height;
|
|
10307
|
+
if (pixels <= 0 || rgba.length < pixels * 4)
|
|
10308
|
+
return false;
|
|
10309
|
+
const r = rgba[0];
|
|
10310
|
+
const g = rgba[1];
|
|
10311
|
+
const b = rgba[2];
|
|
10312
|
+
for (let i = 0; i < pixels; i++) {
|
|
10313
|
+
const o = i * 4;
|
|
10314
|
+
if (rgba[o] !== r || rgba[o + 1] !== g || rgba[o + 2] !== b)
|
|
10315
|
+
return false;
|
|
10316
|
+
}
|
|
10317
|
+
return true;
|
|
10318
|
+
}
|
|
10319
|
+
function isMarkerPixel(rgba, offset) {
|
|
10320
|
+
return rgba[offset] >= MARKER_MIN_RB && rgba[offset + 1] <= MARKER_MAX_G && rgba[offset + 2] >= MARKER_MIN_RB;
|
|
10321
|
+
}
|
|
10322
|
+
function findViewportRect(rgba, width, height, hint) {
|
|
10323
|
+
let minX = width;
|
|
10324
|
+
let minY = height;
|
|
10325
|
+
let maxX = -1;
|
|
10326
|
+
let maxY = -1;
|
|
10327
|
+
for (let y = 0; y < height; y++) {
|
|
10328
|
+
const row = y * width * 4;
|
|
10329
|
+
for (let x = 0; x < width; x++) {
|
|
10330
|
+
if (!isMarkerPixel(rgba, row + x * 4))
|
|
10331
|
+
continue;
|
|
10332
|
+
if (x < minX)
|
|
10333
|
+
minX = x;
|
|
10334
|
+
if (x > maxX)
|
|
10335
|
+
maxX = x;
|
|
10336
|
+
if (y < minY)
|
|
10337
|
+
minY = y;
|
|
10338
|
+
if (y > maxY)
|
|
10339
|
+
maxY = y;
|
|
10340
|
+
}
|
|
10341
|
+
}
|
|
10342
|
+
if (maxX < 0) {
|
|
10343
|
+
return { error: "no viewport markers were visible in the Studio window capture" };
|
|
10344
|
+
}
|
|
10345
|
+
const rect = { x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1 };
|
|
10346
|
+
const inset = Math.max(1, Math.floor(hint.markerSize / 4));
|
|
10347
|
+
const corners = [
|
|
10348
|
+
[minX + inset, minY + inset],
|
|
10349
|
+
[maxX - inset, minY + inset],
|
|
10350
|
+
[minX + inset, maxY - inset],
|
|
10351
|
+
[maxX - inset, maxY - inset]
|
|
10352
|
+
];
|
|
10353
|
+
for (const [x, y] of corners) {
|
|
10354
|
+
if (!isMarkerPixel(rgba, (y * width + x) * 4)) {
|
|
10355
|
+
return { error: `viewport markers were found but do not form a rectangle (box ${rect.width}x${rect.height} at ${rect.x},${rect.y})` };
|
|
10356
|
+
}
|
|
10357
|
+
}
|
|
10358
|
+
const scaleX = rect.width / hint.viewportWidth;
|
|
10359
|
+
const scaleY = rect.height / hint.viewportHeight;
|
|
10360
|
+
if (Math.abs(scaleX - scaleY) > MARKER_SCALE_TOLERANCE * Math.max(scaleX, scaleY)) {
|
|
10361
|
+
return {
|
|
10362
|
+
error: `viewport marker box ${rect.width}x${rect.height} does not match the reported ${hint.viewportWidth}x${hint.viewportHeight} viewport aspect`
|
|
10363
|
+
};
|
|
10364
|
+
}
|
|
10365
|
+
return { rect };
|
|
10366
|
+
}
|
|
10367
|
+
function cropToViewport(rgba, width, height, rect, targetWidth, targetHeight) {
|
|
10368
|
+
const x0 = Math.max(0, Math.min(width - 1, rect.x));
|
|
10369
|
+
const y0 = Math.max(0, Math.min(height - 1, rect.y));
|
|
10370
|
+
const srcW = Math.max(1, Math.min(rect.width, width - x0));
|
|
10371
|
+
const srcH = Math.max(1, Math.min(rect.height, height - y0));
|
|
10372
|
+
const exact = srcW === targetWidth && srcH === targetHeight;
|
|
10373
|
+
const outW = targetWidth;
|
|
10374
|
+
const outH = targetHeight;
|
|
10375
|
+
const out = Buffer.alloc(outW * outH * 4);
|
|
10376
|
+
if (exact) {
|
|
10377
|
+
for (let y = 0; y < outH; y++) {
|
|
10378
|
+
rgba.copy(out, y * outW * 4, ((y0 + y) * width + x0) * 4, ((y0 + y) * width + x0 + outW) * 4);
|
|
10379
|
+
}
|
|
10380
|
+
return { width: outW, height: outH, rgba: out };
|
|
10381
|
+
}
|
|
10382
|
+
const sx = srcW / outW;
|
|
10383
|
+
const sy = srcH / outH;
|
|
10384
|
+
for (let y = 0; y < outH; y++) {
|
|
10385
|
+
const fy = Math.min(srcH - 1, (y + 0.5) * sy - 0.5);
|
|
10386
|
+
const iy = Math.max(0, Math.floor(fy));
|
|
10387
|
+
const ny = Math.min(srcH - 1, iy + 1);
|
|
10388
|
+
const wy = Math.max(0, fy - iy);
|
|
10389
|
+
for (let x = 0; x < outW; x++) {
|
|
10390
|
+
const fx = Math.min(srcW - 1, (x + 0.5) * sx - 0.5);
|
|
10391
|
+
const ix = Math.max(0, Math.floor(fx));
|
|
10392
|
+
const nx = Math.min(srcW - 1, ix + 1);
|
|
10393
|
+
const wx = Math.max(0, fx - ix);
|
|
10394
|
+
const o00 = ((y0 + iy) * width + x0 + ix) * 4;
|
|
10395
|
+
const o10 = ((y0 + iy) * width + x0 + nx) * 4;
|
|
10396
|
+
const o01 = ((y0 + ny) * width + x0 + ix) * 4;
|
|
10397
|
+
const o11 = ((y0 + ny) * width + x0 + nx) * 4;
|
|
10398
|
+
const dst = (y * outW + x) * 4;
|
|
10399
|
+
for (let c = 0; c < 4; c++) {
|
|
10400
|
+
const top = rgba[o00 + c] * (1 - wx) + rgba[o10 + c] * wx;
|
|
10401
|
+
const bottom = rgba[o01 + c] * (1 - wx) + rgba[o11 + c] * wx;
|
|
10402
|
+
out[dst + c] = Math.round(top * (1 - wy) + bottom * wy);
|
|
10403
|
+
}
|
|
10404
|
+
}
|
|
10405
|
+
}
|
|
10406
|
+
return { width: outW, height: outH, rgba: out };
|
|
10407
|
+
}
|
|
10408
|
+
var WINDOWS_CAPTURE_SCRIPT = String.raw`
|
|
10409
|
+
$ErrorActionPreference = 'Stop'
|
|
10410
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
10411
|
+
Add-Type -AssemblyName System.Drawing
|
|
10412
|
+
Add-Type -TypeDefinition @"
|
|
10413
|
+
using System;
|
|
10414
|
+
using System.Collections.Generic;
|
|
10415
|
+
using System.Runtime.InteropServices;
|
|
10416
|
+
using System.Text;
|
|
10417
|
+
public static class McpStudioCapture {
|
|
10418
|
+
[DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lParam);
|
|
10419
|
+
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr hWnd);
|
|
10420
|
+
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
|
|
10421
|
+
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
|
|
10422
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowTextW(IntPtr hWnd, StringBuilder text, int count);
|
|
10423
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowTextLengthW(IntPtr hWnd);
|
|
10424
|
+
[DllImport("user32.dll")] public static extern bool GetClientRect(IntPtr hWnd, out RECT rect);
|
|
10425
|
+
[DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr hWnd, IntPtr hdc, uint flags);
|
|
10426
|
+
delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
10427
|
+
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
|
|
10428
|
+
public class Candidate { public IntPtr Handle; public uint Pid; public string Title; }
|
|
10429
|
+
public static List<Candidate> Find(string processName) {
|
|
10430
|
+
var found = new List<Candidate>();
|
|
10431
|
+
EnumWindows((hWnd, lParam) => {
|
|
10432
|
+
if (!IsWindowVisible(hWnd)) return true;
|
|
10433
|
+
int length = GetWindowTextLengthW(hWnd);
|
|
10434
|
+
if (length <= 0) return true;
|
|
10435
|
+
var sb = new StringBuilder(length + 1);
|
|
10436
|
+
GetWindowTextW(hWnd, sb, sb.Capacity);
|
|
10437
|
+
string title = sb.ToString();
|
|
10438
|
+
if (!title.EndsWith("Roblox Studio", StringComparison.Ordinal)) return true;
|
|
10439
|
+
uint pid;
|
|
10440
|
+
GetWindowThreadProcessId(hWnd, out pid);
|
|
10441
|
+
try {
|
|
10442
|
+
var proc = System.Diagnostics.Process.GetProcessById((int)pid);
|
|
10443
|
+
if (!string.Equals(proc.ProcessName, processName, StringComparison.OrdinalIgnoreCase)) return true;
|
|
10444
|
+
} catch { return true; }
|
|
10445
|
+
found.Add(new Candidate { Handle = hWnd, Pid = pid, Title = title });
|
|
10446
|
+
return true;
|
|
10447
|
+
}, IntPtr.Zero);
|
|
10448
|
+
return found;
|
|
10449
|
+
}
|
|
10450
|
+
}
|
|
10451
|
+
"@
|
|
10452
|
+
function Emit($obj) { Write-Output ($obj | ConvertTo-Json -Compress) }
|
|
10453
|
+
$hint = $env:MCP_CAPTURE_TITLE_HINT
|
|
10454
|
+
$outFile = $env:MCP_CAPTURE_OUT
|
|
10455
|
+
$candidates = [McpStudioCapture]::Find('RobloxStudioBeta')
|
|
10456
|
+
if ($candidates.Count -eq 0) { Emit @{ ok = $false; error = 'no visible Roblox Studio window was found' }; exit 0 }
|
|
10457
|
+
$pick = $null
|
|
10458
|
+
if ($hint) { $pick = $candidates | Where-Object { $_.Title.StartsWith($hint) } | Select-Object -First 1 }
|
|
10459
|
+
if ($pick -eq $null -and $candidates.Count -eq 1) { $pick = $candidates[0] }
|
|
10460
|
+
if ($pick -eq $null) {
|
|
10461
|
+
$titles = ($candidates | ForEach-Object { $_.Title }) -join ' | '
|
|
10462
|
+
Emit @{ ok = $false; error = "could not pick a Studio window for '$hint' among: $titles" }; exit 0
|
|
10463
|
+
}
|
|
10464
|
+
if ([McpStudioCapture]::IsIconic($pick.Handle)) {
|
|
10465
|
+
Emit @{ ok = $false; error = "the Studio window '$($pick.Title)' is minimized; restore it (it may stay behind other windows)" }; exit 0
|
|
10466
|
+
}
|
|
10467
|
+
$rect = New-Object McpStudioCapture+RECT
|
|
10468
|
+
[void][McpStudioCapture]::GetClientRect($pick.Handle, [ref]$rect)
|
|
10469
|
+
$w = $rect.Right - $rect.Left; $h = $rect.Bottom - $rect.Top
|
|
10470
|
+
if ($w -le 0 -or $h -le 0) { Emit @{ ok = $false; error = "the Studio window client area is empty ($w x $h)" }; exit 0 }
|
|
10471
|
+
$bmp = New-Object System.Drawing.Bitmap $w, $h, ([System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
|
10472
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
10473
|
+
$hdc = $g.GetHdc()
|
|
10474
|
+
# PW_CLIENTONLY (1) | PW_RENDERFULLCONTENT (2): DWM-composited client area, occluded windows included.
|
|
10475
|
+
$printed = [McpStudioCapture]::PrintWindow($pick.Handle, $hdc, 3)
|
|
10476
|
+
$g.ReleaseHdc($hdc); $g.Dispose()
|
|
10477
|
+
if (-not $printed) { $bmp.Dispose(); Emit @{ ok = $false; error = 'PrintWindow failed for the Studio window' }; exit 0 }
|
|
10478
|
+
$bounds = New-Object System.Drawing.Rectangle 0, 0, $w, $h
|
|
10479
|
+
$data = $bmp.LockBits($bounds, [System.Drawing.Imaging.ImageLockMode]::ReadOnly, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
|
10480
|
+
$bytes = New-Object byte[] ($data.Stride * $h)
|
|
10481
|
+
[System.Runtime.InteropServices.Marshal]::Copy($data.Scan0, $bytes, 0, $bytes.Length)
|
|
10482
|
+
$stride = $data.Stride
|
|
10483
|
+
$bmp.UnlockBits($data); $bmp.Dispose()
|
|
10484
|
+
[System.IO.File]::WriteAllBytes($outFile, $bytes)
|
|
10485
|
+
Emit @{ ok = $true; width = $w; height = $h; stride = $stride; title = $pick.Title }
|
|
10486
|
+
`;
|
|
10487
|
+
function powershellPath() {
|
|
10488
|
+
const systemRoot = process.env.SystemRoot ?? process.env.windir;
|
|
10489
|
+
if (systemRoot) {
|
|
10490
|
+
const candidate = path5.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
10491
|
+
if (fs3.existsSync(candidate))
|
|
10492
|
+
return candidate;
|
|
10493
|
+
}
|
|
10494
|
+
return "powershell.exe";
|
|
10495
|
+
}
|
|
10496
|
+
function runWindowsCapture(titleHint, outFile) {
|
|
10497
|
+
return new Promise((resolve5, reject) => {
|
|
10498
|
+
const encoded = Buffer.from(WINDOWS_CAPTURE_SCRIPT, "utf16le").toString("base64");
|
|
10499
|
+
const child = spawn2(powershellPath(), ["-NoProfile", "-NonInteractive", "-NoLogo", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], {
|
|
10500
|
+
env: { ...process.env, MCP_CAPTURE_TITLE_HINT: titleHint ?? "", MCP_CAPTURE_OUT: outFile },
|
|
10501
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10502
|
+
windowsHide: true
|
|
10503
|
+
});
|
|
10504
|
+
let stdout = "";
|
|
10505
|
+
let stderr = "";
|
|
10506
|
+
let settled = false;
|
|
10507
|
+
const timer = setTimeout(() => {
|
|
10508
|
+
if (settled)
|
|
10509
|
+
return;
|
|
10510
|
+
settled = true;
|
|
10511
|
+
child.kill();
|
|
10512
|
+
reject(new Error(`host window capture timed out after ${HOST_CAPTURE_TIMEOUT_MS}ms`));
|
|
10513
|
+
}, HOST_CAPTURE_TIMEOUT_MS);
|
|
10514
|
+
child.stdout.setEncoding("utf8");
|
|
10515
|
+
child.stderr.setEncoding("utf8");
|
|
10516
|
+
child.stdout.on("data", (chunk) => {
|
|
10517
|
+
stdout += chunk;
|
|
10518
|
+
});
|
|
10519
|
+
child.stderr.on("data", (chunk) => {
|
|
10520
|
+
stderr += chunk;
|
|
10521
|
+
});
|
|
10522
|
+
child.on("error", (error) => {
|
|
10523
|
+
if (settled)
|
|
10524
|
+
return;
|
|
10525
|
+
settled = true;
|
|
10526
|
+
clearTimeout(timer);
|
|
10527
|
+
reject(new Error(`could not start PowerShell for host window capture: ${error.message}`));
|
|
10528
|
+
});
|
|
10529
|
+
child.on("close", (code) => {
|
|
10530
|
+
if (settled)
|
|
10531
|
+
return;
|
|
10532
|
+
settled = true;
|
|
10533
|
+
clearTimeout(timer);
|
|
10534
|
+
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
10535
|
+
const last = lines[lines.length - 1];
|
|
10536
|
+
if (last && last.startsWith("{")) {
|
|
10537
|
+
try {
|
|
10538
|
+
resolve5(JSON.parse(last));
|
|
10539
|
+
return;
|
|
10540
|
+
} catch {
|
|
10541
|
+
}
|
|
10542
|
+
}
|
|
10543
|
+
const detail = (stderr.trim() || stdout.trim()).slice(0, 600);
|
|
10544
|
+
reject(new Error(`host window capture helper exited with code ${code}${detail ? `: ${detail}` : ""}`));
|
|
10545
|
+
});
|
|
10546
|
+
});
|
|
10547
|
+
}
|
|
10548
|
+
async function captureStudioWindow(titleHint) {
|
|
10549
|
+
if (isHostCaptureDisabled()) {
|
|
10550
|
+
return { ok: false, error: "host window capture is disabled by ROBLOX_STUDIO_HOST_CAPTURE" };
|
|
10551
|
+
}
|
|
10552
|
+
if (!isHostCaptureSupported()) {
|
|
10553
|
+
return { ok: false, error: hostCaptureUnsupportedReason() };
|
|
10554
|
+
}
|
|
10555
|
+
const outFile = path5.join(os3.tmpdir(), `robloxstudio-mcp-capture-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.bgra`);
|
|
10556
|
+
try {
|
|
10557
|
+
const report = await runWindowsCapture(titleHint, outFile);
|
|
10558
|
+
if (!report.ok || !report.width || !report.height || !report.stride) {
|
|
10559
|
+
return { ok: false, error: report.error ?? "host window capture returned no image" };
|
|
10560
|
+
}
|
|
10561
|
+
const raw = fs3.readFileSync(outFile);
|
|
10562
|
+
const { width, height, stride } = report;
|
|
10563
|
+
const rgba = Buffer.alloc(width * height * 4);
|
|
10564
|
+
for (let y = 0; y < height; y++) {
|
|
10565
|
+
const src = y * stride;
|
|
10566
|
+
const dst = y * width * 4;
|
|
10567
|
+
for (let x = 0; x < width; x++) {
|
|
10568
|
+
const s = src + x * 4;
|
|
10569
|
+
const d = dst + x * 4;
|
|
10570
|
+
rgba[d] = raw[s + 2];
|
|
10571
|
+
rgba[d + 1] = raw[s + 1];
|
|
10572
|
+
rgba[d + 2] = raw[s];
|
|
10573
|
+
rgba[d + 3] = 255;
|
|
10574
|
+
}
|
|
10575
|
+
}
|
|
10576
|
+
return { ok: true, capture: { width, height, rgba, title: report.title ?? "" } };
|
|
10577
|
+
} catch (error) {
|
|
10578
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
10579
|
+
} finally {
|
|
10580
|
+
fs3.rmSync(outFile, { force: true });
|
|
10581
|
+
}
|
|
10582
|
+
}
|
|
10583
|
+
|
|
10584
|
+
// ../core/dist/tools/index.js
|
|
10585
|
+
import * as fs4 from "fs";
|
|
10586
|
+
import * as path6 from "path";
|
|
10587
|
+
var HOST_VIEWPORT_RECT_TTL_MS = 6e4;
|
|
10034
10588
|
var MAX_INLINE_IMAGE_BYTES = 6e6;
|
|
10035
10589
|
var MAX_MATRIX_IMAGE_BYTES = 8e6;
|
|
10036
10590
|
var DEFAULT_ASSET_AUDIO_PREVIEWS = 3;
|
|
@@ -10209,9 +10763,9 @@ function compactSoundReference(sound) {
|
|
|
10209
10763
|
name: stringField(sound, "name"),
|
|
10210
10764
|
className: stringField(sound, "className")
|
|
10211
10765
|
};
|
|
10212
|
-
const
|
|
10213
|
-
if (
|
|
10214
|
-
compact.path =
|
|
10766
|
+
const path7 = stringField(sound, "path");
|
|
10767
|
+
if (path7)
|
|
10768
|
+
compact.path = path7;
|
|
10215
10769
|
const assetId = robloxAssetIdFromContentId(sound.assetId ?? sound.soundId ?? sound.asset);
|
|
10216
10770
|
if (assetId !== void 0)
|
|
10217
10771
|
compact.assetId = assetId;
|
|
@@ -10274,8 +10828,8 @@ function loadMicroProfilerBaseline(source, sourcePath) {
|
|
|
10274
10828
|
if (typeof sourcePath !== "string" || sourcePath === "") {
|
|
10275
10829
|
throw new Error("baseline_path must be a non-empty string when provided");
|
|
10276
10830
|
}
|
|
10277
|
-
const resolved =
|
|
10278
|
-
const parsed = JSON.parse(
|
|
10831
|
+
const resolved = path6.resolve(sourcePath);
|
|
10832
|
+
const parsed = JSON.parse(fs4.readFileSync(resolved, "utf8"));
|
|
10279
10833
|
const record = asRecord(parsed);
|
|
10280
10834
|
if (!record)
|
|
10281
10835
|
throw new Error(`baseline_path did not contain a JSON object: ${resolved}`);
|
|
@@ -10777,6 +11331,9 @@ var RobloxStudioTools = class {
|
|
|
10777
11331
|
cookieClient;
|
|
10778
11332
|
instanceManager;
|
|
10779
11333
|
managedConnectionAssociations = Promise.resolve();
|
|
11334
|
+
hostWindowCapture = captureStudioWindow;
|
|
11335
|
+
hostViewportRects = /* @__PURE__ */ new Map();
|
|
11336
|
+
viewportCaptureQueues = /* @__PURE__ */ new Map();
|
|
10780
11337
|
constructor(bridge) {
|
|
10781
11338
|
this.client = new StudioHttpClient(bridge);
|
|
10782
11339
|
this.bridge = bridge;
|
|
@@ -10886,7 +11443,10 @@ var RobloxStudioTools = class {
|
|
|
10886
11443
|
_requestPeer(endpoint, data, targetPeerId, timeoutMs, signal, operationId) {
|
|
10887
11444
|
return this.client.request(endpoint, data, targetPeerId, timeoutMs, signal, operationId);
|
|
10888
11445
|
}
|
|
10889
|
-
_request(endpoint, data, instanceId, role, timeoutMs, signal) {
|
|
11446
|
+
async _request(endpoint, data, instanceId, role, timeoutMs, signal) {
|
|
11447
|
+
const refresh = this.bridge.refreshTopologyForRouting(signal);
|
|
11448
|
+
if (refresh)
|
|
11449
|
+
await refresh;
|
|
10890
11450
|
const peer = this._peerForRoleInScope(instanceId, role);
|
|
10891
11451
|
if (!peer) {
|
|
10892
11452
|
throw new RoutingFailure({
|
|
@@ -10899,6 +11459,9 @@ var RobloxStudioTools = class {
|
|
|
10899
11459
|
}
|
|
10900
11460
|
// Resolve an optional Studio process plus role to one exact Peer and dispatch.
|
|
10901
11461
|
async _callSingle(endpoint, data, target, instance_id, timeoutMs, signal, operationId) {
|
|
11462
|
+
const refresh = this.bridge.refreshTopologyForRouting(signal);
|
|
11463
|
+
if (refresh)
|
|
11464
|
+
await refresh;
|
|
10902
11465
|
const resolved = this.bridge.resolveTarget({ instance_id, target });
|
|
10903
11466
|
if (!resolved.ok)
|
|
10904
11467
|
throw new RoutingFailure(resolved.error);
|
|
@@ -10930,6 +11493,7 @@ var RobloxStudioTools = class {
|
|
|
10930
11493
|
clientRole: client?.role
|
|
10931
11494
|
};
|
|
10932
11495
|
}
|
|
11496
|
+
// Resolve synchronously after the caller has refreshed and captured its topology.
|
|
10933
11497
|
_resolveInstanceIdOnly(instance_id) {
|
|
10934
11498
|
if (instance_id !== void 0) {
|
|
10935
11499
|
const resolvedInstanceId = this.bridge.resolveConnectedInstanceId(instance_id);
|
|
@@ -11143,6 +11707,9 @@ var RobloxStudioTools = class {
|
|
|
11143
11707
|
async _waitForRuntimeRoles(instanceId, opts, timeoutSec = 30) {
|
|
11144
11708
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
11145
11709
|
while (Date.now() < deadline) {
|
|
11710
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
11711
|
+
if (refresh)
|
|
11712
|
+
await refresh;
|
|
11146
11713
|
const roles = this._rolesForScope(instanceId);
|
|
11147
11714
|
const clientRoles = this._clientRolesForScope(instanceId);
|
|
11148
11715
|
const hasServer = !opts.server || roles.includes("server");
|
|
@@ -11164,6 +11731,9 @@ var RobloxStudioTools = class {
|
|
|
11164
11731
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
11165
11732
|
let exactSince;
|
|
11166
11733
|
while (Date.now() < deadline) {
|
|
11734
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
11735
|
+
if (refresh)
|
|
11736
|
+
await refresh;
|
|
11167
11737
|
const roles2 = this._rolesForScope(instanceId);
|
|
11168
11738
|
const clientCount2 = this._clientRolesForScope(instanceId).length;
|
|
11169
11739
|
if (clientCount2 > expectedClientCount) {
|
|
@@ -11186,6 +11756,9 @@ var RobloxStudioTools = class {
|
|
|
11186
11756
|
async _waitForRuntimeRolesFresh(instanceId, connectedAfter, requiredRoles, timeoutSec = 60) {
|
|
11187
11757
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
11188
11758
|
while (Date.now() < deadline) {
|
|
11759
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
11760
|
+
if (refresh)
|
|
11761
|
+
await refresh;
|
|
11189
11762
|
const peers = this.bridge.getPeersInScope(instanceId);
|
|
11190
11763
|
const roles = peers.map((peer) => peer.role);
|
|
11191
11764
|
const freshRoles = new Set(peers.filter((peer) => peer.connectedAt >= connectedAfter).map((peer) => peer.role));
|
|
@@ -11200,8 +11773,8 @@ var RobloxStudioTools = class {
|
|
|
11200
11773
|
timedOut: true
|
|
11201
11774
|
};
|
|
11202
11775
|
}
|
|
11203
|
-
async getFileTree(
|
|
11204
|
-
const response = await this._callSingle("/api/file-tree", { path:
|
|
11776
|
+
async getFileTree(path7 = "", instance_id) {
|
|
11777
|
+
const response = await this._callSingle("/api/file-tree", { path: path7 }, void 0, instance_id);
|
|
11205
11778
|
return {
|
|
11206
11779
|
content: [
|
|
11207
11780
|
{
|
|
@@ -11293,9 +11866,9 @@ var RobloxStudioTools = class {
|
|
|
11293
11866
|
]
|
|
11294
11867
|
};
|
|
11295
11868
|
}
|
|
11296
|
-
async getProjectStructure(
|
|
11869
|
+
async getProjectStructure(path7, maxDepth, scriptsOnly, instance_id) {
|
|
11297
11870
|
const response = await this._callSingle("/api/project-structure", {
|
|
11298
|
-
path:
|
|
11871
|
+
path: path7,
|
|
11299
11872
|
maxDepth,
|
|
11300
11873
|
scriptsOnly
|
|
11301
11874
|
}, void 0, instance_id);
|
|
@@ -11456,7 +12029,7 @@ var RobloxStudioTools = class {
|
|
|
11456
12029
|
};
|
|
11457
12030
|
}
|
|
11458
12031
|
async setSelection(paths, mode, instance_id) {
|
|
11459
|
-
if (!Array.isArray(paths) || paths.some((
|
|
12032
|
+
if (!Array.isArray(paths) || paths.some((path7) => typeof path7 !== "string" || path7.length === 0)) {
|
|
11460
12033
|
throw new Error("selection paths must contain only non-empty instance paths");
|
|
11461
12034
|
}
|
|
11462
12035
|
const selectionMode = mode ?? "set";
|
|
@@ -11483,6 +12056,9 @@ var RobloxStudioTools = class {
|
|
|
11483
12056
|
if (angleY !== void 0 && (angleY < -89 || angleY > 89)) {
|
|
11484
12057
|
throw new Error("selection angleY must be between -89 and 89");
|
|
11485
12058
|
}
|
|
12059
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
12060
|
+
if (refresh)
|
|
12061
|
+
await refresh;
|
|
11486
12062
|
const { instanceId, clientRole } = this._resolveRuntime(instance_id);
|
|
11487
12063
|
const response = await this._callSingle("/api/focus-viewport", {
|
|
11488
12064
|
path: instancePath,
|
|
@@ -11547,6 +12123,9 @@ var RobloxStudioTools = class {
|
|
|
11547
12123
|
}
|
|
11548
12124
|
async setNetworkProfile(profile, target, overrides, instance_id) {
|
|
11549
12125
|
const values = normalizeNetworkProfile(profile, overrides);
|
|
12126
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
12127
|
+
if (refresh)
|
|
12128
|
+
await refresh;
|
|
11550
12129
|
const instanceId = this._resolveInstanceIdOnly(instance_id);
|
|
11551
12130
|
const clientRoles = this._clientRolesForScope(instanceId);
|
|
11552
12131
|
const selectedTarget = target ?? "client-1";
|
|
@@ -11613,6 +12192,9 @@ var RobloxStudioTools = class {
|
|
|
11613
12192
|
const selectedInclude = this._normalizeSimulationInclude(include);
|
|
11614
12193
|
const includeNetwork = selectedInclude === "network" || selectedInclude === "both";
|
|
11615
12194
|
const includeDeviceSimulator = selectedInclude === "deviceSimulator" || selectedInclude === "both";
|
|
12195
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
12196
|
+
if (refresh)
|
|
12197
|
+
await refresh;
|
|
11616
12198
|
const resolved = this._resolveSimulationTargets(target, instance_id, "get_simulation_state");
|
|
11617
12199
|
const roleEntries = await Promise.all(resolved.roles.map(async (role) => {
|
|
11618
12200
|
const state = {};
|
|
@@ -11659,6 +12241,9 @@ var RobloxStudioTools = class {
|
|
|
11659
12241
|
if (!resetNetwork && !resetDeviceSimulator) {
|
|
11660
12242
|
throw new Error("reset_simulation_state requires network=true and/or deviceSimulator=true; both default to true");
|
|
11661
12243
|
}
|
|
12244
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
12245
|
+
if (refresh)
|
|
12246
|
+
await refresh;
|
|
11662
12247
|
const resolved = this._resolveSimulationTargets(target, instance_id, "reset_simulation_state");
|
|
11663
12248
|
const roleEntries = await Promise.all(resolved.roles.map(async (role) => {
|
|
11664
12249
|
const result = {};
|
|
@@ -11717,6 +12302,9 @@ var RobloxStudioTools = class {
|
|
|
11717
12302
|
if (deviceId !== void 0 && (typeof deviceId !== "string" || deviceId.trim() === "")) {
|
|
11718
12303
|
throw new Error("deviceId must be a non-empty string when provided");
|
|
11719
12304
|
}
|
|
12305
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
12306
|
+
if (refresh)
|
|
12307
|
+
await refresh;
|
|
11720
12308
|
const resolved = this._resolveDeviceSimulatorSingleTarget(target, instance_id, "get_device_simulator_state");
|
|
11721
12309
|
const state = await this._executeDeviceSimulatorOperation(resolved.instanceId, resolved.role, "get", {
|
|
11722
12310
|
includeDeviceList: includeDeviceList !== false,
|
|
@@ -11741,6 +12329,9 @@ var RobloxStudioTools = class {
|
|
|
11741
12329
|
if (stopSimulation !== true && !hasDeviceSimulatorSettings(settings)) {
|
|
11742
12330
|
throw new Error("set_device_simulator requires stopSimulation=true or at least one simulator setting");
|
|
11743
12331
|
}
|
|
12332
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
12333
|
+
if (refresh)
|
|
12334
|
+
await refresh;
|
|
11744
12335
|
const resolved = this._resolveDeviceSimulatorSetTargets(target, instance_id);
|
|
11745
12336
|
const responses = await Promise.allSettled(resolved.roles.map(async (role) => {
|
|
11746
12337
|
const result = await this._executeDeviceSimulatorOperation(resolved.instanceId, role, "set", stopSimulation === true ? { stopSimulation: true } : { settings });
|
|
@@ -11799,6 +12390,9 @@ var RobloxStudioTools = class {
|
|
|
11799
12390
|
label: raw.label
|
|
11800
12391
|
};
|
|
11801
12392
|
});
|
|
12393
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
12394
|
+
if (refresh)
|
|
12395
|
+
await refresh;
|
|
11802
12396
|
const resolved = this._resolveDeviceSimulatorSingleTarget(target, instance_id, "capture_device_matrix");
|
|
11803
12397
|
if (resolved.role.startsWith("client-") && await this._isMultiplayerTestRunning(resolved.instanceId)) {
|
|
11804
12398
|
throw new Error("capture_device_matrix does not support StudioTestService multiplayer client targets because Roblox scopes temporary screenshot textures per client process");
|
|
@@ -12150,6 +12744,9 @@ var RobloxStudioTools = class {
|
|
|
12150
12744
|
if (outputPath) {
|
|
12151
12745
|
data.__mcp_include_raw_json = true;
|
|
12152
12746
|
}
|
|
12747
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
12748
|
+
if (refresh)
|
|
12749
|
+
await refresh;
|
|
12153
12750
|
const resolved = this.bridge.resolveTarget({ instance_id, target: targetRole });
|
|
12154
12751
|
if (!resolved.ok)
|
|
12155
12752
|
throw new RoutingFailure(resolved.error);
|
|
@@ -12169,9 +12766,9 @@ var RobloxStudioTools = class {
|
|
|
12169
12766
|
const rawJson = mutable.raw_json;
|
|
12170
12767
|
if (typeof rawJson === "string") {
|
|
12171
12768
|
if (typeof outputPath === "string" && outputPath !== "") {
|
|
12172
|
-
const resolvedOutputPath =
|
|
12173
|
-
|
|
12174
|
-
|
|
12769
|
+
const resolvedOutputPath = path6.resolve(outputPath);
|
|
12770
|
+
fs4.mkdirSync(path6.dirname(resolvedOutputPath), { recursive: true });
|
|
12771
|
+
fs4.writeFileSync(resolvedOutputPath, rawJson, "utf8");
|
|
12175
12772
|
mutable.output_path = resolvedOutputPath;
|
|
12176
12773
|
}
|
|
12177
12774
|
delete mutable.raw_json;
|
|
@@ -12210,6 +12807,9 @@ var RobloxStudioTools = class {
|
|
|
12210
12807
|
if (summaryOutputPath || baselinePath || baseline || includeComparisonIndex) {
|
|
12211
12808
|
data.__mcp_include_comparison_index = true;
|
|
12212
12809
|
}
|
|
12810
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
12811
|
+
if (refresh)
|
|
12812
|
+
await refresh;
|
|
12213
12813
|
const resolved = this.bridge.resolveTarget({ instance_id, target: targetRole });
|
|
12214
12814
|
if (!resolved.ok)
|
|
12215
12815
|
throw new RoutingFailure(resolved.error);
|
|
@@ -12229,9 +12829,9 @@ var RobloxStudioTools = class {
|
|
|
12229
12829
|
const rawSnapshotBase64 = mutable.raw_snapshot_base64;
|
|
12230
12830
|
if (typeof rawSnapshotBase64 === "string") {
|
|
12231
12831
|
if (typeof outputPath === "string" && outputPath !== "") {
|
|
12232
|
-
const resolvedOutputPath =
|
|
12233
|
-
|
|
12234
|
-
|
|
12832
|
+
const resolvedOutputPath = path6.resolve(outputPath);
|
|
12833
|
+
fs4.mkdirSync(path6.dirname(resolvedOutputPath), { recursive: true });
|
|
12834
|
+
fs4.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
|
|
12235
12835
|
mutable.output_path = resolvedOutputPath;
|
|
12236
12836
|
}
|
|
12237
12837
|
delete mutable.raw_snapshot_base64;
|
|
@@ -12245,9 +12845,9 @@ var RobloxStudioTools = class {
|
|
|
12245
12845
|
});
|
|
12246
12846
|
}
|
|
12247
12847
|
if (typeof summaryOutputPath === "string" && summaryOutputPath !== "") {
|
|
12248
|
-
const resolvedSummaryPath =
|
|
12249
|
-
|
|
12250
|
-
|
|
12848
|
+
const resolvedSummaryPath = path6.resolve(summaryOutputPath);
|
|
12849
|
+
fs4.mkdirSync(path6.dirname(resolvedSummaryPath), { recursive: true });
|
|
12850
|
+
fs4.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
|
|
12251
12851
|
mutable.summary_output_path = resolvedSummaryPath;
|
|
12252
12852
|
}
|
|
12253
12853
|
if (!includeComparisonIndex) {
|
|
@@ -12264,6 +12864,9 @@ var RobloxStudioTools = class {
|
|
|
12264
12864
|
const data = { ...request, action };
|
|
12265
12865
|
delete data.target;
|
|
12266
12866
|
delete data.instance_id;
|
|
12867
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
12868
|
+
if (refresh)
|
|
12869
|
+
await refresh;
|
|
12267
12870
|
const resolved = this.bridge.resolveTarget({ instance_id, target: targetRole });
|
|
12268
12871
|
if (!resolved.ok)
|
|
12269
12872
|
throw new RoutingFailure(resolved.error);
|
|
@@ -12306,7 +12909,7 @@ var RobloxStudioTools = class {
|
|
|
12306
12909
|
return record.placeId !== void 0 && instance.placeId === record.placeId;
|
|
12307
12910
|
}
|
|
12308
12911
|
if ((record.source === "baseplate" || record.source === "local_file") && record.localPlaceFile) {
|
|
12309
|
-
const expectedName =
|
|
12912
|
+
const expectedName = path6.basename(record.localPlaceFile);
|
|
12310
12913
|
return instance.placeName === expectedName || instance.dataModelName === expectedName;
|
|
12311
12914
|
}
|
|
12312
12915
|
return true;
|
|
@@ -12453,6 +13056,7 @@ var RobloxStudioTools = class {
|
|
|
12453
13056
|
});
|
|
12454
13057
|
}
|
|
12455
13058
|
return this._textResult({
|
|
13059
|
+
test_worker_job_name: parseStudioTestWorkerJobName(process.env.RSMCP_STUDIO_TEST_WORKER_JOB),
|
|
12456
13060
|
managed: (await this.instanceManager.list()).filter((record2) => record2.closedAt === void 0).map((record2) => this._managedStatus(record2)),
|
|
12457
13061
|
connected: this.bridge.getPublicInstances().map((instance) => ({
|
|
12458
13062
|
instance_id: instance.id,
|
|
@@ -12531,10 +13135,9 @@ var RobloxStudioTools = class {
|
|
|
12531
13135
|
}
|
|
12532
13136
|
record2 = active[0];
|
|
12533
13137
|
}
|
|
12534
|
-
if (record2.instanceId)
|
|
12535
|
-
await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
|
|
12536
13138
|
const closeResult = await this.instanceManager.close(record2);
|
|
12537
13139
|
if (record2.instanceId) {
|
|
13140
|
+
await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
|
|
12538
13141
|
await sleep(500);
|
|
12539
13142
|
await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
|
|
12540
13143
|
}
|
|
@@ -12614,6 +13217,9 @@ var RobloxStudioTools = class {
|
|
|
12614
13217
|
throw new Error("solo_playtest requires action=start|stop|status");
|
|
12615
13218
|
}
|
|
12616
13219
|
if (action === "status") {
|
|
13220
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
13221
|
+
if (refresh)
|
|
13222
|
+
await refresh;
|
|
12617
13223
|
const instanceId = this._resolveInstanceIdOnly(instance_id);
|
|
12618
13224
|
const { roles, runtimeRoles } = this._briefRoles(instanceId);
|
|
12619
13225
|
return this._textResult({
|
|
@@ -12674,6 +13280,9 @@ var RobloxStudioTools = class {
|
|
|
12674
13280
|
}
|
|
12675
13281
|
const data = { mode };
|
|
12676
13282
|
const startedAt = Date.now();
|
|
13283
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
13284
|
+
if (refresh)
|
|
13285
|
+
await refresh;
|
|
12677
13286
|
const resolved = this.bridge.resolveTarget({ instance_id, target: void 0 });
|
|
12678
13287
|
if (!resolved.ok)
|
|
12679
13288
|
throw new RoutingFailure(resolved.error);
|
|
@@ -12724,6 +13333,9 @@ var RobloxStudioTools = class {
|
|
|
12724
13333
|
};
|
|
12725
13334
|
}
|
|
12726
13335
|
async stopPlaytest(instance_id, timeout = 15) {
|
|
13336
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
13337
|
+
if (refresh)
|
|
13338
|
+
await refresh;
|
|
12727
13339
|
const { instanceId } = this._resolveSingleTarget("edit", instance_id);
|
|
12728
13340
|
let response;
|
|
12729
13341
|
let stopRequestError;
|
|
@@ -12774,6 +13386,9 @@ var RobloxStudioTools = class {
|
|
|
12774
13386
|
};
|
|
12775
13387
|
}
|
|
12776
13388
|
async _buildMultiplayerState(instanceId) {
|
|
13389
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
13390
|
+
if (refresh)
|
|
13391
|
+
await refresh;
|
|
12777
13392
|
const peers = this.bridge.getPublicPeers().filter((peer) => this.bridge.getInstanceIdsInScope(instanceId).includes(peer.instanceId)).sort((a, b) => a.role.localeCompare(b.role));
|
|
12778
13393
|
const multiplayerGroup = this.bridge.getMultiplayerGroups().find((group) => group.instanceIds.includes(instanceId));
|
|
12779
13394
|
const body = {
|
|
@@ -12819,6 +13434,9 @@ var RobloxStudioTools = class {
|
|
|
12819
13434
|
async _waitForMultiplayerEditDone(instanceId, timeoutSec = 30) {
|
|
12820
13435
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
12821
13436
|
while (Date.now() < deadline) {
|
|
13437
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
13438
|
+
if (refresh)
|
|
13439
|
+
await refresh;
|
|
12822
13440
|
if (!this._rolesForScope(instanceId).includes("edit"))
|
|
12823
13441
|
return false;
|
|
12824
13442
|
try {
|
|
@@ -12833,6 +13451,9 @@ var RobloxStudioTools = class {
|
|
|
12833
13451
|
return false;
|
|
12834
13452
|
}
|
|
12835
13453
|
async _isMultiplayerTestRunning(instanceId) {
|
|
13454
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
13455
|
+
if (refresh)
|
|
13456
|
+
await refresh;
|
|
12836
13457
|
return this.bridge.getMultiplayerGroups().some((group) => group.instanceIds.includes(instanceId));
|
|
12837
13458
|
}
|
|
12838
13459
|
async _waitForMultiplayerStart(instanceId, clientCount, timeoutSec = 30, connectedAfter) {
|
|
@@ -12873,6 +13494,9 @@ var RobloxStudioTools = class {
|
|
|
12873
13494
|
throw new Error("multiplayer_playtest requires action=start|status|add_players|leave_client|end");
|
|
12874
13495
|
}
|
|
12875
13496
|
const briefState = async (instanceId) => {
|
|
13497
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
13498
|
+
if (refresh)
|
|
13499
|
+
await refresh;
|
|
12876
13500
|
const state = await this._buildMultiplayerState(this._resolveInstanceIdOnly(instanceId));
|
|
12877
13501
|
const roles = Array.isArray(state.peers) ? state.peers.flatMap((peer) => peer !== null && typeof peer === "object" && "role" in peer && typeof peer.role === "string" ? [peer.role] : []) : [];
|
|
12878
13502
|
return {
|
|
@@ -12970,6 +13594,9 @@ var RobloxStudioTools = class {
|
|
|
12970
13594
|
if (!Number.isInteger(numPlayers) || numPlayers < 1 || numPlayers > 8) {
|
|
12971
13595
|
throw new Error("numPlayers must be an integer from 1 to 8");
|
|
12972
13596
|
}
|
|
13597
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
13598
|
+
if (refresh)
|
|
13599
|
+
await refresh;
|
|
12973
13600
|
const editTarget = this._resolveSingleTarget("edit", instance_id);
|
|
12974
13601
|
const existingRuntime = this._runtimeTargetsForScope(editTarget.instanceId);
|
|
12975
13602
|
if (existingRuntime.length > 0) {
|
|
@@ -13026,6 +13653,9 @@ var RobloxStudioTools = class {
|
|
|
13026
13653
|
};
|
|
13027
13654
|
}
|
|
13028
13655
|
async multiplayerTestState(instance_id) {
|
|
13656
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
13657
|
+
if (refresh)
|
|
13658
|
+
await refresh;
|
|
13029
13659
|
const instanceId = this._resolveInstanceIdOnly(instance_id);
|
|
13030
13660
|
const state = await this._buildMultiplayerState(instanceId);
|
|
13031
13661
|
return { content: [{ type: "text", text: JSON.stringify(state) }] };
|
|
@@ -13034,6 +13664,9 @@ var RobloxStudioTools = class {
|
|
|
13034
13664
|
if (!Number.isInteger(numPlayers) || numPlayers < 1 || numPlayers > 8) {
|
|
13035
13665
|
throw new Error("numPlayers must be an integer from 1 to 8");
|
|
13036
13666
|
}
|
|
13667
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
13668
|
+
if (refresh)
|
|
13669
|
+
await refresh;
|
|
13037
13670
|
const serverTarget = this._resolveSingleTarget("server", instance_id);
|
|
13038
13671
|
const group = this.bridge.getMultiplayerGroups().find((candidate) => candidate.instanceIds.includes(serverTarget.instanceId));
|
|
13039
13672
|
const scopeInstanceId = group?.controllerInstanceId ?? serverTarget.instanceId;
|
|
@@ -13062,6 +13695,9 @@ var RobloxStudioTools = class {
|
|
|
13062
13695
|
if (!/^client-\d+$/.test(target)) {
|
|
13063
13696
|
throw new Error(`multiplayer_test_leave_client requires target=client-N (got: ${target})`);
|
|
13064
13697
|
}
|
|
13698
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
13699
|
+
if (refresh)
|
|
13700
|
+
await refresh;
|
|
13065
13701
|
const clientTarget = this._resolveSingleTarget(target, instance_id);
|
|
13066
13702
|
const group = this.bridge.getMultiplayerGroups().find((candidate) => candidate.instanceIds.includes(clientTarget.instanceId));
|
|
13067
13703
|
const scopeInstanceId = group?.controllerInstanceId ?? clientTarget.instanceId;
|
|
@@ -13085,6 +13721,9 @@ var RobloxStudioTools = class {
|
|
|
13085
13721
|
};
|
|
13086
13722
|
}
|
|
13087
13723
|
async multiplayerTestEnd(value, timeout, instance_id) {
|
|
13724
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
13725
|
+
if (refresh)
|
|
13726
|
+
await refresh;
|
|
13088
13727
|
let serverTarget;
|
|
13089
13728
|
try {
|
|
13090
13729
|
serverTarget = this._resolveSingleTarget("server", instance_id);
|
|
@@ -13133,6 +13772,9 @@ var RobloxStudioTools = class {
|
|
|
13133
13772
|
return result;
|
|
13134
13773
|
}
|
|
13135
13774
|
async getConnectedInstances() {
|
|
13775
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
13776
|
+
if (refresh)
|
|
13777
|
+
await refresh;
|
|
13136
13778
|
return this._textResult({
|
|
13137
13779
|
instances: this.bridge.getConnectedInstances(),
|
|
13138
13780
|
multiplayerGroups: this.bridge.getConnectedMultiplayerGroups()
|
|
@@ -13598,11 +14240,11 @@ var RobloxStudioTools = class {
|
|
|
13598
14240
|
return this.resolveUploadedReferenceImageId(decalId, instance_id);
|
|
13599
14241
|
}
|
|
13600
14242
|
async uploadAsset(filePath, assetType, displayName, description, userId, groupId) {
|
|
13601
|
-
if (!
|
|
14243
|
+
if (!fs4.existsSync(filePath)) {
|
|
13602
14244
|
throw new Error(`File not found: ${filePath}`);
|
|
13603
14245
|
}
|
|
13604
|
-
const fileContent =
|
|
13605
|
-
const fileName =
|
|
14246
|
+
const fileContent = fs4.readFileSync(filePath);
|
|
14247
|
+
const fileName = path6.basename(filePath);
|
|
13606
14248
|
const resolvedGroupId = groupId || process.env.ROBLOX_CREATOR_GROUP_ID;
|
|
13607
14249
|
const resolvedUserId = userId || process.env.ROBLOX_CREATOR_USER_ID;
|
|
13608
14250
|
if (assetType === "Decal" && this.cookieClient.hasCookie()) {
|
|
@@ -13674,6 +14316,9 @@ var RobloxStudioTools = class {
|
|
|
13674
14316
|
if (!action) {
|
|
13675
14317
|
throw new Error("action is required for simulate_mouse_input");
|
|
13676
14318
|
}
|
|
14319
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
14320
|
+
if (refresh)
|
|
14321
|
+
await refresh;
|
|
13677
14322
|
const { instanceId, clientRole } = this._resolveRuntime(instance_id);
|
|
13678
14323
|
const response = await this._callSingle("/api/simulate-mouse-input", {
|
|
13679
14324
|
action,
|
|
@@ -13692,6 +14337,9 @@ var RobloxStudioTools = class {
|
|
|
13692
14337
|
if (!keyCode && text === void 0) {
|
|
13693
14338
|
throw new Error("keyCode or text is required for simulate_keyboard_input");
|
|
13694
14339
|
}
|
|
14340
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
14341
|
+
if (refresh)
|
|
14342
|
+
await refresh;
|
|
13695
14343
|
const { instanceId, clientRole } = this._resolveRuntime(instance_id);
|
|
13696
14344
|
const response = await this._callSingle("/api/simulate-keyboard-input", {
|
|
13697
14345
|
keyCode,
|
|
@@ -13730,6 +14378,9 @@ var RobloxStudioTools = class {
|
|
|
13730
14378
|
const data = {};
|
|
13731
14379
|
if (tags !== void 0)
|
|
13732
14380
|
data.tags = tags;
|
|
14381
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
14382
|
+
if (refresh)
|
|
14383
|
+
await refresh;
|
|
13733
14384
|
const resolved = this.bridge.resolveTarget({ instance_id, target: tgt });
|
|
13734
14385
|
if (!resolved.ok)
|
|
13735
14386
|
throw new RoutingFailure(resolved.error);
|
|
@@ -13763,6 +14414,9 @@ var RobloxStudioTools = class {
|
|
|
13763
14414
|
data.topN = topN;
|
|
13764
14415
|
if (raw !== void 0)
|
|
13765
14416
|
data.raw = raw;
|
|
14417
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
14418
|
+
if (refresh)
|
|
14419
|
+
await refresh;
|
|
13766
14420
|
const resolved = this.bridge.resolveTarget({ instance_id, target: tgt });
|
|
13767
14421
|
if (!resolved.ok)
|
|
13768
14422
|
throw new RoutingFailure(resolved.error);
|
|
@@ -13806,10 +14460,10 @@ var RobloxStudioTools = class {
|
|
|
13806
14460
|
return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
|
|
13807
14461
|
}
|
|
13808
14462
|
const bytes = Buffer.from(response.base64, "base64");
|
|
13809
|
-
const resolved =
|
|
14463
|
+
const resolved = path6.resolve(outputPath);
|
|
13810
14464
|
try {
|
|
13811
|
-
|
|
13812
|
-
|
|
14465
|
+
fs4.mkdirSync(path6.dirname(resolved), { recursive: true });
|
|
14466
|
+
fs4.writeFileSync(resolved, bytes);
|
|
13813
14467
|
} catch (err2) {
|
|
13814
14468
|
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err2.message}` }) }] };
|
|
13815
14469
|
}
|
|
@@ -13842,9 +14496,9 @@ var RobloxStudioTools = class {
|
|
|
13842
14496
|
let bytes;
|
|
13843
14497
|
let sourceLabel;
|
|
13844
14498
|
if (source.path !== void 0) {
|
|
13845
|
-
const resolved =
|
|
14499
|
+
const resolved = path6.resolve(source.path);
|
|
13846
14500
|
try {
|
|
13847
|
-
bytes =
|
|
14501
|
+
bytes = fs4.readFileSync(resolved);
|
|
13848
14502
|
} catch (err2) {
|
|
13849
14503
|
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to read ${resolved}: ${err2.message}` }) }] };
|
|
13850
14504
|
}
|
|
@@ -13894,19 +14548,57 @@ var RobloxStudioTools = class {
|
|
|
13894
14548
|
}, tgt, instance_id);
|
|
13895
14549
|
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
13896
14550
|
}
|
|
14551
|
+
// Even native capture must wait while another call has fitted the simulator
|
|
14552
|
+
// or drawn markers. Otherwise it could observe temporary UI or dimensions.
|
|
13897
14553
|
async _captureViewportImage(instanceId, targetRole, format, quality, maxBytes = MAX_INLINE_IMAGE_BYTES) {
|
|
13898
|
-
|
|
13899
|
-
|
|
13900
|
-
|
|
13901
|
-
|
|
13902
|
-
|
|
14554
|
+
const previous = this.viewportCaptureQueues.get(instanceId) ?? Promise.resolve();
|
|
14555
|
+
const capture = previous.then(() => this._captureViewportImageNow(instanceId, targetRole, format, quality, maxBytes));
|
|
14556
|
+
const settled = capture.then(() => void 0, () => void 0);
|
|
14557
|
+
this.viewportCaptureQueues.set(instanceId, settled);
|
|
14558
|
+
try {
|
|
14559
|
+
return await capture;
|
|
14560
|
+
} finally {
|
|
14561
|
+
if (this.viewportCaptureQueues.get(instanceId) === settled)
|
|
14562
|
+
this.viewportCaptureQueues.delete(instanceId);
|
|
14563
|
+
}
|
|
14564
|
+
}
|
|
14565
|
+
async _captureViewportImageNow(instanceId, targetRole, format, quality, maxBytes = MAX_INLINE_IMAGE_BYTES) {
|
|
14566
|
+
const fmt = format === "png" ? "png" : "jpeg";
|
|
14567
|
+
const q = quality === void 0 ? 92 : Math.max(1, Math.min(100, Math.floor(quality)));
|
|
14568
|
+
let response = await this._callSingle("/api/capture-studio", { encoding: fmt === "png" ? "png" : "rgba8" }, targetRole, instanceId);
|
|
14569
|
+
const studioFastPathMissing = response.unavailable !== void 0 || (response.error?.includes("/api/capture-studio") ?? false);
|
|
14570
|
+
if (studioFastPathMissing) {
|
|
14571
|
+
if (targetRole.startsWith("client-")) {
|
|
14572
|
+
const begin = await this._callSingle("/api/capture-begin", {}, targetRole, instanceId);
|
|
14573
|
+
if (begin.error) {
|
|
14574
|
+
return { success: false, error: begin.error };
|
|
14575
|
+
}
|
|
14576
|
+
if (!begin.contentId) {
|
|
14577
|
+
return { success: false, error: "Screenshot capture failed: no content id returned from client." };
|
|
14578
|
+
}
|
|
14579
|
+
response = await this._callSingle("/api/capture-read", { contentId: begin.contentId }, "edit", instanceId);
|
|
14580
|
+
} else {
|
|
14581
|
+
response = await this._callSingle("/api/capture-screenshot", {}, "edit", instanceId);
|
|
13903
14582
|
}
|
|
13904
|
-
|
|
13905
|
-
|
|
14583
|
+
}
|
|
14584
|
+
let hostNote = "";
|
|
14585
|
+
let hostReason;
|
|
14586
|
+
try {
|
|
14587
|
+
hostReason = response.error ? `Studio's capture failed (${response.error})` : this._isUniformCaptureResponse(response) ? "Studio's CaptureService returned a blank (single-colour) frame" : void 0;
|
|
14588
|
+
} catch (error) {
|
|
14589
|
+
response = { ...response, error: `Could not decode Studio's screenshot: ${error instanceof Error ? error.message : String(error)}` };
|
|
14590
|
+
hostReason = `Studio's capture failed (${response.error})`;
|
|
14591
|
+
}
|
|
14592
|
+
if (hostReason !== void 0) {
|
|
14593
|
+
const host = await this._captureViewportFromHostWindow(instanceId, targetRole);
|
|
14594
|
+
if (host.success) {
|
|
14595
|
+
response = host.response;
|
|
14596
|
+
hostNote = ` Captured from the Studio window through the host OS because ${hostReason}.`;
|
|
14597
|
+
} else if (response.error) {
|
|
14598
|
+
response = { ...response, error: `${response.error} Host window capture also failed: ${host.error}.` };
|
|
14599
|
+
} else {
|
|
14600
|
+
hostNote = ` Warning: ${hostReason} and host window capture also failed (${host.error}), so this image may be blank.`;
|
|
13906
14601
|
}
|
|
13907
|
-
response = await this._callSingle("/api/capture-read", { contentId: begin.contentId }, "edit", instanceId);
|
|
13908
|
-
} else {
|
|
13909
|
-
response = await this._callSingle("/api/capture-screenshot", {}, "edit", instanceId);
|
|
13910
14602
|
}
|
|
13911
14603
|
if (response.error) {
|
|
13912
14604
|
let text = response.error;
|
|
@@ -13920,11 +14612,19 @@ var RobloxStudioTools = class {
|
|
|
13920
14612
|
if (w === void 0 || h === void 0) {
|
|
13921
14613
|
return { success: false, error: "Screenshot response missing dimensions." };
|
|
13922
14614
|
}
|
|
13923
|
-
|
|
13924
|
-
|
|
13925
|
-
|
|
13926
|
-
|
|
13927
|
-
|
|
14615
|
+
let buffer;
|
|
14616
|
+
let mimeType;
|
|
14617
|
+
if (response.encoding === "png") {
|
|
14618
|
+
if (!response.data) {
|
|
14619
|
+
return { success: false, error: "Screenshot response missing PNG data." };
|
|
14620
|
+
}
|
|
14621
|
+
buffer = Buffer.from(response.data, "base64");
|
|
14622
|
+
mimeType = "image/png";
|
|
14623
|
+
} else {
|
|
14624
|
+
const encoded = encodeImageFromRgbaResponse(response, fmt, q);
|
|
14625
|
+
buffer = encoded.buffer;
|
|
14626
|
+
mimeType = encoded.mimeType;
|
|
14627
|
+
}
|
|
13928
14628
|
let usedQ = q;
|
|
13929
14629
|
let note = "";
|
|
13930
14630
|
if (buffer.length > maxBytes) {
|
|
@@ -13949,7 +14649,7 @@ var RobloxStudioTools = class {
|
|
|
13949
14649
|
}
|
|
13950
14650
|
const nativeW = response.nativeWidth ?? w;
|
|
13951
14651
|
const nativeH = response.nativeHeight ?? h;
|
|
13952
|
-
const message = nativeW !== w || nativeH !== h ? `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}, downscaled from the ${nativeW}x${nativeH} viewport to fit transport limits. For simulate_mouse_input, multiply x read off this image by ${(nativeW / w).toFixed(4)} and y by ${(nativeH / h).toFixed(4)} to get viewport pixel coordinates ((0,0) at the top-left).` : `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}. For simulate_mouse_input, x/y are pixel coordinates in this exact image with (0,0) at the top-left; it is not downscaled, so use coordinates as you read them off the image
|
|
14652
|
+
const message = (nativeW !== w || nativeH !== h ? `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}, downscaled from the ${nativeW}x${nativeH} viewport to fit transport limits. For simulate_mouse_input, multiply x read off this image by ${(nativeW / w).toFixed(4)} and y by ${(nativeH / h).toFixed(4)} to get viewport pixel coordinates ((0,0) at the top-left).` : `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}. For simulate_mouse_input, x/y are pixel coordinates in this exact image with (0,0) at the top-left; it is not downscaled, so use coordinates as you read them off the image.`) + hostNote;
|
|
13953
14653
|
return {
|
|
13954
14654
|
success: true,
|
|
13955
14655
|
width: w,
|
|
@@ -13962,7 +14662,153 @@ var RobloxStudioTools = class {
|
|
|
13962
14662
|
message
|
|
13963
14663
|
};
|
|
13964
14664
|
}
|
|
14665
|
+
_isUniformCaptureResponse(response) {
|
|
14666
|
+
if (!response.data || !response.width || !response.height)
|
|
14667
|
+
return false;
|
|
14668
|
+
const data = Buffer.from(response.data, "base64");
|
|
14669
|
+
if (response.encoding === "png")
|
|
14670
|
+
return isUniformPng(data);
|
|
14671
|
+
return isUniformFrame(data, response.width, response.height);
|
|
14672
|
+
}
|
|
14673
|
+
_hostCaptureTitleHint(instanceId) {
|
|
14674
|
+
const peers = this.bridge.getPeers().filter((peer) => peer.instanceId === instanceId);
|
|
14675
|
+
const edit = peers.find((peer) => peer.role === "edit") ?? peers[0];
|
|
14676
|
+
const name = edit?.dataModelName || edit?.placeName;
|
|
14677
|
+
return name ? name : void 0;
|
|
14678
|
+
}
|
|
14679
|
+
async _callViewportMarkers(action, targetRole, instanceId, captureId) {
|
|
14680
|
+
return await this._callSingle("/api/capture-markers", { action, ...captureId ? { captureId } : {} }, targetRole, instanceId);
|
|
14681
|
+
}
|
|
14682
|
+
// Always restore the simulator, including failures and cached window grabs.
|
|
14683
|
+
async _captureViewportFromHostWindow(instanceId, targetRole) {
|
|
14684
|
+
let prepared;
|
|
14685
|
+
let result;
|
|
14686
|
+
try {
|
|
14687
|
+
prepared = await this._callViewportMarkers("prepare", targetRole, instanceId);
|
|
14688
|
+
if (prepared.error || !prepared.captureId || !prepared.viewportWidth || !prepared.viewportHeight) {
|
|
14689
|
+
result = { success: false, error: prepared.error ?? "the Studio plugin could not prepare host capture (update the plugin)" };
|
|
14690
|
+
} else {
|
|
14691
|
+
if (prepared.viewportChanged)
|
|
14692
|
+
this.hostViewportRects.delete(`${instanceId}|${targetRole}`);
|
|
14693
|
+
result = await this._capturePreparedViewportFromHostWindow(instanceId, targetRole, prepared);
|
|
14694
|
+
}
|
|
14695
|
+
} catch (error) {
|
|
14696
|
+
result = { success: false, error: `host capture failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
14697
|
+
} finally {
|
|
14698
|
+
if (prepared?.captureId) {
|
|
14699
|
+
try {
|
|
14700
|
+
const restored = await this._callViewportMarkers("finish", targetRole, instanceId, prepared.captureId);
|
|
14701
|
+
if (restored.error)
|
|
14702
|
+
result = { success: false, error: `could not restore the Studio viewport: ${restored.error}` };
|
|
14703
|
+
else if (restored.stale || !restored.success)
|
|
14704
|
+
result = { success: false, error: "the host capture transaction expired or lost ownership; retry the capture" };
|
|
14705
|
+
} catch (error) {
|
|
14706
|
+
result = { success: false, error: `could not restore the Studio viewport: ${error instanceof Error ? error.message : String(error)}` };
|
|
14707
|
+
}
|
|
14708
|
+
}
|
|
14709
|
+
}
|
|
14710
|
+
if (!result.success)
|
|
14711
|
+
this.hostViewportRects.delete(`${instanceId}|${targetRole}`);
|
|
14712
|
+
return result;
|
|
14713
|
+
}
|
|
14714
|
+
// Grabs the Studio window through the host OS and crops it to the viewport
|
|
14715
|
+
// of `targetRole`. The plugin pins magenta markers to the viewport corners
|
|
14716
|
+
// so the crop is exact regardless of Studio's dock layout or DPI scale; the
|
|
14717
|
+
// located rect is cached briefly so repeated captures cost one window grab.
|
|
14718
|
+
async _capturePreparedViewportFromHostWindow(instanceId, targetRole, prepared) {
|
|
14719
|
+
const titleHint = this._hostCaptureTitleHint(instanceId);
|
|
14720
|
+
const cacheKey = `${instanceId}|${targetRole}`;
|
|
14721
|
+
const cached = this.hostViewportRects.get(cacheKey);
|
|
14722
|
+
let sizeInfo;
|
|
14723
|
+
try {
|
|
14724
|
+
sizeInfo = await this._callViewportMarkers("query", targetRole, instanceId, prepared.captureId);
|
|
14725
|
+
} catch (error) {
|
|
14726
|
+
return { success: false, error: `viewport markers unavailable: ${error instanceof Error ? error.message : String(error)}` };
|
|
14727
|
+
}
|
|
14728
|
+
if (sizeInfo.error || !sizeInfo.viewportWidth || !sizeInfo.viewportHeight) {
|
|
14729
|
+
return {
|
|
14730
|
+
success: false,
|
|
14731
|
+
error: sizeInfo.error ?? "the Studio plugin did not report a viewport size (update the plugin to a build with /api/capture-markers)"
|
|
14732
|
+
};
|
|
14733
|
+
}
|
|
14734
|
+
const viewportWidth = prepared.viewportWidth ?? sizeInfo.viewportWidth;
|
|
14735
|
+
const viewportHeight = prepared.viewportHeight ?? sizeInfo.viewportHeight;
|
|
14736
|
+
if (cached && Date.now() - cached.cachedAt < HOST_VIEWPORT_RECT_TTL_MS && cached.viewportWidth === viewportWidth && cached.viewportHeight === viewportHeight) {
|
|
14737
|
+
const grab = await this.hostWindowCapture(titleHint);
|
|
14738
|
+
if (!grab.ok)
|
|
14739
|
+
return { success: false, error: grab.error };
|
|
14740
|
+
if (grab.capture.width === cached.windowWidth && grab.capture.height === cached.windowHeight) {
|
|
14741
|
+
return {
|
|
14742
|
+
success: true,
|
|
14743
|
+
response: this._hostResponseFromCrop(grab.capture.rgba, grab.capture.width, grab.capture.height, cached.rect, viewportWidth, viewportHeight)
|
|
14744
|
+
};
|
|
14745
|
+
}
|
|
14746
|
+
this.hostViewportRects.delete(cacheKey);
|
|
14747
|
+
}
|
|
14748
|
+
let shown;
|
|
14749
|
+
try {
|
|
14750
|
+
shown = await this._callViewportMarkers("show", targetRole, instanceId, prepared.captureId);
|
|
14751
|
+
} catch (error) {
|
|
14752
|
+
return { success: false, error: `could not draw viewport markers: ${error instanceof Error ? error.message : String(error)}` };
|
|
14753
|
+
}
|
|
14754
|
+
if (shown.error)
|
|
14755
|
+
return { success: false, error: `could not draw viewport markers: ${shown.error}` };
|
|
14756
|
+
const markerSize = shown.markerSize ?? 12;
|
|
14757
|
+
let markerGrab;
|
|
14758
|
+
let hidden;
|
|
14759
|
+
try {
|
|
14760
|
+
markerGrab = await this.hostWindowCapture(titleHint);
|
|
14761
|
+
} finally {
|
|
14762
|
+
hidden = await this._callViewportMarkers("hide", targetRole, instanceId, prepared.captureId);
|
|
14763
|
+
}
|
|
14764
|
+
if (hidden.error)
|
|
14765
|
+
return { success: false, error: `could not hide viewport markers: ${hidden.error}` };
|
|
14766
|
+
if (!markerGrab.ok)
|
|
14767
|
+
return { success: false, error: markerGrab.error };
|
|
14768
|
+
const located = findViewportRect(markerGrab.capture.rgba, markerGrab.capture.width, markerGrab.capture.height, {
|
|
14769
|
+
viewportWidth: shown.viewportWidth ?? viewportWidth,
|
|
14770
|
+
viewportHeight: shown.viewportHeight ?? viewportHeight,
|
|
14771
|
+
markerSize
|
|
14772
|
+
});
|
|
14773
|
+
if ("error" in located)
|
|
14774
|
+
return { success: false, error: located.error };
|
|
14775
|
+
this.hostViewportRects.set(cacheKey, {
|
|
14776
|
+
rect: located.rect,
|
|
14777
|
+
windowWidth: markerGrab.capture.width,
|
|
14778
|
+
windowHeight: markerGrab.capture.height,
|
|
14779
|
+
viewportWidth,
|
|
14780
|
+
viewportHeight,
|
|
14781
|
+
cachedAt: Date.now()
|
|
14782
|
+
});
|
|
14783
|
+
const cleanGrab = await this.hostWindowCapture(titleHint);
|
|
14784
|
+
if (!cleanGrab.ok)
|
|
14785
|
+
return { success: false, error: cleanGrab.error };
|
|
14786
|
+
if (cleanGrab.capture.width !== markerGrab.capture.width || cleanGrab.capture.height !== markerGrab.capture.height) {
|
|
14787
|
+
this.hostViewportRects.delete(cacheKey);
|
|
14788
|
+
return { success: false, error: "the Studio window was resized while locating the viewport; retry the capture" };
|
|
14789
|
+
}
|
|
14790
|
+
return {
|
|
14791
|
+
success: true,
|
|
14792
|
+
response: this._hostResponseFromCrop(cleanGrab.capture.rgba, cleanGrab.capture.width, cleanGrab.capture.height, located.rect, viewportWidth, viewportHeight)
|
|
14793
|
+
};
|
|
14794
|
+
}
|
|
14795
|
+
_hostResponseFromCrop(rgba, width, height, rect, viewportWidth, viewportHeight) {
|
|
14796
|
+
const cropped = cropToViewport(rgba, width, height, rect, viewportWidth, viewportHeight);
|
|
14797
|
+
return {
|
|
14798
|
+
success: true,
|
|
14799
|
+
encoding: "rgba8",
|
|
14800
|
+
source: "host-window",
|
|
14801
|
+
width: cropped.width,
|
|
14802
|
+
height: cropped.height,
|
|
14803
|
+
nativeWidth: viewportWidth,
|
|
14804
|
+
nativeHeight: viewportHeight,
|
|
14805
|
+
data: cropped.rgba.toString("base64")
|
|
14806
|
+
};
|
|
14807
|
+
}
|
|
13965
14808
|
async captureScreenshot(instance_id, format, quality) {
|
|
14809
|
+
const refresh = this.bridge.refreshTopologyForRouting();
|
|
14810
|
+
if (refresh)
|
|
14811
|
+
await refresh;
|
|
13966
14812
|
const { instanceId, clientRole } = this._resolveRuntime(instance_id);
|
|
13967
14813
|
const capture = await this._captureViewportImage(instanceId, clientRole ?? "edit", format, quality);
|
|
13968
14814
|
if (!capture.success) {
|
|
@@ -14160,6 +15006,13 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
|
|
|
14160
15006
|
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
14161
15007
|
body: JSON.stringify({ groupId })
|
|
14162
15008
|
});
|
|
15009
|
+
if (response.status === 409) {
|
|
15010
|
+
const refusal = await response.json().catch(() => void 0);
|
|
15011
|
+
if (refusal && typeof refusal === "object" && !Array.isArray(refusal) && "success" in refusal && refusal.success === false && "error" in refusal && refusal.error === "multiplayer_group_in_use" && "groupId" in refusal && refusal.groupId === groupId) {
|
|
15012
|
+
throw new MultiplayerGroupInUseError(groupId);
|
|
15013
|
+
}
|
|
15014
|
+
throw new Error("Proxy Multiplayer Group removal returned an invalid refusal.");
|
|
15015
|
+
}
|
|
14163
15016
|
if (!response.ok) {
|
|
14164
15017
|
const body = await response.text().catch(() => "");
|
|
14165
15018
|
throw new Error(`Proxy Multiplayer Group removal failed (${response.status}): ${body || response.statusText}`);
|
|
@@ -16495,10 +17348,10 @@ var getReadOnlyTools = () => TOOL_DEFINITIONS.filter((t) => t.category === "read
|
|
|
16495
17348
|
|
|
16496
17349
|
// ../core/dist/install-plugin-helpers.js
|
|
16497
17350
|
var import_saxes = __toESM(require_saxes(), 1);
|
|
16498
|
-
import { existsSync as
|
|
17351
|
+
import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync7, renameSync, rmSync as rmSync3, rmdirSync, statSync as statSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
16499
17352
|
import { execSync } from "child_process";
|
|
16500
17353
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
16501
|
-
import { basename as basename3, join as
|
|
17354
|
+
import { basename as basename3, join as join6 } from "path";
|
|
16502
17355
|
import { homedir as homedir4 } from "os";
|
|
16503
17356
|
import { isUtf8 } from "buffer";
|
|
16504
17357
|
var DEFAULT_PLUGIN_PORT = 58741;
|
|
@@ -16550,15 +17403,15 @@ function getPluginsFolder() {
|
|
|
16550
17403
|
if (process.env.MCP_PLUGINS_DIR)
|
|
16551
17404
|
return process.env.MCP_PLUGINS_DIR;
|
|
16552
17405
|
if (process.platform === "win32") {
|
|
16553
|
-
return
|
|
17406
|
+
return join6(process.env.LOCALAPPDATA || join6(homedir4(), "AppData", "Local"), "Roblox", "Plugins");
|
|
16554
17407
|
}
|
|
16555
17408
|
if (isWSL()) {
|
|
16556
17409
|
const localAppData = getWindowsUserLocalAppData();
|
|
16557
17410
|
if (localAppData)
|
|
16558
|
-
return
|
|
17411
|
+
return join6(localAppData.linuxPath, "Roblox", "Plugins");
|
|
16559
17412
|
console.warn("[install-plugin] WSL detected but could not resolve Windows %LOCALAPPDATA%. Falling back to ~/Documents/Roblox/Plugins/ - you will likely need to copy the rbxmx to /mnt/c/Users/<you>/AppData/Local/Roblox/Plugins/ manually. Set MCP_PLUGINS_DIR to skip detection.");
|
|
16560
17413
|
}
|
|
16561
|
-
return
|
|
17414
|
+
return join6(homedir4(), "Documents", "Roblox", "Plugins");
|
|
16562
17415
|
}
|
|
16563
17416
|
var STALE_TEST_PLUGINS_DIRECTORY = "RsmcpIsolatedPlugins";
|
|
16564
17417
|
function pluginDirectorySettingPattern() {
|
|
@@ -16568,12 +17421,12 @@ function escapeXmlText(value) {
|
|
|
16568
17421
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
16569
17422
|
}
|
|
16570
17423
|
function repairStaleStudioPluginDirectorySetting({ settingsPath, studioPluginsDirectory }) {
|
|
16571
|
-
if (!
|
|
17424
|
+
if (!existsSync7(settingsPath))
|
|
16572
17425
|
return false;
|
|
16573
17426
|
if (studioPluginsDirectory === "") {
|
|
16574
17427
|
throw new Error("studioPluginsDirectory must be a non-empty path.");
|
|
16575
17428
|
}
|
|
16576
|
-
const contents =
|
|
17429
|
+
const contents = readFileSync7(settingsPath, "utf8");
|
|
16577
17430
|
const matches = [...contents.matchAll(pluginDirectorySettingPattern())];
|
|
16578
17431
|
if (matches.length !== 1) {
|
|
16579
17432
|
throw new Error(`Expected exactly one Studio.PluginsDir QDir in ${settingsPath}; found ${matches.length}.`);
|
|
@@ -16590,7 +17443,7 @@ function repairStaleStudioPluginDirectorySetting({ settingsPath, studioPluginsDi
|
|
|
16590
17443
|
});
|
|
16591
17444
|
renameSync(temporaryPath, settingsPath);
|
|
16592
17445
|
} finally {
|
|
16593
|
-
|
|
17446
|
+
rmSync3(temporaryPath, { force: true });
|
|
16594
17447
|
}
|
|
16595
17448
|
return true;
|
|
16596
17449
|
}
|
|
@@ -16601,16 +17454,16 @@ function repairDefaultStudioPluginDirectorySetting(pluginsFolder, log) {
|
|
|
16601
17454
|
let defaultPluginsFolder;
|
|
16602
17455
|
let studioPluginsDirectory;
|
|
16603
17456
|
if (process.platform === "win32") {
|
|
16604
|
-
const localAppData = process.env.LOCALAPPDATA ||
|
|
16605
|
-
defaultPluginsFolder =
|
|
16606
|
-
settingsPath =
|
|
17457
|
+
const localAppData = process.env.LOCALAPPDATA || join6(homedir4(), "AppData", "Local");
|
|
17458
|
+
defaultPluginsFolder = join6(localAppData, "Roblox", "Plugins");
|
|
17459
|
+
settingsPath = join6(localAppData, "Roblox", "GlobalSettings_13.xml");
|
|
16607
17460
|
studioPluginsDirectory = defaultPluginsFolder.replaceAll("\\", "/");
|
|
16608
17461
|
} else if (isWSL()) {
|
|
16609
17462
|
const localAppData = getWindowsUserLocalAppData();
|
|
16610
17463
|
if (!localAppData)
|
|
16611
17464
|
return;
|
|
16612
|
-
defaultPluginsFolder =
|
|
16613
|
-
settingsPath =
|
|
17465
|
+
defaultPluginsFolder = join6(localAppData.linuxPath, "Roblox", "Plugins");
|
|
17466
|
+
settingsPath = join6(localAppData.linuxPath, "Roblox", "GlobalSettings_13.xml");
|
|
16614
17467
|
studioPluginsDirectory = [
|
|
16615
17468
|
localAppData.windowsPath.replace(/[\\/]+$/u, ""),
|
|
16616
17469
|
"Roblox",
|
|
@@ -16626,8 +17479,8 @@ function repairDefaultStudioPluginDirectorySetting(pluginsFolder, log) {
|
|
|
16626
17479
|
}
|
|
16627
17480
|
}
|
|
16628
17481
|
function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
|
|
16629
|
-
const otherDest =
|
|
16630
|
-
if (!
|
|
17482
|
+
const otherDest = join6(pluginsFolder, otherAssetName);
|
|
17483
|
+
if (!existsSync7(otherDest))
|
|
16631
17484
|
return;
|
|
16632
17485
|
if (replace) {
|
|
16633
17486
|
try {
|
|
@@ -16652,11 +17505,11 @@ function pluginInstallLockError(lockPath, reason) {
|
|
|
16652
17505
|
return new Error(`${reason}: ${lockPath}. If no installer or lock recovery is running, remove that lock directory and retry.`);
|
|
16653
17506
|
}
|
|
16654
17507
|
function readPluginInstallLockOwner(lockPath) {
|
|
16655
|
-
const ownerPath =
|
|
17508
|
+
const ownerPath = join6(lockPath, "owner.json");
|
|
16656
17509
|
if (!lstatSync(lockPath).isDirectory() || !lstatSync(ownerPath).isFile()) {
|
|
16657
17510
|
throw new Error("Lock and owner metadata must be a directory and regular file");
|
|
16658
17511
|
}
|
|
16659
|
-
const owner = JSON.parse(
|
|
17512
|
+
const owner = JSON.parse(readFileSync7(ownerPath, "utf8"));
|
|
16660
17513
|
if (owner === null || typeof owner !== "object" || !("pid" in owner) || typeof owner.pid !== "number" || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 || owner.pid > 2147483647 || !("token" in owner) || typeof owner.token !== "string" || !/^[a-zA-Z0-9-]{1,128}$/.test(owner.token) || !("createdAt" in owner) || typeof owner.createdAt !== "number" || !Number.isFinite(owner.createdAt) || owner.createdAt < 0) {
|
|
16661
17514
|
throw new Error("Invalid lock owner metadata");
|
|
16662
17515
|
}
|
|
@@ -16682,7 +17535,7 @@ function recoverPluginInstallLock(lockPath, warn) {
|
|
|
16682
17535
|
throw pluginInstallLockError(lockPath, `Cannot safely read plugin installation lock owner (${error})`);
|
|
16683
17536
|
}
|
|
16684
17537
|
assertPluginInstallOwnerExited(lockPath, owner);
|
|
16685
|
-
const claimPath =
|
|
17538
|
+
const claimPath = join6(lockPath, `recovery-${owner.token}`);
|
|
16686
17539
|
try {
|
|
16687
17540
|
mkdirSync4(claimPath);
|
|
16688
17541
|
} catch (error) {
|
|
@@ -16715,14 +17568,14 @@ function recoverPluginInstallLock(lockPath, warn) {
|
|
|
16715
17568
|
}
|
|
16716
17569
|
}
|
|
16717
17570
|
try {
|
|
16718
|
-
|
|
17571
|
+
rmSync3(abandonedPath, { recursive: true, force: true });
|
|
16719
17572
|
} catch (error) {
|
|
16720
17573
|
warn(`[install-plugin] Could not clean abandoned install lock ${abandonedPath}: ${error}`);
|
|
16721
17574
|
}
|
|
16722
17575
|
}
|
|
16723
17576
|
function acquirePluginInstallLock(pluginsFolder, warn) {
|
|
16724
|
-
const lockPath =
|
|
16725
|
-
const ownerPath =
|
|
17577
|
+
const lockPath = join6(pluginsFolder, PLUGIN_INSTALL_LOCK_NAME);
|
|
17578
|
+
const ownerPath = join6(lockPath, "owner.json");
|
|
16726
17579
|
const owner = {
|
|
16727
17580
|
pid: process.pid,
|
|
16728
17581
|
token: randomUUID5(),
|
|
@@ -16750,7 +17603,7 @@ function acquirePluginInstallLock(pluginsFolder, warn) {
|
|
|
16750
17603
|
});
|
|
16751
17604
|
} catch (error) {
|
|
16752
17605
|
try {
|
|
16753
|
-
|
|
17606
|
+
rmSync3(lockPath, { recursive: true, force: true });
|
|
16754
17607
|
} catch (cleanupError) {
|
|
16755
17608
|
warn(`[install-plugin] Could not clean failed install lock ${lockPath}: ${cleanupError}`);
|
|
16756
17609
|
}
|
|
@@ -16760,7 +17613,7 @@ function acquirePluginInstallLock(pluginsFolder, warn) {
|
|
|
16760
17613
|
try {
|
|
16761
17614
|
const currentOwner = readPluginInstallLockOwner(lockPath);
|
|
16762
17615
|
if (currentOwner.pid === owner.pid && currentOwner.token === owner.token) {
|
|
16763
|
-
|
|
17616
|
+
rmSync3(lockPath, { recursive: true, force: true });
|
|
16764
17617
|
}
|
|
16765
17618
|
} catch (error) {
|
|
16766
17619
|
if (errorCode(error) !== "ENOENT") {
|
|
@@ -16852,11 +17705,11 @@ function assertPluginAssetIdentity(source, assetName, expectedVersion, expectedV
|
|
|
16852
17705
|
}
|
|
16853
17706
|
}
|
|
16854
17707
|
function installedFileMatches(source, destination) {
|
|
16855
|
-
if (!
|
|
17708
|
+
if (!existsSync7(destination))
|
|
16856
17709
|
return false;
|
|
16857
17710
|
if (!lstatSync(destination).isFile())
|
|
16858
17711
|
return false;
|
|
16859
|
-
const installed =
|
|
17712
|
+
const installed = readFileSync7(destination);
|
|
16860
17713
|
return installed.length === source.length && installed.equals(source);
|
|
16861
17714
|
}
|
|
16862
17715
|
function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source, expectedVersion, expectedVariant, rawPort, replaceVariant = true, log = console.log, warn = console.warn }) {
|
|
@@ -16868,12 +17721,12 @@ function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source,
|
|
|
16868
17721
|
const configured = configurePluginAssetForPort(source, rawPort);
|
|
16869
17722
|
assertPluginAssetIdentity(configured, assetName, expectedVersion, expectedVariant);
|
|
16870
17723
|
mkdirSync4(pluginsFolder, { recursive: true });
|
|
16871
|
-
const destination =
|
|
17724
|
+
const destination = join6(pluginsFolder, assetName);
|
|
16872
17725
|
const releaseLock = acquirePluginInstallLock(pluginsFolder, warn);
|
|
16873
17726
|
try {
|
|
16874
17727
|
let installed = false;
|
|
16875
17728
|
if (!installedFileMatches(configured, destination)) {
|
|
16876
|
-
const staged =
|
|
17729
|
+
const staged = join6(pluginsFolder, `.${assetName}.${process.pid}.${randomUUID5()}.tmp`);
|
|
16877
17730
|
try {
|
|
16878
17731
|
writeFileSync3(staged, configured, { flag: "wx" });
|
|
16879
17732
|
renameSync(staged, destination);
|
|
@@ -16906,8 +17759,8 @@ function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source,
|
|
|
16906
17759
|
import { createRequire } from "module";
|
|
16907
17760
|
|
|
16908
17761
|
// src/install-plugin.ts
|
|
16909
|
-
import { existsSync as
|
|
16910
|
-
import { dirname as dirname5, join as
|
|
17762
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
17763
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
16911
17764
|
import { fileURLToPath } from "url";
|
|
16912
17765
|
import { get } from "https";
|
|
16913
17766
|
var REPO = "chrrxs/robloxstudio-mcp";
|
|
@@ -16967,18 +17820,18 @@ async function fetchJson(url) {
|
|
|
16967
17820
|
function bundledAssetPath() {
|
|
16968
17821
|
const currentDir = dirname5(fileURLToPath(import.meta.url));
|
|
16969
17822
|
const candidates = [
|
|
16970
|
-
|
|
16971
|
-
|
|
17823
|
+
join7(currentDir, "..", "studio-plugin", ASSET_NAME),
|
|
17824
|
+
join7(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
|
|
16972
17825
|
];
|
|
16973
|
-
return candidates.find((candidate) =>
|
|
17826
|
+
return candidates.find((candidate) => existsSync8(candidate)) ?? null;
|
|
16974
17827
|
}
|
|
16975
17828
|
function resolvePluginAssetPath(sourcePath) {
|
|
16976
17829
|
if (sourcePath === void 0) return bundledAssetPath();
|
|
16977
|
-
return
|
|
17830
|
+
return existsSync8(sourcePath) ? sourcePath : null;
|
|
16978
17831
|
}
|
|
16979
17832
|
function packageVersion() {
|
|
16980
17833
|
const currentDir = dirname5(fileURLToPath(import.meta.url));
|
|
16981
|
-
const pkg = JSON.parse(
|
|
17834
|
+
const pkg = JSON.parse(readFileSync8(join7(currentDir, "..", "package.json"), "utf8"));
|
|
16982
17835
|
if (!pkg.version) {
|
|
16983
17836
|
throw new Error("Package version not found");
|
|
16984
17837
|
}
|
|
@@ -16998,7 +17851,7 @@ async function installBundledPlugin(options = {}) {
|
|
|
16998
17851
|
pluginsFolder: getPluginsFolder(),
|
|
16999
17852
|
assetName: ASSET_NAME,
|
|
17000
17853
|
otherAssetName: OTHER_VARIANT,
|
|
17001
|
-
source:
|
|
17854
|
+
source: readFileSync8(sourcePath),
|
|
17002
17855
|
expectedVersion: packageVersion(),
|
|
17003
17856
|
expectedVariant: "inspector",
|
|
17004
17857
|
replaceVariant,
|
|
@@ -17022,7 +17875,7 @@ async function installPlugin(options = {}) {
|
|
|
17022
17875
|
pluginsFolder: getPluginsFolder(),
|
|
17023
17876
|
assetName: ASSET_NAME,
|
|
17024
17877
|
otherAssetName: OTHER_VARIANT,
|
|
17025
|
-
source:
|
|
17878
|
+
source: readFileSync8(bundled),
|
|
17026
17879
|
expectedVersion: packageVersion(),
|
|
17027
17880
|
expectedVariant: "inspector",
|
|
17028
17881
|
replaceVariant,
|