flockbay 0.10.19 → 0.10.20

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.
@@ -614,6 +614,14 @@ async function main() {
614
614
  "Unreal Editor: Launch Project",
615
615
  "Launch Unreal Editor for a given .uproject (no auto-restart). If the editor later crashes or becomes unreachable, Flockbay will abort the current agent run and report it in the chat."
616
616
  );
617
+ forwardTool(
618
+ "unreal_editor_relaunch_last",
619
+ {
620
+ extraArgs: z.z.array(z.z.string()).optional().describe("Optional replacement UnrealEditor command-line args (advanced).")
621
+ },
622
+ "Unreal Editor: Relaunch Last Project",
623
+ "Relaunch the last Unreal project previously launched via unreal_editor_launch in this session (no auto-restart). Use this after a crash once you\u2019ve fixed files."
624
+ );
617
625
  forwardTool(
618
626
  "unreal_headless_screenshot",
619
627
  {
@@ -612,6 +612,14 @@ async function main() {
612
612
  "Unreal Editor: Launch Project",
613
613
  "Launch Unreal Editor for a given .uproject (no auto-restart). If the editor later crashes or becomes unreachable, Flockbay will abort the current agent run and report it in the chat."
614
614
  );
615
+ forwardTool(
616
+ "unreal_editor_relaunch_last",
617
+ {
618
+ extraArgs: z.array(z.string()).optional().describe("Optional replacement UnrealEditor command-line args (advanced).")
619
+ },
620
+ "Unreal Editor: Relaunch Last Project",
621
+ "Relaunch the last Unreal project previously launched via unreal_editor_launch in this session (no auto-restart). Use this after a crash once you\u2019ve fixed files."
622
+ );
615
623
  forwardTool(
616
624
  "unreal_headless_screenshot",
617
625
  {
@@ -1,14 +1,14 @@
1
1
  import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import chalk from 'chalk';
2
2
  import os, { homedir } from 'node:os';
3
3
  import { randomUUID, createCipheriv, randomBytes } from 'node:crypto';
4
- import { l as logger, p as projectPath, d as backoff, e as delay, R as RawJSONLinesSchema, c as configuration, f as readDaemonState, g as clearDaemonState, b as packageJson, r as readSettings, h as readCredentials, u as updateSettings, w as writeCredentials, i as unrealMcpPythonDir, j as acquireDaemonLock, k as writeDaemonState, m as ApiMachineClient, n as releaseDaemonLock, s as sendUnrealMcpTcpCommand, A as ApiClient, o as clearCredentials, q as clearMachineId, t as installUnrealMcpPluginToEngine, v as getLatestDaemonLog, x as normalizeServerUrlForNode } from './types-BQvaA3sv.mjs';
4
+ import { l as logger, p as projectPath, d as backoff, e as delay, R as RawJSONLinesSchema, c as configuration, f as readDaemonState, g as clearDaemonState, b as packageJson, r as readSettings, h as readCredentials, u as updateSettings, w as writeCredentials, i as unrealMcpPythonDir, j as acquireDaemonLock, k as writeDaemonState, m as ApiMachineClient, n as releaseDaemonLock, s as sendUnrealMcpTcpCommand, A as ApiClient, o as clearCredentials, q as clearMachineId, t as installUnrealMcpPluginToEngine, v as getLatestDaemonLog, x as normalizeServerUrlForNode } from './types-C4QeUggl.mjs';
5
5
  import { spawn, execFileSync, execSync } from 'node:child_process';
6
6
  import path, { resolve, join, dirname } from 'node:path';
7
7
  import { createInterface } from 'node:readline';
8
8
  import * as fs from 'node:fs';
9
9
  import fs__default, { existsSync, readFileSync, mkdirSync, readdirSync, accessSync, constants, statSync, createReadStream, writeFileSync, unlinkSync } from 'node:fs';
10
10
  import process$1 from 'node:process';
11
- import fs$1, { readFile, access as access$1, mkdir, stat, readdir } from 'node:fs/promises';
11
+ import fs$1, { readFile, access as access$1, mkdir, readdir, stat } from 'node:fs/promises';
12
12
  import fs$2, { watch, access } from 'fs/promises';
13
13
  import { useStdout, useInput, Box, Text, render } from 'ink';
14
14
  import React, { useState, useRef, useEffect, useCallback } from 'react';
@@ -7208,7 +7208,8 @@ async function startFlockbayServer(client, options) {
7208
7208
  lastReachableAtMs: 0,
7209
7209
  lastIssueAtMs: 0,
7210
7210
  lastIssueKey: "",
7211
- launched: null
7211
+ launched: null,
7212
+ lastLaunch: null
7212
7213
  };
7213
7214
  const emitIssue = (event) => {
7214
7215
  const key = `${event.kind}:${event.severity}:${event.message}`;
@@ -7233,6 +7234,93 @@ async function startFlockbayServer(client, options) {
7233
7234
  }
7234
7235
  }
7235
7236
  };
7237
+ const tailText = (text, maxChars) => {
7238
+ const t = String(text || "").trim();
7239
+ if (!t) return "";
7240
+ if (t.length <= maxChars) return t;
7241
+ return t.slice(t.length - maxChars);
7242
+ };
7243
+ const findLatestFile = async (dir, filter) => {
7244
+ try {
7245
+ const entries = await readdir(dir);
7246
+ let best = null;
7247
+ for (const name of entries) {
7248
+ if (!filter(name)) continue;
7249
+ const full = path.join(dir, name);
7250
+ let st;
7251
+ try {
7252
+ st = await stat(full);
7253
+ } catch {
7254
+ continue;
7255
+ }
7256
+ if (!st?.isFile?.()) continue;
7257
+ const mtimeMs = Number(st.mtimeMs || 0);
7258
+ if (!best || mtimeMs > best.mtimeMs) best = { path: full, mtimeMs };
7259
+ }
7260
+ return best?.path ?? null;
7261
+ } catch {
7262
+ return null;
7263
+ }
7264
+ };
7265
+ const findLatestCrashDir = async (projectRoot) => {
7266
+ const crashesDir = path.join(projectRoot, "Saved", "Crashes");
7267
+ try {
7268
+ const entries = await readdir(crashesDir);
7269
+ let best = null;
7270
+ for (const name of entries) {
7271
+ const full = path.join(crashesDir, name);
7272
+ let st;
7273
+ try {
7274
+ st = await stat(full);
7275
+ } catch {
7276
+ continue;
7277
+ }
7278
+ if (!st?.isDirectory?.()) continue;
7279
+ const mtimeMs = Number(st.mtimeMs || 0);
7280
+ if (!best || mtimeMs > best.mtimeMs) best = { path: full, mtimeMs };
7281
+ }
7282
+ return best?.path ?? null;
7283
+ } catch {
7284
+ return null;
7285
+ }
7286
+ };
7287
+ const readFileTail = async (filePath, maxBytes) => {
7288
+ try {
7289
+ const buf = await readFile(filePath);
7290
+ const slice = buf.length > maxBytes ? buf.subarray(buf.length - maxBytes) : buf;
7291
+ return slice.toString("utf8");
7292
+ } catch {
7293
+ return null;
7294
+ }
7295
+ };
7296
+ const gatherCrashDiagnosticsBestEffort = async (uprojectPath) => {
7297
+ const projectRoot = path.dirname(uprojectPath);
7298
+ const summary = [];
7299
+ const detail = { uprojectPath, projectRoot };
7300
+ const latestLog = await findLatestFile(path.join(projectRoot, "Saved", "Logs"), (n) => n.toLowerCase().endsWith(".log"));
7301
+ if (latestLog) {
7302
+ detail.projectLogPath = latestLog;
7303
+ const tail = await readFileTail(latestLog, 24e3);
7304
+ if (tail) {
7305
+ detail.projectLogTail = tailText(tail, 12e3);
7306
+ summary.push(`Latest log: ${latestLog}`);
7307
+ }
7308
+ }
7309
+ const latestCrashDir = await findLatestCrashDir(projectRoot);
7310
+ if (latestCrashDir) {
7311
+ detail.latestCrashDir = latestCrashDir;
7312
+ const crashContext = await findLatestFile(latestCrashDir, (n) => n.toLowerCase().includes("crashcontext") && n.toLowerCase().endsWith(".xml"));
7313
+ if (crashContext) {
7314
+ detail.crashContextPath = crashContext;
7315
+ const tail = await readFileTail(crashContext, 24e3);
7316
+ if (tail) {
7317
+ detail.crashContextTail = tailText(tail, 12e3);
7318
+ summary.push(`CrashContext: ${crashContext}`);
7319
+ }
7320
+ }
7321
+ }
7322
+ return { detail, summary };
7323
+ };
7236
7324
  const getUnrealEditorExe = (engineRoot) => {
7237
7325
  const root = engineRoot.trim().replace(/[\\/]+$/, "");
7238
7326
  if (process.platform === "darwin") {
@@ -7288,7 +7376,7 @@ ${res.stderr}`;
7288
7376
  kind: "unreachable",
7289
7377
  severity: "warning",
7290
7378
  detectedAtMs: now,
7291
- message: "Unreal Editor is no longer reachable (it was reachable earlier). It may have crashed or been closed.",
7379
+ message: "Unreal Editor is no longer reachable (it was reachable earlier). It may have crashed or been closed.\nNext: fix the issue, then relaunch via unreal_editor_relaunch_last (if you launched from this session) or unreal_editor_launch.",
7292
7380
  detail: {
7293
7381
  lastReachableAtMs: state.lastReachableAtMs
7294
7382
  }
@@ -7320,20 +7408,35 @@ ${res.stderr}`;
7320
7408
  engineRoot,
7321
7409
  startedAtMs: Date.now()
7322
7410
  };
7411
+ state.lastLaunch = {
7412
+ uprojectPath,
7413
+ engineRoot,
7414
+ extraArgs,
7415
+ startedAtMs: Date.now()
7416
+ };
7323
7417
  child.on("exit", (code, signal) => {
7324
- const now = Date.now();
7325
- const exitCode = typeof code === "number" ? code : null;
7326
- const sig = typeof signal === "string" ? signal : null;
7327
- const isCrash = sig !== null || exitCode !== null && exitCode !== 0;
7328
- state.launched = null;
7329
- if (!isCrash) return;
7330
- emitIssue({
7331
- kind: "process_exit",
7332
- severity: "crash",
7333
- detectedAtMs: now,
7334
- message: `Unreal Editor process exited unexpectedly (code=${exitCode ?? "null"} signal=${sig ?? "null"}).`,
7335
- detail: { exitCode, signal: sig, pid, uprojectPath }
7336
- });
7418
+ void (async () => {
7419
+ const now = Date.now();
7420
+ const exitCode = typeof code === "number" ? code : null;
7421
+ const sig = typeof signal === "string" ? signal : null;
7422
+ const isCrash = sig !== null || exitCode !== null && exitCode !== 0;
7423
+ state.launched = null;
7424
+ if (!isCrash) return;
7425
+ const diag = await gatherCrashDiagnosticsBestEffort(uprojectPath).catch(() => ({ detail: {}, summary: [] }));
7426
+ const msgParts = [
7427
+ `Unreal Editor process exited unexpectedly (code=${exitCode ?? "null"} signal=${sig ?? "null"}).`,
7428
+ `Project: ${uprojectPath}`,
7429
+ ...diag.summary,
7430
+ `Next: fix the issue, then relaunch via unreal_editor_relaunch_last (or unreal_editor_launch).`
7431
+ ];
7432
+ emitIssue({
7433
+ kind: "process_exit",
7434
+ severity: "crash",
7435
+ detectedAtMs: now,
7436
+ message: msgParts.filter(Boolean).join("\n"),
7437
+ detail: { exitCode, signal: sig, pid, uprojectPath, ...diag.detail }
7438
+ });
7439
+ })();
7337
7440
  });
7338
7441
  child.on("error", (err) => {
7339
7442
  const now = Date.now();
@@ -7342,7 +7445,9 @@ ${res.stderr}`;
7342
7445
  kind: "process_exit",
7343
7446
  severity: "crash",
7344
7447
  detectedAtMs: now,
7345
- message: `Failed to launch Unreal Editor: ${err instanceof Error ? err.message : String(err)}`,
7448
+ message: `Failed to launch Unreal Editor: ${err instanceof Error ? err.message : String(err)}
7449
+ Project: ${uprojectPath}
7450
+ Next: fix the issue, then relaunch via unreal_editor_relaunch_last (or unreal_editor_launch).`,
7346
7451
  detail: { pid, uprojectPath }
7347
7452
  });
7348
7453
  });
@@ -7352,6 +7457,12 @@ ${res.stderr}`;
7352
7457
  noteUnrealActivity,
7353
7458
  noteUnrealReachable,
7354
7459
  launchEditor,
7460
+ relaunchLast: async (extraArgs) => {
7461
+ const last = state.lastLaunch;
7462
+ if (!last) throw new Error("No known prior Unreal launch in this session. Use unreal_editor_launch with an explicit uprojectPath.");
7463
+ const mergedArgs = Array.isArray(extraArgs) && extraArgs.length > 0 ? extraArgs.filter((a) => typeof a === "string" && a.trim()) : last.extraArgs;
7464
+ return launchEditor({ uprojectPath: last.uprojectPath, engineRoot: last.engineRoot, extraArgs: mergedArgs });
7465
+ },
7355
7466
  stop: () => {
7356
7467
  try {
7357
7468
  interval.unref();
@@ -8858,6 +8969,32 @@ ${String(st.stdout || "").trim()}`
8858
8969
  isError: false
8859
8970
  };
8860
8971
  }));
8972
+ mcp.registerTool("unreal_editor_relaunch_last", {
8973
+ title: "Unreal Editor: Relaunch Last Project",
8974
+ description: "Relaunch the last Unreal project previously launched via unreal_editor_launch in this session (no auto-restart). Use this after a crash once you\u2019ve fixed files. If it crashes again, Flockbay will abort and report again.",
8975
+ inputSchema: {
8976
+ extraArgs: z.array(z.string()).optional().describe("Optional replacement UnrealEditor command-line args (advanced).")
8977
+ }
8978
+ }, async (args) => runWithMcpToolCard("unreal_editor_relaunch_last", args, async () => {
8979
+ const extraArgs = Array.isArray(args?.extraArgs) ? args.extraArgs : void 0;
8980
+ unrealEditorSupervisor.noteUnrealActivity();
8981
+ try {
8982
+ const launched = await unrealEditorSupervisor.relaunchLast(extraArgs);
8983
+ return {
8984
+ content: [
8985
+ { type: "text", text: `Relaunched Unreal Editor (last project).` },
8986
+ { type: "text", text: JSON.stringify({ pid: launched.pid, exePath: launched.exePath }, null, 2) },
8987
+ { type: "text", text: "Next: wait for the editor to finish loading, then re-run UnrealMCP tools." }
8988
+ ],
8989
+ isError: false
8990
+ };
8991
+ } catch (err) {
8992
+ return {
8993
+ content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
8994
+ isError: true
8995
+ };
8996
+ }
8997
+ }));
8861
8998
  mcp.registerTool("unreal_build_project", {
8862
8999
  title: "Unreal Build Project (UBT)",
8863
9000
  description: "Build the project via Unreal Build Tool (via Engine/Build/BatchFiles/Build.*). Returns structured errors (file/line) and a log path for deep debugging.",
@@ -10283,6 +10420,7 @@ Fix: ${res.hint}` : "";
10283
10420
  "unreal_headless_screenshot",
10284
10421
  "unreal_latest_screenshots",
10285
10422
  "unreal_editor_launch",
10423
+ "unreal_editor_relaunch_last",
10286
10424
  "unreal_build_project",
10287
10425
  "unreal_mcp_command",
10288
10426
  "unreal_mcp_list_capabilities",
@@ -12064,7 +12202,7 @@ ${engineRoot}`, {
12064
12202
  } else if (subcommand === "codex") {
12065
12203
  try {
12066
12204
  await chdirToNearestUprojectRootIfPresent();
12067
- const { runCodex } = await import('./runCodex-DwsaTF4s.mjs');
12205
+ const { runCodex } = await import('./runCodex-Biis9GFw.mjs');
12068
12206
  let startedBy = void 0;
12069
12207
  let sessionId = void 0;
12070
12208
  for (let i = 1; i < args.length; i++) {
@@ -12159,7 +12297,7 @@ ${engineRoot}`, {
12159
12297
  }
12160
12298
  try {
12161
12299
  await chdirToNearestUprojectRootIfPresent();
12162
- const { runGemini } = await import('./runGemini-qA5dD13X.mjs');
12300
+ const { runGemini } = await import('./runGemini-BSH4b0wu.mjs');
12163
12301
  let startedBy = void 0;
12164
12302
  let sessionId = void 0;
12165
12303
  for (let i = 1; i < args.length; i++) {
@@ -3,7 +3,7 @@
3
3
  var chalk = require('chalk');
4
4
  var os = require('node:os');
5
5
  var node_crypto = require('node:crypto');
6
- var types = require('./types-CL_3YyS9.cjs');
6
+ var types = require('./types-BYHCKlu_.cjs');
7
7
  var node_child_process = require('node:child_process');
8
8
  var path = require('node:path');
9
9
  var node_readline = require('node:readline');
@@ -1272,7 +1272,7 @@ function buildDaemonSafeEnv(baseEnv, binPath) {
1272
1272
  env[pathKey] = [...prepend, ...existingParts].join(pathSep);
1273
1273
  return env;
1274
1274
  }
1275
- const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-BiUf5vLX.cjs', document.baseURI).href)));
1275
+ const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-D_mglYG0.cjs', document.baseURI).href)));
1276
1276
  const __dirname$1 = path.join(__filename$1, "..");
1277
1277
  function getGlobalClaudeVersion(claudeExecutable) {
1278
1278
  try {
@@ -7230,7 +7230,8 @@ async function startFlockbayServer(client, options) {
7230
7230
  lastReachableAtMs: 0,
7231
7231
  lastIssueAtMs: 0,
7232
7232
  lastIssueKey: "",
7233
- launched: null
7233
+ launched: null,
7234
+ lastLaunch: null
7234
7235
  };
7235
7236
  const emitIssue = (event) => {
7236
7237
  const key = `${event.kind}:${event.severity}:${event.message}`;
@@ -7255,6 +7256,93 @@ async function startFlockbayServer(client, options) {
7255
7256
  }
7256
7257
  }
7257
7258
  };
7259
+ const tailText = (text, maxChars) => {
7260
+ const t = String(text || "").trim();
7261
+ if (!t) return "";
7262
+ if (t.length <= maxChars) return t;
7263
+ return t.slice(t.length - maxChars);
7264
+ };
7265
+ const findLatestFile = async (dir, filter) => {
7266
+ try {
7267
+ const entries = await fs$2.readdir(dir);
7268
+ let best = null;
7269
+ for (const name of entries) {
7270
+ if (!filter(name)) continue;
7271
+ const full = path.join(dir, name);
7272
+ let st;
7273
+ try {
7274
+ st = await fs$2.stat(full);
7275
+ } catch {
7276
+ continue;
7277
+ }
7278
+ if (!st?.isFile?.()) continue;
7279
+ const mtimeMs = Number(st.mtimeMs || 0);
7280
+ if (!best || mtimeMs > best.mtimeMs) best = { path: full, mtimeMs };
7281
+ }
7282
+ return best?.path ?? null;
7283
+ } catch {
7284
+ return null;
7285
+ }
7286
+ };
7287
+ const findLatestCrashDir = async (projectRoot) => {
7288
+ const crashesDir = path.join(projectRoot, "Saved", "Crashes");
7289
+ try {
7290
+ const entries = await fs$2.readdir(crashesDir);
7291
+ let best = null;
7292
+ for (const name of entries) {
7293
+ const full = path.join(crashesDir, name);
7294
+ let st;
7295
+ try {
7296
+ st = await fs$2.stat(full);
7297
+ } catch {
7298
+ continue;
7299
+ }
7300
+ if (!st?.isDirectory?.()) continue;
7301
+ const mtimeMs = Number(st.mtimeMs || 0);
7302
+ if (!best || mtimeMs > best.mtimeMs) best = { path: full, mtimeMs };
7303
+ }
7304
+ return best?.path ?? null;
7305
+ } catch {
7306
+ return null;
7307
+ }
7308
+ };
7309
+ const readFileTail = async (filePath, maxBytes) => {
7310
+ try {
7311
+ const buf = await fs$2.readFile(filePath);
7312
+ const slice = buf.length > maxBytes ? buf.subarray(buf.length - maxBytes) : buf;
7313
+ return slice.toString("utf8");
7314
+ } catch {
7315
+ return null;
7316
+ }
7317
+ };
7318
+ const gatherCrashDiagnosticsBestEffort = async (uprojectPath) => {
7319
+ const projectRoot = path.dirname(uprojectPath);
7320
+ const summary = [];
7321
+ const detail = { uprojectPath, projectRoot };
7322
+ const latestLog = await findLatestFile(path.join(projectRoot, "Saved", "Logs"), (n) => n.toLowerCase().endsWith(".log"));
7323
+ if (latestLog) {
7324
+ detail.projectLogPath = latestLog;
7325
+ const tail = await readFileTail(latestLog, 24e3);
7326
+ if (tail) {
7327
+ detail.projectLogTail = tailText(tail, 12e3);
7328
+ summary.push(`Latest log: ${latestLog}`);
7329
+ }
7330
+ }
7331
+ const latestCrashDir = await findLatestCrashDir(projectRoot);
7332
+ if (latestCrashDir) {
7333
+ detail.latestCrashDir = latestCrashDir;
7334
+ const crashContext = await findLatestFile(latestCrashDir, (n) => n.toLowerCase().includes("crashcontext") && n.toLowerCase().endsWith(".xml"));
7335
+ if (crashContext) {
7336
+ detail.crashContextPath = crashContext;
7337
+ const tail = await readFileTail(crashContext, 24e3);
7338
+ if (tail) {
7339
+ detail.crashContextTail = tailText(tail, 12e3);
7340
+ summary.push(`CrashContext: ${crashContext}`);
7341
+ }
7342
+ }
7343
+ }
7344
+ return { detail, summary };
7345
+ };
7258
7346
  const getUnrealEditorExe = (engineRoot) => {
7259
7347
  const root = engineRoot.trim().replace(/[\\/]+$/, "");
7260
7348
  if (process.platform === "darwin") {
@@ -7310,7 +7398,7 @@ ${res.stderr}`;
7310
7398
  kind: "unreachable",
7311
7399
  severity: "warning",
7312
7400
  detectedAtMs: now,
7313
- message: "Unreal Editor is no longer reachable (it was reachable earlier). It may have crashed or been closed.",
7401
+ message: "Unreal Editor is no longer reachable (it was reachable earlier). It may have crashed or been closed.\nNext: fix the issue, then relaunch via unreal_editor_relaunch_last (if you launched from this session) or unreal_editor_launch.",
7314
7402
  detail: {
7315
7403
  lastReachableAtMs: state.lastReachableAtMs
7316
7404
  }
@@ -7342,20 +7430,35 @@ ${res.stderr}`;
7342
7430
  engineRoot,
7343
7431
  startedAtMs: Date.now()
7344
7432
  };
7433
+ state.lastLaunch = {
7434
+ uprojectPath,
7435
+ engineRoot,
7436
+ extraArgs,
7437
+ startedAtMs: Date.now()
7438
+ };
7345
7439
  child.on("exit", (code, signal) => {
7346
- const now = Date.now();
7347
- const exitCode = typeof code === "number" ? code : null;
7348
- const sig = typeof signal === "string" ? signal : null;
7349
- const isCrash = sig !== null || exitCode !== null && exitCode !== 0;
7350
- state.launched = null;
7351
- if (!isCrash) return;
7352
- emitIssue({
7353
- kind: "process_exit",
7354
- severity: "crash",
7355
- detectedAtMs: now,
7356
- message: `Unreal Editor process exited unexpectedly (code=${exitCode ?? "null"} signal=${sig ?? "null"}).`,
7357
- detail: { exitCode, signal: sig, pid, uprojectPath }
7358
- });
7440
+ void (async () => {
7441
+ const now = Date.now();
7442
+ const exitCode = typeof code === "number" ? code : null;
7443
+ const sig = typeof signal === "string" ? signal : null;
7444
+ const isCrash = sig !== null || exitCode !== null && exitCode !== 0;
7445
+ state.launched = null;
7446
+ if (!isCrash) return;
7447
+ const diag = await gatherCrashDiagnosticsBestEffort(uprojectPath).catch(() => ({ detail: {}, summary: [] }));
7448
+ const msgParts = [
7449
+ `Unreal Editor process exited unexpectedly (code=${exitCode ?? "null"} signal=${sig ?? "null"}).`,
7450
+ `Project: ${uprojectPath}`,
7451
+ ...diag.summary,
7452
+ `Next: fix the issue, then relaunch via unreal_editor_relaunch_last (or unreal_editor_launch).`
7453
+ ];
7454
+ emitIssue({
7455
+ kind: "process_exit",
7456
+ severity: "crash",
7457
+ detectedAtMs: now,
7458
+ message: msgParts.filter(Boolean).join("\n"),
7459
+ detail: { exitCode, signal: sig, pid, uprojectPath, ...diag.detail }
7460
+ });
7461
+ })();
7359
7462
  });
7360
7463
  child.on("error", (err) => {
7361
7464
  const now = Date.now();
@@ -7364,7 +7467,9 @@ ${res.stderr}`;
7364
7467
  kind: "process_exit",
7365
7468
  severity: "crash",
7366
7469
  detectedAtMs: now,
7367
- message: `Failed to launch Unreal Editor: ${err instanceof Error ? err.message : String(err)}`,
7470
+ message: `Failed to launch Unreal Editor: ${err instanceof Error ? err.message : String(err)}
7471
+ Project: ${uprojectPath}
7472
+ Next: fix the issue, then relaunch via unreal_editor_relaunch_last (or unreal_editor_launch).`,
7368
7473
  detail: { pid, uprojectPath }
7369
7474
  });
7370
7475
  });
@@ -7374,6 +7479,12 @@ ${res.stderr}`;
7374
7479
  noteUnrealActivity,
7375
7480
  noteUnrealReachable,
7376
7481
  launchEditor,
7482
+ relaunchLast: async (extraArgs) => {
7483
+ const last = state.lastLaunch;
7484
+ if (!last) throw new Error("No known prior Unreal launch in this session. Use unreal_editor_launch with an explicit uprojectPath.");
7485
+ const mergedArgs = Array.isArray(extraArgs) && extraArgs.length > 0 ? extraArgs.filter((a) => typeof a === "string" && a.trim()) : last.extraArgs;
7486
+ return launchEditor({ uprojectPath: last.uprojectPath, engineRoot: last.engineRoot, extraArgs: mergedArgs });
7487
+ },
7377
7488
  stop: () => {
7378
7489
  try {
7379
7490
  interval.unref();
@@ -8880,6 +8991,32 @@ ${String(st.stdout || "").trim()}`
8880
8991
  isError: false
8881
8992
  };
8882
8993
  }));
8994
+ mcp.registerTool("unreal_editor_relaunch_last", {
8995
+ title: "Unreal Editor: Relaunch Last Project",
8996
+ description: "Relaunch the last Unreal project previously launched via unreal_editor_launch in this session (no auto-restart). Use this after a crash once you\u2019ve fixed files. If it crashes again, Flockbay will abort and report again.",
8997
+ inputSchema: {
8998
+ extraArgs: z.z.array(z.z.string()).optional().describe("Optional replacement UnrealEditor command-line args (advanced).")
8999
+ }
9000
+ }, async (args) => runWithMcpToolCard("unreal_editor_relaunch_last", args, async () => {
9001
+ const extraArgs = Array.isArray(args?.extraArgs) ? args.extraArgs : void 0;
9002
+ unrealEditorSupervisor.noteUnrealActivity();
9003
+ try {
9004
+ const launched = await unrealEditorSupervisor.relaunchLast(extraArgs);
9005
+ return {
9006
+ content: [
9007
+ { type: "text", text: `Relaunched Unreal Editor (last project).` },
9008
+ { type: "text", text: JSON.stringify({ pid: launched.pid, exePath: launched.exePath }, null, 2) },
9009
+ { type: "text", text: "Next: wait for the editor to finish loading, then re-run UnrealMCP tools." }
9010
+ ],
9011
+ isError: false
9012
+ };
9013
+ } catch (err) {
9014
+ return {
9015
+ content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
9016
+ isError: true
9017
+ };
9018
+ }
9019
+ }));
8883
9020
  mcp.registerTool("unreal_build_project", {
8884
9021
  title: "Unreal Build Project (UBT)",
8885
9022
  description: "Build the project via Unreal Build Tool (via Engine/Build/BatchFiles/Build.*). Returns structured errors (file/line) and a log path for deep debugging.",
@@ -10305,6 +10442,7 @@ Fix: ${res.hint}` : "";
10305
10442
  "unreal_headless_screenshot",
10306
10443
  "unreal_latest_screenshots",
10307
10444
  "unreal_editor_launch",
10445
+ "unreal_editor_relaunch_last",
10308
10446
  "unreal_build_project",
10309
10447
  "unreal_mcp_command",
10310
10448
  "unreal_mcp_list_capabilities",
@@ -12086,7 +12224,7 @@ ${engineRoot}`, {
12086
12224
  } else if (subcommand === "codex") {
12087
12225
  try {
12088
12226
  await chdirToNearestUprojectRootIfPresent();
12089
- const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex-Bh3-ebwT.cjs'); });
12227
+ const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex-CXJW0tzo.cjs'); });
12090
12228
  let startedBy = void 0;
12091
12229
  let sessionId = void 0;
12092
12230
  for (let i = 1; i < args.length; i++) {
@@ -12181,7 +12319,7 @@ ${engineRoot}`, {
12181
12319
  }
12182
12320
  try {
12183
12321
  await chdirToNearestUprojectRootIfPresent();
12184
- const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-hXryGqFd.cjs'); });
12322
+ const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-FOBXtEU6.cjs'); });
12185
12323
  let startedBy = void 0;
12186
12324
  let sessionId = void 0;
12187
12325
  for (let i = 1; i < args.length; i++) {
package/dist/index.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  require('chalk');
4
- require('./index-BiUf5vLX.cjs');
5
- require('./types-CL_3YyS9.cjs');
4
+ require('./index-D_mglYG0.cjs');
5
+ require('./types-BYHCKlu_.cjs');
6
6
  require('zod');
7
7
  require('node:child_process');
8
8
  require('node:fs');
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import 'chalk';
2
- import './index-5jfGXWTy.mjs';
3
- import './types-BQvaA3sv.mjs';
2
+ import './index-CX0Z8pmz.mjs';
3
+ import './types-C4QeUggl.mjs';
4
4
  import 'zod';
5
5
  import 'node:child_process';
6
6
  import 'node:fs';
package/dist/lib.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var types = require('./types-CL_3YyS9.cjs');
3
+ var types = require('./types-BYHCKlu_.cjs');
4
4
  require('axios');
5
5
  require('node:fs');
6
6
  require('node:os');
package/dist/lib.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { A as ApiClient, a as ApiSessionClient, R as RawJSONLinesSchema, c as configuration, l as logger } from './types-BQvaA3sv.mjs';
1
+ export { A as ApiClient, a as ApiSessionClient, R as RawJSONLinesSchema, c as configuration, l as logger } from './types-C4QeUggl.mjs';
2
2
  import 'axios';
3
3
  import 'node:fs';
4
4
  import 'node:os';
@@ -1,6 +1,6 @@
1
1
  import { useStdout, useInput, Box, Text, render } from 'ink';
2
2
  import React, { useState, useRef, useEffect, useCallback } from 'react';
3
- import { l as logger, A as ApiClient, r as readSettings, p as projectPath, c as configuration, b as packageJson } from './types-BQvaA3sv.mjs';
3
+ import { l as logger, A as ApiClient, r as readSettings, p as projectPath, c as configuration, b as packageJson } from './types-C4QeUggl.mjs';
4
4
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
5
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
6
6
  import { z } from 'zod';
@@ -10,7 +10,7 @@ import fs__default from 'node:fs';
10
10
  import os from 'node:os';
11
11
  import path, { resolve, join } from 'node:path';
12
12
  import { spawnSync } from 'node:child_process';
13
- import { s as shouldCountToolCall, c as consumeToolQuota, f as formatQuotaDeniedReason, h as hashObject, i as initialMachineMetadata, E as ElicitationHub, n as notifyDaemonSessionStarted, M as MessageQueue2, P as PLATFORM_SYSTEM_PROMPT, a as setLatestUserImages, w as withUserImagesMarker, r as registerKillSessionHandler, b as MessageBuffer, d as startFlockbayServer, e as detectUnrealProject, g as buildProjectCapsule, t as trimIdent, j as autoFinalizeCoordinationWorkItem, k as detectScreenshotsForGate, l as applyCoordinationSideEffectsFromMcpToolResult, m as stopCaffeinate } from './index-5jfGXWTy.mjs';
13
+ import { s as shouldCountToolCall, c as consumeToolQuota, f as formatQuotaDeniedReason, h as hashObject, i as initialMachineMetadata, E as ElicitationHub, n as notifyDaemonSessionStarted, M as MessageQueue2, P as PLATFORM_SYSTEM_PROMPT, a as setLatestUserImages, w as withUserImagesMarker, r as registerKillSessionHandler, b as MessageBuffer, d as startFlockbayServer, e as detectUnrealProject, g as buildProjectCapsule, t as trimIdent, j as autoFinalizeCoordinationWorkItem, k as detectScreenshotsForGate, l as applyCoordinationSideEffectsFromMcpToolResult, m as stopCaffeinate } from './index-CX0Z8pmz.mjs';
14
14
  import 'axios';
15
15
  import 'node:events';
16
16
  import 'socket.io-client';
@@ -2587,7 +2587,8 @@ async function runCodex(opts) {
2587
2587
  const push = (role, text) => {
2588
2588
  const t = String(text || "").trim();
2589
2589
  if (!t) return;
2590
- lines.push(`${role === "user" ? "User" : "Assistant"}: ${t}`);
2590
+ const label = role === "user" ? "User" : role === "assistant" ? "Assistant" : "System";
2591
+ lines.push(`${label}: ${t}`);
2591
2592
  };
2592
2593
  for (const row of rows.slice(-80)) {
2593
2594
  const content = row?.content;
@@ -2611,6 +2612,11 @@ async function runCodex(opts) {
2611
2612
  if (txt) push("assistant", txt);
2612
2613
  }
2613
2614
  }
2615
+ } else if (payload.type === "event") {
2616
+ const data = payload.data;
2617
+ if (data && typeof data === "object" && data.type === "message" && typeof data.message === "string") {
2618
+ push("system", data.message);
2619
+ }
2614
2620
  } else if (payload.type === "codex") {
2615
2621
  const data = payload.data;
2616
2622
  if (data && typeof data === "object" && data.type === "message" && typeof data.message === "string") {
@@ -2,7 +2,7 @@
2
2
 
3
3
  var ink = require('ink');
4
4
  var React = require('react');
5
- var types = require('./types-CL_3YyS9.cjs');
5
+ var types = require('./types-BYHCKlu_.cjs');
6
6
  var index_js = require('@modelcontextprotocol/sdk/client/index.js');
7
7
  var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
8
8
  var z = require('zod');
@@ -12,7 +12,7 @@ var fs = require('node:fs');
12
12
  var os = require('node:os');
13
13
  var path = require('node:path');
14
14
  var node_child_process = require('node:child_process');
15
- var index = require('./index-BiUf5vLX.cjs');
15
+ var index = require('./index-D_mglYG0.cjs');
16
16
  require('axios');
17
17
  require('node:events');
18
18
  require('socket.io-client');
@@ -2589,7 +2589,8 @@ async function runCodex(opts) {
2589
2589
  const push = (role, text) => {
2590
2590
  const t = String(text || "").trim();
2591
2591
  if (!t) return;
2592
- lines.push(`${role === "user" ? "User" : "Assistant"}: ${t}`);
2592
+ const label = role === "user" ? "User" : role === "assistant" ? "Assistant" : "System";
2593
+ lines.push(`${label}: ${t}`);
2593
2594
  };
2594
2595
  for (const row of rows.slice(-80)) {
2595
2596
  const content = row?.content;
@@ -2613,6 +2614,11 @@ async function runCodex(opts) {
2613
2614
  if (txt) push("assistant", txt);
2614
2615
  }
2615
2616
  }
2617
+ } else if (payload.type === "event") {
2618
+ const data = payload.data;
2619
+ if (data && typeof data === "object" && data.type === "message" && typeof data.message === "string") {
2620
+ push("system", data.message);
2621
+ }
2616
2622
  } else if (payload.type === "codex") {
2617
2623
  const data = payload.data;
2618
2624
  if (data && typeof data === "object" && data.type === "message" && typeof data.message === "string") {
@@ -4,8 +4,8 @@ import { randomUUID, createHash } from 'node:crypto';
4
4
  import os from 'node:os';
5
5
  import path, { resolve, join as join$1, basename } from 'node:path';
6
6
  import { mkdir, writeFile, readFile } from 'node:fs/promises';
7
- import { l as logger, b as packageJson, A as ApiClient, r as readSettings, p as projectPath, c as configuration } from './types-BQvaA3sv.mjs';
8
- import { s as shouldCountToolCall, c as consumeToolQuota, f as formatQuotaDeniedReason, h as hashObject, i as initialMachineMetadata, n as notifyDaemonSessionStarted, M as MessageQueue2, g as buildProjectCapsule, a as setLatestUserImages, b as MessageBuffer, w as withUserImagesMarker, P as PLATFORM_SYSTEM_PROMPT, r as registerKillSessionHandler, d as startFlockbayServer, o as extractUserImagesMarker, p as getLatestUserImages, j as autoFinalizeCoordinationWorkItem, E as ElicitationHub, k as detectScreenshotsForGate, m as stopCaffeinate } from './index-5jfGXWTy.mjs';
7
+ import { l as logger, b as packageJson, A as ApiClient, r as readSettings, p as projectPath, c as configuration } from './types-C4QeUggl.mjs';
8
+ import { s as shouldCountToolCall, c as consumeToolQuota, f as formatQuotaDeniedReason, h as hashObject, i as initialMachineMetadata, n as notifyDaemonSessionStarted, M as MessageQueue2, g as buildProjectCapsule, a as setLatestUserImages, b as MessageBuffer, w as withUserImagesMarker, P as PLATFORM_SYSTEM_PROMPT, r as registerKillSessionHandler, d as startFlockbayServer, o as extractUserImagesMarker, p as getLatestUserImages, j as autoFinalizeCoordinationWorkItem, E as ElicitationHub, k as detectScreenshotsForGate, m as stopCaffeinate } from './index-CX0Z8pmz.mjs';
9
9
  import { spawn, spawnSync } from 'node:child_process';
10
10
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
11
11
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
@@ -6,8 +6,8 @@ var node_crypto = require('node:crypto');
6
6
  var os = require('node:os');
7
7
  var path = require('node:path');
8
8
  var fs$2 = require('node:fs/promises');
9
- var types = require('./types-CL_3YyS9.cjs');
10
- var index = require('./index-BiUf5vLX.cjs');
9
+ var types = require('./types-BYHCKlu_.cjs');
10
+ var index = require('./index-D_mglYG0.cjs');
11
11
  var node_child_process = require('node:child_process');
12
12
  var sdk = require('@agentclientprotocol/sdk');
13
13
  var fs = require('fs');
@@ -42,7 +42,7 @@ function _interopNamespaceDefault(e) {
42
42
  var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
43
43
 
44
44
  var name = "flockbay";
45
- var version = "0.10.19";
45
+ var version = "0.10.20";
46
46
  var description = "Flockbay CLI (local agent + daemon)";
47
47
  var author = "Eduardo Orellana";
48
48
  var license = "UNLICENSED";
@@ -770,7 +770,7 @@ class RpcHandlerManager {
770
770
  }
771
771
  }
772
772
 
773
- const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-CL_3YyS9.cjs', document.baseURI).href))));
773
+ const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-BYHCKlu_.cjs', document.baseURI).href))));
774
774
  function projectPath() {
775
775
  const path = path$1.resolve(__dirname$1, "..");
776
776
  return path;
@@ -21,7 +21,7 @@ import net from 'node:net';
21
21
  import { spawn as spawn$1 } from 'node:child_process';
22
22
 
23
23
  var name = "flockbay";
24
- var version = "0.10.19";
24
+ var version = "0.10.20";
25
25
  var description = "Flockbay CLI (local agent + daemon)";
26
26
  var author = "Eduardo Orellana";
27
27
  var license = "UNLICENSED";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flockbay",
3
- "version": "0.10.19",
3
+ "version": "0.10.20",
4
4
  "description": "Flockbay CLI (local agent + daemon)",
5
5
  "author": "Eduardo Orellana",
6
6
  "license": "UNLICENSED",
@@ -360,6 +360,16 @@ static TMap<FString, FUnrealMCPCommandSchema> BuildSchemas()
360
360
 
361
361
  {
362
362
  TArray<FUnrealMCPParamSchema> P;
363
+ AddEnumParam(
364
+ P,
365
+ TEXT("source"),
366
+ false,
367
+ TEXT("Capture source: auto picks PIE when playing, otherwise editor viewport."),
368
+ {TEXT("auto"), TEXT("pie"), TEXT("editor")},
369
+ {TEXT("mode")},
370
+ {{TEXT("play"), TEXT("pie")}, {TEXT("game"), TEXT("pie")}, {TEXT("viewport"), TEXT("editor")}});
371
+ AddNumberParam(P, TEXT("width"), false, TEXT("Optional output width (defaults to game viewport size or 1280)."), {TEXT("resX"), TEXT("w")});
372
+ AddNumberParam(P, TEXT("height"), false, TEXT("Optional output height (defaults to game viewport size or 720)."), {TEXT("resY"), TEXT("h")});
363
373
  AddStringParam(P, TEXT("filepath"), false, TEXT("Output path. If omitted, defaults to <Project>/Saved/Screenshots/Flockbay/."), {TEXT("path"), TEXT("file_path")});
364
374
  AddStringParam(P, TEXT("filename"), false, TEXT("Optional file name (auto-generated if omitted)."));
365
375
  AddSimpleCommand(TEXT("take_screenshot"), TEXT("Take Screenshot"), TEXT("Capture a screenshot from the editor or PIE."), P, nullptr);
@@ -49,6 +49,10 @@
49
49
  #include "Engine/EngineTypes.h"
50
50
  #include "GameFramework/PlayerController.h"
51
51
  #include "Camera/PlayerCameraManager.h"
52
+ #include "Engine/LocalPlayer.h"
53
+ #include "Engine/SceneCapture2D.h"
54
+ #include "Components/SceneCaptureComponent2D.h"
55
+ #include "Engine/TextureRenderTarget2D.h"
52
56
 
53
57
  FUnrealMCPEditorCommands::FUnrealMCPEditorCommands()
54
58
  {
@@ -2295,26 +2299,193 @@ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleTakeScreenshot(const TSh
2295
2299
  FilePath += TEXT(".png");
2296
2300
  }
2297
2301
 
2298
- // Get the active viewport
2302
+ FString Source = TEXT("auto");
2303
+ if (Params->TryGetStringField(TEXT("source"), Source))
2304
+ {
2305
+ Source = Source.TrimStartAndEnd().ToLower();
2306
+ if (Source.IsEmpty())
2307
+ {
2308
+ Source = TEXT("auto");
2309
+ }
2310
+ }
2311
+
2312
+ double WidthNum = 0.0;
2313
+ double HeightNum = 0.0;
2314
+ int32 RequestedWidth = 0;
2315
+ int32 RequestedHeight = 0;
2316
+ if (Params->TryGetNumberField(TEXT("width"), WidthNum))
2317
+ {
2318
+ RequestedWidth = static_cast<int32>(WidthNum);
2319
+ }
2320
+ else if (Params->TryGetNumberField(TEXT("resX"), WidthNum))
2321
+ {
2322
+ RequestedWidth = static_cast<int32>(WidthNum);
2323
+ }
2324
+
2325
+ if (Params->TryGetNumberField(TEXT("height"), HeightNum))
2326
+ {
2327
+ RequestedHeight = static_cast<int32>(HeightNum);
2328
+ }
2329
+ else if (Params->TryGetNumberField(TEXT("resY"), HeightNum))
2330
+ {
2331
+ RequestedHeight = static_cast<int32>(HeightNum);
2332
+ }
2333
+
2334
+ const bool bPieRunning = (GEditor && GEditor->PlayWorld);
2335
+ const bool bWantsPie = (Source == TEXT("pie"));
2336
+ const bool bWantsEditor = (Source == TEXT("editor"));
2337
+ const bool bAuto = (Source == TEXT("auto"));
2338
+ if (!bWantsPie && !bWantsEditor && !bAuto)
2339
+ {
2340
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Invalid screenshot source: %s (expected: auto|pie|editor)"), *Source));
2341
+ }
2342
+
2343
+ const bool bUsePieCapture = bWantsPie || (bAuto && bPieRunning);
2344
+ if (bUsePieCapture)
2345
+ {
2346
+ if (!bPieRunning)
2347
+ {
2348
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("PIE is not running (PlayWorld is null)."));
2349
+ }
2350
+
2351
+ UWorld* PlayWorld = GEditor->PlayWorld;
2352
+ APlayerController* PC = PlayWorld ? PlayWorld->GetFirstPlayerController() : nullptr;
2353
+ if (!PC)
2354
+ {
2355
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to find PlayerController in PlayWorld."));
2356
+ }
2357
+
2358
+ APlayerCameraManager* PCM = PC->PlayerCameraManager;
2359
+ if (!PCM)
2360
+ {
2361
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to find PlayerCameraManager (PlayerController.PlayerCameraManager is null)."));
2362
+ }
2363
+
2364
+ int32 Width = RequestedWidth;
2365
+ int32 Height = RequestedHeight;
2366
+ if (Width <= 0 || Height <= 0)
2367
+ {
2368
+ if (ULocalPlayer* LocalPlayer = PC->GetLocalPlayer())
2369
+ {
2370
+ if (LocalPlayer->ViewportClient && LocalPlayer->ViewportClient->Viewport)
2371
+ {
2372
+ const FIntPoint ViewSize = LocalPlayer->ViewportClient->Viewport->GetSizeXY();
2373
+ if (ViewSize.X > 0 && ViewSize.Y > 0)
2374
+ {
2375
+ if (Width <= 0)
2376
+ {
2377
+ Width = ViewSize.X;
2378
+ }
2379
+ if (Height <= 0)
2380
+ {
2381
+ Height = ViewSize.Y;
2382
+ }
2383
+ }
2384
+ }
2385
+ }
2386
+ }
2387
+
2388
+ if (Width <= 0 || Height <= 0)
2389
+ {
2390
+ Width = 1280;
2391
+ Height = 720;
2392
+ }
2393
+
2394
+ const FVector CamLocation = PCM->GetCameraLocation();
2395
+ const FRotator CamRotation = PCM->GetCameraRotation();
2396
+
2397
+ FActorSpawnParameters SpawnParams;
2398
+ SpawnParams.ObjectFlags |= RF_Transient;
2399
+ SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
2400
+
2401
+ ASceneCapture2D* CaptureActor = PlayWorld->SpawnActor<ASceneCapture2D>(ASceneCapture2D::StaticClass(), CamLocation, CamRotation, SpawnParams);
2402
+ if (!CaptureActor)
2403
+ {
2404
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn ASceneCapture2D for PIE screenshot capture."));
2405
+ }
2406
+
2407
+ USceneCaptureComponent2D* CaptureComp = CaptureActor->GetCaptureComponent2D();
2408
+ if (!CaptureComp)
2409
+ {
2410
+ CaptureActor->Destroy();
2411
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get SceneCaptureComponent2D from ASceneCapture2D."));
2412
+ }
2413
+
2414
+ UTextureRenderTarget2D* RenderTarget = NewObject<UTextureRenderTarget2D>(CaptureComp);
2415
+ if (!RenderTarget)
2416
+ {
2417
+ CaptureActor->Destroy();
2418
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create UTextureRenderTarget2D."));
2419
+ }
2420
+ RenderTarget->InitCustomFormat(Width, Height, PF_B8G8R8A8, /*bForceLinearGamma*/ false);
2421
+ RenderTarget->ClearColor = FLinearColor::Black;
2422
+ RenderTarget->UpdateResourceImmediate(true);
2423
+
2424
+ CaptureComp->TextureTarget = RenderTarget;
2425
+ CaptureComp->bCaptureEveryFrame = false;
2426
+ CaptureComp->bCaptureOnMovement = false;
2427
+ CaptureComp->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR;
2428
+ CaptureComp->FOVAngle = PCM->GetFOVAngle();
2429
+
2430
+ CaptureComp->CaptureScene();
2431
+
2432
+ FTextureRenderTargetResource* RenderResource = RenderTarget->GameThread_GetRenderTargetResource();
2433
+ if (!RenderResource)
2434
+ {
2435
+ CaptureActor->Destroy();
2436
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get render target resource for PIE screenshot capture."));
2437
+ }
2438
+
2439
+ TArray<FColor> Bitmap;
2440
+ if (!RenderResource->ReadPixels(Bitmap))
2441
+ {
2442
+ CaptureActor->Destroy();
2443
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to read pixels from PIE render target."));
2444
+ }
2445
+
2446
+ CaptureActor->Destroy();
2447
+
2448
+ TArray<uint8> CompressedBitmap;
2449
+ FImageUtils::CompressImageArray(Width, Height, Bitmap, CompressedBitmap);
2450
+ if (!FFileHelper::SaveArrayToFile(CompressedBitmap, *FilePath))
2451
+ {
2452
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to save screenshot PNG to disk."));
2453
+ }
2454
+
2455
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
2456
+ ResultObj->SetStringField(TEXT("filepath"), FilePath);
2457
+ ResultObj->SetStringField(TEXT("source"), TEXT("pie"));
2458
+ ResultObj->SetNumberField(TEXT("width"), Width);
2459
+ ResultObj->SetNumberField(TEXT("height"), Height);
2460
+ return ResultObj;
2461
+ }
2462
+
2463
+ // Editor viewport screenshot (can be stale if the OS window is not focused; prefer PIE capture when playing).
2299
2464
  if (GEditor && GEditor->GetActiveViewport())
2300
2465
  {
2466
+ GEditor->RedrawAllViewports(/*bInvalidateHitProxies*/ true);
2467
+
2301
2468
  FViewport* Viewport = GEditor->GetActiveViewport();
2469
+ const FIntPoint ViewSize = Viewport->GetSizeXY();
2302
2470
  TArray<FColor> Bitmap;
2303
- FIntRect ViewportRect(0, 0, Viewport->GetSizeXY().X, Viewport->GetSizeXY().Y);
2304
-
2471
+ FIntRect ViewportRect(0, 0, ViewSize.X, ViewSize.Y);
2472
+
2305
2473
  if (Viewport->ReadPixels(Bitmap, FReadSurfaceDataFlags(), ViewportRect))
2306
2474
  {
2307
2475
  TArray<uint8> CompressedBitmap;
2308
- FImageUtils::CompressImageArray(Viewport->GetSizeXY().X, Viewport->GetSizeXY().Y, Bitmap, CompressedBitmap);
2309
-
2476
+ FImageUtils::CompressImageArray(ViewSize.X, ViewSize.Y, Bitmap, CompressedBitmap);
2477
+
2310
2478
  if (FFileHelper::SaveArrayToFile(CompressedBitmap, *FilePath))
2311
2479
  {
2312
2480
  TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
2313
2481
  ResultObj->SetStringField(TEXT("filepath"), FilePath);
2482
+ ResultObj->SetStringField(TEXT("source"), TEXT("editor"));
2483
+ ResultObj->SetNumberField(TEXT("width"), ViewSize.X);
2484
+ ResultObj->SetNumberField(TEXT("height"), ViewSize.Y);
2314
2485
  return ResultObj;
2315
2486
  }
2316
2487
  }
2317
2488
  }
2318
-
2319
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to take screenshot"));
2489
+
2490
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to take screenshot."));
2320
2491
  }