gdharness 1.3.66 → 1.3.67

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/build/cli.js CHANGED
@@ -2003,9 +2003,6 @@ function nonEmpty(value) {
2003
2003
  function keptSlotsOf(count) {
2004
2004
  return Math.max(2, Math.floor(count / 4));
2005
2005
  }
2006
- function reservedSlotOf(count) {
2007
- return count - Math.max(1, count - keptSlotsOf(count)) >= 2 ? count - 1 : null;
2008
- }
2009
2006
  function slotWaiters(directory) {
2010
2007
  const marks = join6(directory, WAITING_FOLDER);
2011
2008
  let names;
@@ -2072,8 +2069,20 @@ class SlotLine {
2072
2069
  return { ...holder, ...this.game ? { game: true } : {}, taker: this.taker, since: this.since / 1000 };
2073
2070
  }
2074
2071
  ahead(waiters) {
2075
- const own = this.mark ?? "￿";
2076
- return waiters.filter((one) => one.mark !== this.mark && (one.since < this.since || one.since === this.since && one.mark < own)).toSorted((a, b) => a.since - b.since || (a.mark < b.mark ? -1 : 1));
2072
+ const now = this.clock();
2073
+ const young = (taker, game, since) => taker === "other" && (game || now - since < ONE_OFF_AGED_MS) ? 1 : 0;
2074
+ const own = {
2075
+ group: young(this.taker, this.game, this.since),
2076
+ since: this.since,
2077
+ mark: this.mark ?? "￿"
2078
+ };
2079
+ const placeOf = (one) => ({
2080
+ group: young(one.taker, one.game, one.since),
2081
+ since: one.since,
2082
+ mark: one.mark
2083
+ });
2084
+ const before = (a, b) => a.group - b.group || a.since - b.since || (a.mark < b.mark ? -1 : a.mark > b.mark ? 1 : 0);
2085
+ return waiters.filter((one) => one.mark !== this.mark && before(placeOf(one), own) < 0).toSorted((a, b) => before(placeOf(a), placeOf(b)));
2077
2086
  }
2078
2087
  admitted(numbers, theirs) {
2079
2088
  const now = this.clock();
@@ -2085,7 +2094,7 @@ class SlotLine {
2085
2094
  }
2086
2095
  }
2087
2096
  function gamesKeptOf(kept) {
2088
- return Math.max(0, Math.min(Math.floor(kept / 2), kept - GATE_KEPT - 1));
2097
+ return Math.max(0, Math.min(Math.floor(kept / 2), kept - 1));
2089
2098
  }
2090
2099
  function slotsDrawnOn(count, taker, queue) {
2091
2100
  const gated = Math.max(1, count - keptSlotsOf(count));
@@ -2094,14 +2103,35 @@ function slotsDrawnOn(count, taker, queue) {
2094
2103
  }
2095
2104
  const every = Array.from({ length: count }, (_, slot) => count - 1 - slot);
2096
2105
  const kept = every.slice(0, count - gated);
2106
+ if (taker === "tier") {
2107
+ return { numbers: [...every.slice(count - gated), ...kept] };
2108
+ }
2097
2109
  if (taker === "timing" || queue === "none" || kept.length === 0) {
2098
2110
  return { numbers: every };
2099
2111
  }
2100
- if (taker === "tier") {
2101
- const reserve = reservedSlotOf(count);
2102
- return { numbers: [...kept].reverse().filter((slot) => slot !== reserve), limit: "tier" };
2112
+ if (queue === "spare") {
2113
+ return { numbers: [...kept, ...every.slice(count - gated)] };
2103
2114
  }
2104
- return { numbers: kept.slice(0, Math.max(1, kept.length - GATE_KEPT)), limit: "kept" };
2115
+ return { numbers: kept, limit: "kept" };
2116
+ }
2117
+ function spareSaid(content, now) {
2118
+ let said;
2119
+ try {
2120
+ said = JSON.parse(content ?? "");
2121
+ } catch {
2122
+ return 0;
2123
+ }
2124
+ if (!isRecord(said)) {
2125
+ return 0;
2126
+ }
2127
+ const { spare, written } = said;
2128
+ if (typeof spare !== "number" || !Number.isInteger(spare) || typeof written !== "number") {
2129
+ return 0;
2130
+ }
2131
+ return now - written > SPARE_STALE_MS ? 0 : Math.max(0, spare);
2132
+ }
2133
+ function forAGame(queue, game) {
2134
+ return game && queue === "spare" ? "queued" : queue;
2105
2135
  }
2106
2136
  function inAGate(env = process4.env) {
2107
2137
  return nonEmpty(env[SHARE_VARIABLE]) !== undefined && nonEmpty(env[SHARE_COUNT_VARIABLE]) !== undefined;
@@ -2294,10 +2324,8 @@ class EngineSlots {
2294
2324
  const aged = taker === "other" && !game && waitedMs >= ONE_OFF_AGED_MS;
2295
2325
  let queue;
2296
2326
  const queued = async () => queue ??= await this.gateQueue();
2297
- this.drawn = slotsDrawnOn(this.count, taker, taker === "gate" || taker === "timing" || aged ? "none" : await queued());
2327
+ this.drawn = slotsDrawnOn(this.count, taker, taker === "other" && !aged ? forAGame(await queued(), game) : "none");
2298
2328
  if (game && this.drawn.limit === "kept") {
2299
- const reserve = reservedSlotOf(this.count);
2300
- this.drawn = { ...this.drawn, numbers: this.drawn.numbers.filter((slot) => slot !== reserve) };
2301
2329
  const kept = this.count - Math.max(1, this.count - keptSlotsOf(this.count));
2302
2330
  const games = await this.heldByGames(Array.from({ length: kept }, (_, at) => this.count - kept + at));
2303
2331
  if (games.length >= gamesKeptOf(kept)) {
@@ -2353,12 +2381,11 @@ class EngineSlots {
2353
2381
  return new Map;
2354
2382
  }
2355
2383
  const now = Date.now();
2356
- const reserve = reservedSlotOf(this.count);
2357
2384
  const drawn = [];
2358
2385
  for (const one of ahead) {
2359
2386
  const aged = one.taker === "other" && !one.game && now - one.since >= ONE_OFF_AGED_MS;
2360
- const theirs = slotsDrawnOn(this.count, one.taker, one.taker === "gate" || one.taker === "timing" || aged ? "none" : await queued());
2361
- drawn.push(one.game && theirs.limit === "kept" ? theirs.numbers.filter((slot) => slot !== reserve) : theirs.numbers);
2387
+ const theirs = slotsDrawnOn(this.count, one.taker, one.taker === "other" && !aged ? forAGame(await queued(), one.game) : "none");
2388
+ drawn.push(theirs.numbers);
2362
2389
  }
2363
2390
  return new Map([...slotsClaimed(drawn, open)].map(([slot, at]) => [slot, ahead[at]?.mark ?? ""]));
2364
2391
  }
@@ -2433,7 +2460,12 @@ class EngineSlots {
2433
2460
  live = "queued";
2434
2461
  }
2435
2462
  }
2436
- return live;
2463
+ return live === "queued" && await this.spare() > 0 ? "spare" : live;
2464
+ }
2465
+ async spare() {
2466
+ const path = join6(this.directory, SPARE_FILE);
2467
+ const said = spareSaid(readWhole(path), Date.now());
2468
+ return said > 0 && !await this.free(path) ? said : 0;
2437
2469
  }
2438
2470
  holders() {
2439
2471
  const found = [];
@@ -2654,7 +2686,7 @@ function slotWaitSaid(waitedMs, count, directory, holders = [], drawn = { number
2654
2686
  const named = holdersNamed(taking, holders);
2655
2687
  const waiting = `${so}: ${slots}`;
2656
2688
  const back = `the engine starts when ${taking === 1 ? "it" : "one"} is given back`;
2657
- const starts = drawn.limit === "kept" || drawn.limit === "tier" || drawn.limit === "games" ? `${back} or no gate is queued` : back;
2689
+ const starts = drawn.limit === "kept" || drawn.limit === "games" ? `${back} or no gate is queued` : back;
2658
2690
  const said = named.length === 0 ? `${waiting}, by a gate or another tool, and ${starts}.` : taking === 1 ? `${waiting}, by ${named.join(", ")}, and ${starts}.` : `${waiting}: ${listed(named)}. ${starts.charAt(0).toUpperCase()}${starts.slice(1)}.`;
2659
2691
  return `${said}${leftSaid(drawn)}`;
2660
2692
  }
@@ -2674,9 +2706,6 @@ function slotsSaid(taking, count, directory, limit) {
2674
2706
  if (limit === "gate") {
2675
2707
  return `${taking === 1 ? "the one engine slot" : `the ${String(taking)} engine slots`} of the ${String(count)} ${where} a gate's engines draw on`;
2676
2708
  }
2677
- if (limit === "tier") {
2678
- return `${taking === 1 ? "the one engine slot" : `the ${String(taking)} engine slots`} of the ${String(count)} ${where} a gate's own steps take while a gate is queued`;
2679
- }
2680
2709
  if (limit === "games") {
2681
2710
  return `${taking === 1 ? "the one engine slot" : `the ${String(taking)} engine slots`} of the ${String(count)} ${where} that games editor_run started may hold while a gate is queued, the rest kept for short calls,`;
2682
2711
  }
@@ -2743,7 +2772,7 @@ async function inASlot(body) {
2743
2772
  held.release();
2744
2773
  }
2745
2774
  }
2746
- var HELD_VARIABLE = "GODOT_ENGINE_HELD", SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 2, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", QUEUED_FOLDER = "queued", SHARE_VARIABLE = "GODOT_ENGINE_SHARE", SHARE_COUNT_VARIABLE = "GODOT_ENGINE_SHARE_COUNT", GATE_KEPT = 1, ONE_OFF_AGED_MS = 120000, SLOT_YIELD_MS = 5000, MEMORY_TAKER = "memory", TIMED_VARIABLE = "GODOT_ENGINE_TIMED", TIER_VARIABLE = "GODOT_ENGINE_TIER", SEATED_SUFFIX = ".seated", waitsMarked = 0, ENGINE_PID_LOOK_MS = 100, ours, saidTo, takenBy;
2775
+ var HELD_VARIABLE = "GODOT_ENGINE_HELD", SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 2, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", QUEUED_FOLDER = "queued", SHARE_VARIABLE = "GODOT_ENGINE_SHARE", SHARE_COUNT_VARIABLE = "GODOT_ENGINE_SHARE_COUNT", ONE_OFF_AGED_MS = 120000, SLOT_YIELD_MS = 5000, MEMORY_TAKER = "memory", TIMED_VARIABLE = "GODOT_ENGINE_TIMED", TIER_VARIABLE = "GODOT_ENGINE_TIER", SEATED_SUFFIX = ".seated", SPARE_FILE = "spare.json", SPARE_STALE_MS = 1e4, waitsMarked = 0, ENGINE_PID_LOOK_MS = 100, ours, saidTo, takenBy;
2747
2776
  var init_engine_slots = __esm(() => {
2748
2777
  init_alive();
2749
2778
  init_cache_home();
@@ -13081,13 +13110,17 @@ function crashIn(lines) {
13081
13110
  const at = trimmed.findIndex((line) => CRASH_HEADER.test(line));
13082
13111
  const said = trimmed.some((line) => line.includes(QUEUE_OVERFLOW_LINE));
13083
13112
  if (at === -1) {
13084
- return said ? { signal: "", overflowed: true } : null;
13113
+ return said ? { reported: false, signal: undefined, overflowed: true } : null;
13085
13114
  }
13086
13115
  const report = trimmed.slice(at);
13087
13116
  const build = report.map((line) => CRASH_BUILD.exec(line)?.[1]).find((commit) => commit !== undefined);
13088
13117
  const frames = build === undefined ? undefined : QUEUE_OVERFLOW_FRAMES.get(build);
13089
13118
  const framed = frames?.every((frame) => report.some((line) => line.includes(`(${frame})`))) === true;
13090
- return { signal: CRASH_HEADER.exec(trimmed[at] ?? "")?.[1] ?? "", overflowed: said || framed };
13119
+ return { reported: true, signal: CRASH_HEADER.exec(trimmed[at] ?? "")?.[1], overflowed: said || framed };
13120
+ }
13121
+ function crashSaid(crashed, during) {
13122
+ const signal = crashed.signal === undefined ? "" : `, its crash handler reporting signal ${crashed.signal}`;
13123
+ return `the engine crashed${during}${signal}`;
13091
13124
  }
13092
13125
  function whileRunning(running) {
13093
13126
  if (running === null) {
@@ -13115,10 +13148,10 @@ function judgeEngine(printed, handed, end) {
13115
13148
  faults.push(end.ended);
13116
13149
  } else if (end.code !== 0 && !explained) {
13117
13150
  const free = end.freeBytes === undefined ? "" : `, with ${(end.freeBytes / 1024 ** 3).toFixed(1)} GB of memory free as it ended`;
13118
- faults.push(crashed !== null && crashed.signal !== "" ? `the engine crashed${whileRunning(printed.running)}, its crash handler reporting signal ${crashed.signal}${free}` : end.code === null ? `the engine was ended by ${end.signal ?? "a signal"}${free}` : `the engine exited ${exitNamed(end.code)}${free}`);
13151
+ faults.push(crashed?.reported === true ? `${crashSaid(crashed, whileRunning(printed.running))}${free}` : end.code === null ? `the engine was ended by ${end.signal ?? "a signal"}${free}` : `the engine exited ${exitNamed(end.code)}${free}`);
13119
13152
  }
13120
13153
  if (crashed?.overflowed === true) {
13121
- faults.push(`the engine's call queue overflowed${whileRunning(printed.running)}: something queued deferred calls without end within one frame, most often a layout that changes the size it is laid out from in a deferred handler, so the frame never ends${crashed.signal === "" ? "" : ", and the engine crashed reporting the overflow"}`);
13154
+ faults.push(`the engine's call queue overflowed${whileRunning(printed.running)}: something queued deferred calls without end within one frame, most often a layout that changes the size it is laid out from in a deferred handler, so the frame never ends${crashed.reported ? ", and the engine crashed reporting the overflow" : ""}`);
13122
13155
  }
13123
13156
  if (printed.lines.every((line) => line.trim() === "")) {
13124
13157
  faults.push("the engine printed nothing at all");
@@ -13250,7 +13283,7 @@ function judgeCompile(printed, end) {
13250
13283
  if (end.ended !== null) {
13251
13284
  faults.push(end.ended);
13252
13285
  } else if (end.code !== 0) {
13253
- faults.push(crashed !== null && crashed.signal !== "" ? `the engine crashed, its crash handler reporting signal ${crashed.signal}` : end.code === null ? `the engine was ended by ${end.signal ?? "a signal"}` : `the engine exited ${exitNamed(end.code)}`);
13286
+ faults.push(crashed?.reported === true ? crashSaid(crashed, "") : end.code === null ? `the engine was ended by ${end.signal ?? "a signal"}` : `the engine exited ${exitNamed(end.code)}`);
13254
13287
  }
13255
13288
  if (!printed.lines.some((line) => line.trim() === COMPILED_SAID)) {
13256
13289
  faults.push("it never said it had compiled them");
@@ -13390,7 +13423,7 @@ var init_suite_judge = __esm(() => {
13390
13423
  [105, "gdUnit4's code for scripts that did not compile"]
13391
13424
  ]);
13392
13425
  GODOT_EXITS = new Set([1, 255]);
13393
- CRASH_HEADER = /^(?:CrashHandlerException|handle_crash): Program crashed with signal (\d+)/;
13426
+ CRASH_HEADER = /^(?:CrashHandlerException|handle_crash): Program crashed(?: with signal (\d+))?$/;
13394
13427
  CRASH_BUILD = /^Engine version: .*\(([0-9a-f]{40})\)$/;
13395
13428
  QUEUE_OVERFLOW_FRAMES = new Map([
13396
13429
  ["ed1daf0bf001b61586d9930840f2f1394092c079", ["main+3e1082a", "main+3e0f4aa"]]
@@ -13969,11 +14002,14 @@ function runArguments(options) {
13969
14002
  }
13970
14003
  return args;
13971
14004
  }
13972
- function editorArguments(projectPath, ports, logFile, hidden, silent) {
14005
+ function editorArguments(projectPath, ports, logFile, hidden, silent, platform = process.platform) {
14006
+ const headless = hidden && platform !== "win32";
14007
+ const windowed = hidden && !headless;
13973
14008
  return [
13974
14009
  "-e",
13975
- ...hidden ? ["--headless"] : [],
13976
- ...silent && !hidden ? ["--audio-driver", "Dummy"] : [],
14010
+ ...headless ? ["--headless"] : [],
14011
+ ...windowed ? hiddenWindowFlags([], false) : [],
14012
+ ...(silent || hidden) && !headless ? ["--audio-driver", "Dummy"] : [],
13977
14013
  "--path",
13978
14014
  projectPath,
13979
14015
  "--lsp-port",
@@ -13981,12 +14017,13 @@ function editorArguments(projectPath, ports, logFile, hidden, silent) {
13981
14017
  "--dap-port",
13982
14018
  String(ports.dap),
13983
14019
  "--log-file",
13984
- logFile
14020
+ logFile,
14021
+ ...windowed ? ["--", HIDDEN_EDITOR_MARK] : []
13985
14022
  ];
13986
14023
  }
13987
14024
  function startedAs(commandLine) {
13988
14025
  const words = commandLineWords(commandLine);
13989
- const hidden = words.includes("--headless");
14026
+ const hidden = words.includes("--headless") || words.includes(HIDDEN_EDITOR_MARK);
13990
14027
  const driver = words.indexOf("--audio-driver");
13991
14028
  return { hidden, silent: hidden || driver >= 0 && words[driver + 1] === "Dummy" };
13992
14029
  }
@@ -14035,7 +14072,7 @@ function environmentFor2(options, variables = process.env) {
14035
14072
  const base = options.savesIn === undefined ? { ...variables } : userDataIn(options.savesIn, variables);
14036
14073
  return { ...base, ...options.env, GDHARNESS_RUNTIME_DIR: options.runtimeDirectory };
14037
14074
  }
14038
- var HIDDEN_WINDOW_FPS = 60, ENGINE_FLAGS, VALUE_KIND, SET_BY_ARGUMENT, OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", EDITOR_PID_VARIABLE = "GDHARNESS_EDITOR_PID", RESERVED_VARIABLE_PREFIX = "GDHARNESS_", SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
14075
+ var HIDDEN_WINDOW_FPS = 60, ENGINE_FLAGS, VALUE_KIND, SET_BY_ARGUMENT, HIDDEN_EDITOR_MARK = "--gdharness-hidden", OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", EDITOR_PID_VARIABLE = "GDHARNESS_EDITOR_PID", RESERVED_VARIABLE_PREFIX = "GDHARNESS_", SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
14039
14076
  var init_launch = __esm(() => {
14040
14077
  ENGINE_FLAGS = Object.freeze({
14041
14078
  "--verbose": "none",
@@ -14452,7 +14489,7 @@ function answersTo(entry, severity, contains) {
14452
14489
  var HEADLINE, CRASH, GDUNIT_CASE, CRASH_FILLER, COLOUR_CODE, STREAMS, ADMITTED, LOCATION2, FRAME, LONGEST_CYCLE = 64;
14453
14490
  var init_game_log = __esm(() => {
14454
14491
  HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
14455
- CRASH = /^(?:CrashHandlerException|handle_crash): Program crashed with signal \d+/;
14492
+ CRASH = /^(?:CrashHandlerException|handle_crash): Program crashed(?: with signal \d+)?\s*$/;
14456
14493
  GDUNIT_CASE = /^\s+res:\/\/.+?\.(?:gd|cs) > .+? (?:STARTED|PASSED|FAILED|ERROR|SKIPPED|FLAKY)(?:\s|$)/;
14457
14494
  CRASH_FILLER = /^(?:=+|-- END OF .* BACKTRACE --|\[\d+\] .* - no debug info in .*)$/;
14458
14495
  COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
@@ -15475,6 +15512,7 @@ var init_scratch = __esm(() => {
15475
15512
  KINDS = [
15476
15513
  "answers",
15477
15514
  "desktop",
15515
+ "editor-capture",
15478
15516
  "export",
15479
15517
  "import",
15480
15518
  "params",
@@ -19461,19 +19499,19 @@ var init_tool_definitions = __esm(() => {
19461
19499
  {
19462
19500
  name: "editor_launch",
19463
19501
  title: "Open, restart or close the editor, or bind its debug adapter again",
19464
- description: "Opens the Godot editor on a project, or restarts or closes the one already connected, or binds its debug adapter again. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it. An editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead. open answers once the process has started, and the editor dials in after its import. A call needing the editor made before then waits for the one this server opened, for as long as a restart waits, and is refused naming it if it has not arrived. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too. A hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one. It saves open scenes on the way out. An editor a server opened is started again by the server, hidden or visible and silent or not as it was started unless hidden or silent says otherwise, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. Godot tries each server port once, as the editor starts, so a restart binds a debug adapter that came back without its port again, in the editor that came back. rebind does the same for the connected editor without restarting it, whoever opened it. A debug session open on the adapter ends with it and is opened again by the next debug call. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so in editor_status rather than waiting for an editor that is not coming, and open finishes it. An open is refused when the editor it would start could never reach this server. Those are a project other than the one this server serves (its editor dials that project's own server), a project without the gdharness editor addon installed and enabled, and a project whose editor a restart is already bringing back.",
19502
+ description: "Opens the Godot editor on a project, or restarts or closes the one already connected, or binds its debug adapter again. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it. An editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead. open answers once the process has started, and the editor dials in after its import. A call needing the editor made before then waits for the one this server opened, for as long as a restart waits, and is refused naming it if it has not arrived. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it out of sight instead. On Windows it gets a desktop of its own, shared by the games it plays, and still draws its views for editor_capture. On Linux and macOS it is headless, so editor_capture refuses it. A hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one. It saves open scenes on the way out. An editor a server opened is started again by the server, hidden or visible and silent or not as it was started unless hidden or silent says otherwise, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. Godot tries each server port once, as the editor starts, so a restart binds a debug adapter that came back without its port again, in the editor that came back. rebind does the same for the connected editor without restarting it, whoever opened it. A debug session open on the adapter ends with it and is opened again by the next debug call. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so in editor_status rather than waiting for an editor that is not coming, and open finishes it. An open is refused when the editor it would start could never reach this server. Those are a project other than the one this server serves (its editor dials that project's own server), a project without the gdharness editor addon installed and enabled, and a project whose editor a restart is already bringing back.",
19465
19503
  answer: "close answers settingsDropped, naming what its save took out of project.godot and left out when it took nothing, and settingsNote when the file could not be read to tell. An editor stopped before it connected is answered with beforeConnecting. close and restart name under savedScenes the open scenes that had unsaved changes and were saved on the way out, an empty list when none had. A scene that would not save refuses the close or restart and leaves the editor running. Both answer tookMs, how long the editor took to go or to come back. restart answers with the version that came back, and hidden, whether it came back with no window, and says under settingsDropped which keys its save dropped, because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart answers lspListening and dapListening once the editor's servers are up. When either came back without its port, serversSaid gives the lines the editor's log panel has about its servers, the one place Godot says why a port would not bind, and when it was the debug adapter, which is bound again in the editor that came back, debugAdapterRebound says whether that took. Neither is there when both came up. Godot clears that panel whenever a game is played, unless its run/output/always_clear_output_on_play setting is off, so after a play the lines from the editor's own start are gone. rebind answers rebound, with lspListening, dapListening and serversSaid, the lines that rebind made the panel print. A restart that a server's end interrupted shows as restartInterrupted in editor_status, and the open that finishes it says so under finishesRestart.",
19466
19504
  parameters: {
19467
19505
  projectPath: { ...PROJECT_PATH, ops: ["open"] },
19468
19506
  hidden: {
19469
19507
  type: "boolean",
19470
19508
  ops: ["open", "restart"],
19471
- description: "Open the editor headless and, on Windows, on a desktop of its own, so no window of it or of the games it plays appears. Default false on open: one visible editor, shown without taking the focus. On restart only for an editor a server opened, where the default is how the editor being replaced was started, read from its process. One whose start cannot be read is left running and refused until hidden is passed. One opened by hand comes back as it was, so hidden is refused there."
19509
+ description: "Open the editor out of sight, with the games it plays: on a desktop of its own on Windows, and headless elsewhere. Default false on open: one visible editor, shown without taking the focus. On restart it applies only to an editor a server opened. The default there is how the replaced editor was started, read from its process. One whose start cannot be read is left running and refused until hidden is passed. One opened by hand comes back as it was, so hidden is refused there."
19472
19510
  },
19473
19511
  silent: {
19474
19512
  type: "boolean",
19475
19513
  ops: ["open", "restart"],
19476
- description: "Open the editor with no sound out of the machine, started with --audio-driver Dummy, so a tool script, a preview or a scene playing in its viewport is heard by nobody. Default true on open, and on restart how the editor being replaced was started: pass false to hear it. The answer's arguments are what the editor was started with. The editor hands no audio driver to a game it plays, so editor_run's own silent decides that game's sound. A hidden editor is headless and silent either way, so its arguments carry no --audio-driver. On restart only for an editor a server opened. One opened by hand comes back as it was."
19514
+ description: "Open the editor with no sound out of the machine, started with --audio-driver Dummy, so a tool script, a preview or a scene playing in its viewport is heard by nobody. Default true on open, and on restart how the editor being replaced was started: pass false to hear it. The answer's arguments are what the editor was started with. The editor hands no audio driver to a game it plays, so editor_run's own silent decides that game's sound. A hidden editor is silent whatever silent says. A headless editor has no sound to turn off, so its arguments show no --audio-driver. On restart only for an editor a server opened. One opened by hand comes back as it was."
19477
19515
  }
19478
19516
  },
19479
19517
  requires: [],
@@ -19714,7 +19752,7 @@ var init_tool_definitions = __esm(() => {
19714
19752
  title: "Editor status",
19715
19753
  effect: "read",
19716
19754
  description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and which games with the runtime addon are reachable, of this project and of every other on the machine. Read connected with mayYetConnect: the editor dials this server and backs off between tries, so for the first half-minute after a server starts false means not reached yet as often as no editor. mayYetConnect true is worth waiting out, false is final, and editorCannotConnect, restartInterrupted, restartUnderway, closedHere and editorsNotYetConnected say why. An editor that has connected and not yet greeted reports nothing about itself, and every other call made in that window waits up to ten seconds for it to settle. playingInEditor null means unanswered rather than nothing playing: processActive and runtimes do not depend on the editor answering. game.run names the run editor_output and editor_run stop answer for, and stays named after it ends. addonIsStale, on a connected editor and on each running game (absent with neither), says it runs addon code other than what this server ships, and staleNote says which half moves: the project, by a gdharness upgrade it spells out to run in a shell, the editor by a restart, or this server by a reconnect. A false editor.lspListening or editor.dapListening comes with serversNote: editor_launch rebind binds the debug adapter again in place, and editor_launch restart the language server. editor.scan says whether the editor is scanning, which editor_run waits out before starting a game. A game announced in a protocol this server does not speak is listed under unreadable, during an upgrade that moves the protocol, until the halves match.",
19717
- answer: "projectIs, when present, is the gdharness version of the addons in the project, which differs from this server's (project_info says which of the two to move), and configIs, when present, the version the project's MCP config starts, which differs from this server's, so the next connection runs that one. server names this server: its pid, its version, and lifecycleLog, the file every gdharness server of the user appends a line to when it starts, when its client first names itself, when it replaces another server of its project, and when it ends, with why it ended (the client closing its end as stdin:end, a signal by name, superseded with the successor's pid, an uncaught exception or rejection with its stack, or the exit code) and the calls it was answering then. The parent's command line follows on its own line a few seconds after the start. A server killed without warning writes no ended line, and on Windows every kill is that, so a started line with no ended line after it is a server something killed. " + "runtimeConnected counts the games of the project this server serves on this release's runtime addon, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath, and userDir, the directory its user:// is, where its saves and settings are read. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Its play says which play the editor is on: play.play counts the plays that editor has started since it opened, whoever started them, and play.editorPid is the editor, so two reads with different pairs are two plays even when both say playing. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed. It is null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote: editor_launch rebind binds the debug adapter again in place, and editor_launch restart the language server. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. editor.openScenes lists the scenes open in the editor's tabs, leaving out a tab of a scene never saved, editor.editedScene the one it is showing (null when none is, or when that scene was never saved), and editor.unsavedScenes those with changes not yet written to disk, where an empty entry is a scene never saved. All three come with scan and are absent when it is null. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout. For the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself. A game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say). addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs the gdharness upgrade command, spelled out to run in a shell, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. A runtime_* call to such a game is refused with the staleNote and not sent. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out. False is an editor that is not there. False beside editorCannotConnect is a project without the gdharness editor addon installed or enabled, where no editor can ever dial in, however young the server or whichever editor is open. Its remedies say how to install or enable it. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for. editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in and took a key out of it, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. A stale editor is asked nothing but to quit: every other call to it is refused with staleNote, which names editor_launch restart, the upgrade command or a reconnect. port is the one the editor bridge is on. When that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds. bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it. startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and from the socket opening until its greeting and that read are done, identifying is true and connected false. One started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window. serverVersion is the gdharness version of this server. bridgeAvailable says the editor bridge is listening. When it failed to start, startupError gives the error, and retryingBridge is true while this server tries again. announcedAt is the file where this server told editors of its project which port to dial. connectedAt is when the editor connected, and lastPongAt when it last answered this server's keep-alive ping. addonDigest is a hash of the editor addon's code, which addonIsStale compares with what this server ships. debugPort is the port of the editor's remote debugger, the one games it plays connect to. openedByAServer says a gdharness server started this editor, syncsBreakpoints that it keeps its breakpoints when a debug session opens, which Godot otherwise clears, and hidden that it runs headless with no window anybody can see. pendingRequests counts the requests sent to the editor and not yet answered, and queuedResources the files with writes waiting behind another write to the same file.",
19755
+ answer: "projectIs, when present, is the gdharness version of the addons in the project, which differs from this server's (project_info says which of the two to move), and configIs, when present, the version the project's MCP config starts, which differs from this server's, so the next connection runs that one. server names this server: its pid, its version, and lifecycleLog, the file every gdharness server of the user appends a line to when it starts, when its client first names itself, when it replaces another server of its project, and when it ends, with why it ended (the client closing its end as stdin:end, a signal by name, superseded with the successor's pid, an uncaught exception or rejection with its stack, or the exit code) and the calls it was answering then. The parent's command line follows on its own line a few seconds after the start. A server killed without warning writes no ended line, and on Windows every kill is that, so a started line with no ended line after it is a server something killed. " + "runtimeConnected counts the games of the project this server serves on this release's runtime addon, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath, and userDir, the directory its user:// is, where its saves and settings are read. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Its play says which play the editor is on: play.play counts the plays that editor has started since it opened, whoever started them, and play.editorPid is the editor, so two reads with different pairs are two plays even when both say playing. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed. It is null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote: editor_launch rebind binds the debug adapter again in place, and editor_launch restart the language server. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. editor.openScenes lists the scenes open in the editor's tabs, leaving out a tab of a scene never saved, editor.editedScene the one it is showing (null when none is, or when that scene was never saved), and editor.unsavedScenes those with changes not yet written to disk, where an empty entry is a scene never saved. All three come with scan and are absent when it is null. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout. For the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself. A game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say). addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs the gdharness upgrade command, spelled out to run in a shell, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. A runtime_* call to such a game is refused with the staleNote and not sent. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out. False is an editor that is not there. False beside editorCannotConnect is a project without the gdharness editor addon installed or enabled, where no editor can ever dial in, however young the server or whichever editor is open. Its remedies say how to install or enable it. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for. editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in and took a key out of it, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. A stale editor is asked nothing but to quit: every other call to it is refused with staleNote, which names editor_launch restart, the upgrade command or a reconnect. port is the one the editor bridge is on. When that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds. bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it. startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and from the socket opening until its greeting and that read are done, identifying is true and connected false. One started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window. serverVersion is the gdharness version of this server. bridgeAvailable says the editor bridge is listening. When it failed to start, startupError gives the error, and retryingBridge is true while this server tries again. announcedAt is the file where this server told editors of its project which port to dial. connectedAt is when the editor connected, and lastPongAt when it last answered this server's keep-alive ping. addonDigest is a hash of the editor addon's code, which addonIsStale compares with what this server ships. debugPort is the port of the editor's remote debugger, the one games it plays connect to. openedByAServer says a gdharness server started this editor, syncsBreakpoints that it keeps its breakpoints when a debug session opens, which Godot otherwise clears, and hidden that it runs with no window anybody can see. pendingRequests counts the requests sent to the editor and not yet answered, and queuedResources the files with writes waiting behind another write to the same file.",
19718
19756
  parameters: {},
19719
19757
  requires: []
19720
19758
  },
@@ -19744,6 +19782,41 @@ var init_tool_definitions = __esm(() => {
19744
19782
  },
19745
19783
  requires: ["projectPath"]
19746
19784
  },
19785
+ {
19786
+ name: "editor_capture",
19787
+ title: "Capture the editor's view of a scene",
19788
+ effect: "read",
19789
+ description: "A picture of a scene as the editor's 2D or 3D view draws it, including what its @tool scripts draw under Engine.is_editor_hint(). No game and no headless engine runs that branch, so runtime_capture cannot show it. The editor opens the scene, shows its 2D or 3D view for a few frames and captures it. It then returns to the scene and view it showed before, and closes the scene again if it was not open. The view is the editor's own, at its zoom and scroll for that scene, and in 3D with its grid and gizmos. A scene open with unsaved changes is captured as the editor holds it. Other calls to the editor wait for the capture. Needs a connected editor that draws: a visible one, or a hidden one on Windows. A hidden editor on Linux or macOS is headless and is refused.",
19790
+ answer: "The picture, with its size in pixels and which view drew it. When width or height scaled the picture, it also gives the size the view drew at. A scene with unsaved changes in the editor says so.",
19791
+ parameters: {
19792
+ projectPath: PROJECT_PATH,
19793
+ scenePath: SCENE_PATH,
19794
+ view: {
19795
+ type: "string",
19796
+ enum: ["2d", "3d"],
19797
+ description: "Which of the editor's views draws the scene. Default 3d for a scene whose root is a Node3D, and 2d otherwise."
19798
+ },
19799
+ width: {
19800
+ type: "integer",
19801
+ minimum: 1,
19802
+ description: "Scale the picture to this width. Given alone, the height keeps the picture's proportions."
19803
+ },
19804
+ height: {
19805
+ type: "integer",
19806
+ minimum: 1,
19807
+ description: "Scale the picture to this height. Given alone, the width keeps the picture's proportions."
19808
+ },
19809
+ outputPath: {
19810
+ type: "string",
19811
+ description: "Also save the picture as a lossless PNG at this absolute path. The answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
19812
+ },
19813
+ inline: {
19814
+ type: "boolean",
19815
+ description: "Send the picture back in the answer. Default true, as a JPEG when that is smaller than the PNG and the picture has no transparency. false sends none and needs outputPath."
19816
+ }
19817
+ },
19818
+ requires: ["projectPath", "scenePath"]
19819
+ },
19747
19820
  {
19748
19821
  name: "editor_classes",
19749
19822
  title: "Engine classes",
@@ -51102,27 +51175,109 @@ var init_test_selection = __esm(() => {
51102
51175
  CASE_AFTER_SCRIPT = /^(.*\.gd):(\w+)$/;
51103
51176
  });
51104
51177
 
51178
+ // src/write-turns.ts
51179
+ import { createHash as createHash8 } from "node:crypto";
51180
+ import { closeSync as closeSync6, mkdirSync as mkdirSync23, openSync as openSync7, readFileSync as readFileSync41, rmSync as rmSync21, statSync as statSync20, writeSync as writeSync5 } from "node:fs";
51181
+ import { join as join45, relative as relative10 } from "node:path";
51182
+ import process18 from "node:process";
51183
+ import { setTimeout as delay8 } from "node:timers/promises";
51184
+ function writeLockOf(projectPath, file) {
51185
+ const named = relative10(projectPath, file).split("\\").join("/");
51186
+ const key = process18.platform === "win32" ? named.toLowerCase() : named;
51187
+ const hash = createHash8("sha256").update(key).digest("hex").slice(0, 16);
51188
+ return join45(projectPath, ".godot", WRITES_FOLDER, `${hash}.lock`);
51189
+ }
51190
+ async function inTurnFor(projectPath, file, write) {
51191
+ const lock = writeLockOf(projectPath, file);
51192
+ mkdirSync23(join45(projectPath, ".godot", WRITES_FOLDER), { recursive: true });
51193
+ const holder = JSON.stringify({ pid: process18.pid, started: await ownStart(), file });
51194
+ const starts = new Map;
51195
+ while (!made(lock, holder) && !await takenOver(lock, holder, starts)) {
51196
+ await delay8(TURN_LOOK_MS);
51197
+ }
51198
+ try {
51199
+ return await write();
51200
+ } finally {
51201
+ removeWhole(lock);
51202
+ }
51203
+ }
51204
+ function made(path, content) {
51205
+ let file;
51206
+ try {
51207
+ file = openSync7(path, "wx");
51208
+ } catch {
51209
+ return false;
51210
+ }
51211
+ try {
51212
+ writeSync5(file, content);
51213
+ } finally {
51214
+ closeSync6(file);
51215
+ }
51216
+ return true;
51217
+ }
51218
+ async function takenOver(lock, holder, starts) {
51219
+ if (!await gone2(lock, starts)) {
51220
+ return false;
51221
+ }
51222
+ const steal = `${lock}.steal`;
51223
+ if (!made(steal, holder)) {
51224
+ try {
51225
+ if (Date.now() - statSync20(steal).mtimeMs > UNWRITTEN_MS) {
51226
+ rmSync21(steal, { force: true });
51227
+ }
51228
+ } catch {}
51229
+ return false;
51230
+ }
51231
+ try {
51232
+ return await gone2(lock, starts) && removeWhole(lock) && made(lock, holder);
51233
+ } finally {
51234
+ rmSync21(steal, { force: true });
51235
+ }
51236
+ }
51237
+ async function gone2(lock, starts) {
51238
+ let content;
51239
+ let writtenAt;
51240
+ try {
51241
+ content = readFileSync41(lock, "utf8");
51242
+ writtenAt = statSync20(lock).mtimeMs;
51243
+ } catch {
51244
+ return false;
51245
+ }
51246
+ const pid = holderOf(content)?.pid;
51247
+ if (pid !== undefined && !starts.has(pid) && alive(pid)) {
51248
+ starts.set(pid, (await startTimesOf([pid])).get(pid));
51249
+ }
51250
+ return lockIsFree(content, writtenAt, Date.now(), alive, (asked) => starts.get(asked));
51251
+ }
51252
+ var WRITES_FOLDER = "gdharness-writes", TURN_LOOK_MS = 100;
51253
+ var init_write_turns = __esm(() => {
51254
+ init_alive();
51255
+ init_engine_slots();
51256
+ init_process_children();
51257
+ init_whole_file();
51258
+ });
51259
+
51105
51260
  // src/server.ts
51106
51261
  import { randomUUID as randomUUID2 } from "node:crypto";
51107
51262
  import {
51108
51263
  appendFileSync as appendFileSync2,
51109
- closeSync as closeSync6,
51264
+ closeSync as closeSync7,
51110
51265
  constants,
51111
51266
  copyFileSync as copyFileSync2,
51112
51267
  existsSync as existsSync36,
51113
51268
  fstatSync as fstatSync3,
51114
- mkdirSync as mkdirSync23,
51115
- openSync as openSync7,
51269
+ mkdirSync as mkdirSync24,
51270
+ openSync as openSync8,
51116
51271
  readdirSync as readdirSync21,
51117
- readFileSync as readFileSync41,
51272
+ readFileSync as readFileSync42,
51118
51273
  readSync as readSync4,
51119
51274
  realpathSync as realpathSync7,
51120
51275
  rmdirSync as rmdirSync2,
51121
- statSync as statSync20,
51276
+ statSync as statSync21,
51122
51277
  writeFileSync as writeFileSync24
51123
51278
  } from "node:fs";
51124
- import { basename as basename14, dirname as dirname25, extname as extname6, isAbsolute as isAbsolute9, join as join45, normalize as normalize2, relative as relative10 } from "node:path";
51125
- import { setTimeout as delay8 } from "node:timers/promises";
51279
+ import { basename as basename14, dirname as dirname25, extname as extname6, isAbsolute as isAbsolute9, join as join46, normalize as normalize2, relative as relative11 } from "node:path";
51280
+ import { setTimeout as delay9 } from "node:timers/promises";
51126
51281
  import { fileURLToPath as fileURLToPath6 } from "node:url";
51127
51282
  function editorRestartTimeoutMs() {
51128
51283
  return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
@@ -51135,9 +51290,9 @@ function greetingWaitMs() {
51135
51290
  return waitFromEnv("GDHARNESS_GREETING_WAIT_MS", 1e4);
51136
51291
  }
51137
51292
  function pictureToSend(pngPath, jpegPath) {
51138
- const png = readFileSync41(pngPath);
51293
+ const png = readFileSync42(pngPath);
51139
51294
  const beside = pngPath.replace(/\.png$/, ".jpg");
51140
- const jpeg = jpegPath !== undefined && normalize2(jpegPath) === normalize2(beside) && existsSync36(beside) ? readFileSync41(beside) : null;
51295
+ const jpeg = jpegPath !== undefined && normalize2(jpegPath) === normalize2(beside) && existsSync36(beside) ? readFileSync42(beside) : null;
51141
51296
  const sent = jpeg !== null && jpeg.length < png.length ? jpeg : png;
51142
51297
  return {
51143
51298
  data: sent.toString("base64"),
@@ -51181,7 +51336,7 @@ function writtenFileRefusal(path, rule) {
51181
51336
  if (!existsSync36(directory)) {
51182
51337
  return `${argument}'s directory ${dirname25(path)} does not exist. ${whatOpening} creates no directories.`;
51183
51338
  }
51184
- const where = join45(directory, basename14(path));
51339
+ const where = join46(directory, basename14(path));
51185
51340
  const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realAsFarAsItExists(project), where));
51186
51341
  if (inside !== undefined) {
51187
51342
  return `${argument} ${path} is inside the project at ${inside}. ${whatOpening} is saved outside it, so it never becomes part of the game.`;
@@ -51277,7 +51432,7 @@ function keptGameRequest(tool, op, args) {
51277
51432
  }
51278
51433
  }
51279
51434
  function projectSpelling(projectPath, absolutePath) {
51280
- return `res://${relative10(realAsFarAsItExists(projectPath), realAsFarAsItExists(absolutePath)).replaceAll("\\", "/")}`;
51435
+ return `res://${relative11(realAsFarAsItExists(projectPath), realAsFarAsItExists(absolutePath)).replaceAll("\\", "/")}`;
51281
51436
  }
51282
51437
  function whichGame(args) {
51283
51438
  return { projectPath: args["projectPath"], pid: args["pid"] };
@@ -51408,7 +51563,7 @@ async function untilGone(pids, withinMs = stopWaitMs()) {
51408
51563
  const gone = () => pids.every((pid) => !alive(pid));
51409
51564
  const deadline = Date.now() + withinMs;
51410
51565
  while (!gone() && Date.now() < deadline) {
51411
- await delay8(50);
51566
+ await delay9(50);
51412
51567
  }
51413
51568
  return gone();
51414
51569
  }
@@ -51612,7 +51767,7 @@ function endedToStartThis(ended) {
51612
51767
  return ` ${which} was ended to start this one. Its output is no longer what editor_output answers about.`;
51613
51768
  }
51614
51769
  function lineNothingStopsOn(absolutePath, relativePath, line) {
51615
- const text = readFileSync41(absolutePath, "utf8");
51770
+ const text = readFileSync42(absolutePath, "utf8");
51616
51771
  const lines = text === "" ? 0 : text.split(`
51617
51772
  `).length - (text.endsWith(`
51618
51773
  `) ? 1 : 0);
@@ -51820,7 +51975,7 @@ function notRunNote(notRun, failFast) {
51820
51975
  function presetsIn(projectPath) {
51821
51976
  let text;
51822
51977
  try {
51823
- text = readFileSync41(join45(projectPath, "export_presets.cfg"), "utf8");
51978
+ text = readFileSync42(join46(projectPath, "export_presets.cfg"), "utf8");
51824
51979
  } catch {
51825
51980
  return null;
51826
51981
  }
@@ -51927,7 +52082,7 @@ function filesIn(folder) {
51927
52082
  try {
51928
52083
  for (const entry of readdirSync21(folder, { withFileTypes: true })) {
51929
52084
  if (entry.isFile()) {
51930
- found.set(entry.name, statSync20(join45(folder, entry.name)).mtimeMs);
52085
+ found.set(entry.name, statSync21(join46(folder, entry.name)).mtimeMs);
51931
52086
  }
51932
52087
  }
51933
52088
  } catch {}
@@ -51967,7 +52122,7 @@ function unsavedInEditorNote(scripts) {
51967
52122
  return `${listed} ${one ? "is" : "are"} open in the editor's script ${one ? "tab" : "tabs"} with unsaved edits, and saving ${one ? "that tab" : "them"} there writes ${one ? "its" : "their"} text over what was written here: close ${one ? "it" : "them"} without saving, or save first and edit again.`;
51968
52123
  }
51969
52124
  function editorAddonMissing(projectPath) {
51970
- if (!existsSync36(join45(projectPath, "project.godot"))) {
52125
+ if (!existsSync36(join46(projectPath, "project.godot"))) {
51971
52126
  return null;
51972
52127
  }
51973
52128
  const report = inspectProject(projectPath);
@@ -51995,8 +52150,8 @@ function rewriteBusNamed(projectPath, from, to) {
51995
52150
  const giving = [`bus = &"${quoted(from)}"`, `bus = "${quoted(from)}"`];
51996
52151
  const rewritten = [];
51997
52152
  for (const file of projectFiles(projectPath, [".tscn", ".tres"])) {
51998
- const absolute = join45(projectPath, file);
51999
- const lines = readFileSync41(absolute, "utf8").split(`
52153
+ const absolute = join46(projectPath, file);
52154
+ const lines = readFileSync42(absolute, "utf8").split(`
52000
52155
  `);
52001
52156
  const at = [];
52002
52157
  const after = lines.map((line, index) => {
@@ -52015,7 +52170,7 @@ function rewriteBusNamed(projectPath, from, to) {
52015
52170
  }
52016
52171
  const namedInScripts = [];
52017
52172
  for (const file of projectFiles(projectPath, [".gd"])) {
52018
- readFileSync41(join45(projectPath, file), "utf8").split(`
52173
+ readFileSync42(join46(projectPath, file), "utf8").split(`
52019
52174
  `).forEach((line, index) => {
52020
52175
  if (line.includes(`"${from}"`)) {
52021
52176
  namedInScripts.push({ file: `res://${file}`, line: index + 1, text: line.trim() });
@@ -52306,7 +52461,7 @@ function profileFrameFunctions(projectPath) {
52306
52461
  let projectGodot = null;
52307
52462
  if (projectPath !== null) {
52308
52463
  try {
52309
- projectGodot = readFileSync41(join45(projectPath, "project.godot"), "utf8");
52464
+ projectGodot = readFileSync42(join46(projectPath, "project.godot"), "utf8");
52310
52465
  } catch {}
52311
52466
  }
52312
52467
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -52330,7 +52485,7 @@ function projectSourceOf(projectPath) {
52330
52485
  let source = null;
52331
52486
  if (inside.ok) {
52332
52487
  try {
52333
- source = readFileSync41(inside.absolutePath, "utf8");
52488
+ source = readFileSync42(inside.absolutePath, "utf8");
52334
52489
  } catch {}
52335
52490
  }
52336
52491
  read.set(script, source);
@@ -52502,7 +52657,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
52502
52657
  }
52503
52658
  function gdUnitFailsOnScriptErrors(projectPath) {
52504
52659
  try {
52505
- return settingKeys(readFileSync41(join45(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
52660
+ return settingKeys(readFileSync42(join46(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
52506
52661
  } catch {
52507
52662
  return true;
52508
52663
  }
@@ -52517,7 +52672,7 @@ function uncaughtNote(uncaught) {
52517
52672
  }
52518
52673
  function elsewhereIn(projectPath, asked) {
52519
52674
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
52520
- if (wanted === "" || existsSync36(join45(projectPath, wanted))) {
52675
+ if (wanted === "" || existsSync36(join46(projectPath, wanted))) {
52521
52676
  return "";
52522
52677
  }
52523
52678
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -52540,7 +52695,7 @@ function windowNoteFor(needing, sharing) {
52540
52695
  return `${named(needing)} ${one ? "needs" : "need"} a window, as gdharness test reads ${one ? "it" : "them"}, so the run had one: the engine ran without --headless, at the size project.godot gives its viewport, with no sound${process.platform === "win32" ? ", on a desktop of its own, where its window neither shows nor takes the keyboard" : ""}. ${why}${shared}`;
52541
52696
  }
52542
52697
  function testPathsIn(projectPath, given, argument = "path") {
52543
- const defaults = TEST_DIRECTORIES.filter((directory) => existsSync36(join45(projectPath, directory)));
52698
+ const defaults = TEST_DIRECTORIES.filter((directory) => existsSync36(join46(projectPath, directory)));
52544
52699
  const entries = given === undefined || given === null || given === "" ? defaults.length > 0 ? defaults : TEST_DIRECTORIES : given;
52545
52700
  const listed = Array.isArray(entries) ? entries : [entries];
52546
52701
  if (listed.length === 0) {
@@ -52643,17 +52798,17 @@ function asWarning(raised) {
52643
52798
  }
52644
52799
  function isDirectory(path) {
52645
52800
  try {
52646
- return statSync20(path).isDirectory();
52801
+ return statSync21(path).isDirectory();
52647
52802
  } catch {
52648
52803
  return false;
52649
52804
  }
52650
52805
  }
52651
52806
  function mainSceneOf(projectFile) {
52652
- const scene = parseProjectGodot(readFileSync41(projectFile, "utf8"))["application"]?.["run/main_scene"];
52807
+ const scene = parseProjectGodot(readFileSync42(projectFile, "utf8"))["application"]?.["run/main_scene"];
52653
52808
  return typeof scene === "string" && scene !== "" ? scene : null;
52654
52809
  }
52655
52810
  function editorPlaysHeadless(projectFile) {
52656
- const runArgs = parseProjectGodot(readFileSync41(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
52811
+ const runArgs = parseProjectGodot(readFileSync42(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
52657
52812
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
52658
52813
  }
52659
52814
  function ownArguments(params) {
@@ -52685,7 +52840,7 @@ function unheldNote(changed) {
52685
52840
  class GodotServer {
52686
52841
  stdio = null;
52687
52842
  locator = new GodotLocator;
52688
- operationsScript = join45(__dirname2, "godot", "operations", "godot_operations.gd");
52843
+ operationsScript = join46(__dirname2, "godot", "operations", "godot_operations.gd");
52689
52844
  exportOptionsRead = new Map;
52690
52845
  heldWhilePending = new Set;
52691
52846
  evaluatedWhilePending = new Set;
@@ -52844,7 +52999,7 @@ class GodotServer {
52844
52999
  taken = true;
52845
53000
  this.logDebug(`Took port ${wanted} over from pid ${announced.pid}`);
52846
53001
  } catch {
52847
- await delay8(HANDOVER_POLL_MS);
53002
+ await delay9(HANDOVER_POLL_MS);
52848
53003
  }
52849
53004
  }
52850
53005
  this.notHandedOverBy = taken ? null : announced.pid;
@@ -52975,7 +53130,7 @@ class GodotServer {
52975
53130
  }
52976
53131
  noteTheStart() {
52977
53132
  const cwd = process.cwd();
52978
- const mcpJson = join45(cwd, ".mcp.json");
53133
+ const mcpJson = join46(cwd, ".mcp.json");
52979
53134
  const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
52980
53135
  const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
52981
53136
  writeLifecycle("started", {
@@ -53124,7 +53279,7 @@ class GodotServer {
53124
53279
  return checked.response;
53125
53280
  }
53126
53281
  const project = args["projectPath"];
53127
- if (typeof project === "string" && existsSync36(join45(project, "project.godot"))) {
53282
+ if (typeof project === "string" && existsSync36(join46(project, "project.godot"))) {
53128
53283
  this.lastProjectPath = project;
53129
53284
  }
53130
53285
  this.updates.refresh();
@@ -53167,7 +53322,7 @@ class GodotServer {
53167
53322
  let notSaved;
53168
53323
  try {
53169
53324
  savedAt = answer.content.map((item, index) => {
53170
- const file = join45(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
53325
+ const file = join46(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
53171
53326
  writeFileSync24(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
53172
53327
  return file;
53173
53328
  });
@@ -53631,6 +53786,8 @@ class GodotServer {
53631
53786
  return await this.handleEditorStatus();
53632
53787
  case "editor_rescan":
53633
53788
  return await this.handleRescanFilesystem(args);
53789
+ case "editor_capture":
53790
+ return await this.handleEditorCapture(args);
53634
53791
  case "runtime_inspect": {
53635
53792
  const patience = readPositiveNumber(args, "timeoutMs") ?? this.runtimeTimeoutMs();
53636
53793
  switch (op) {
@@ -53864,7 +54021,7 @@ class GodotServer {
53864
54021
  if (path === undefined) {
53865
54022
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
53866
54023
  }
53867
- const file = join45(path, "project.godot");
54024
+ const file = join46(path, "project.godot");
53868
54025
  if (!existsSync36(file)) {
53869
54026
  return {
53870
54027
  ok: false,
@@ -54614,7 +54771,7 @@ class GodotServer {
54614
54771
  if (!engine.ok) {
54615
54772
  return engine.response;
54616
54773
  }
54617
- const application = parseProjectGodot(readFileSync41(project.value.file, "utf8"))["application"] ?? {};
54774
+ const application = parseProjectGodot(readFileSync42(project.value.file, "utf8"))["application"] ?? {};
54618
54775
  const name = application["config/name"];
54619
54776
  const mainScene = application["run/main_scene"];
54620
54777
  const info = {
@@ -54768,7 +54925,7 @@ class GodotServer {
54768
54925
  const platform = presetPlatform(project.value.path, preset);
54769
54926
  if (platform !== null) {
54770
54927
  const known = await this.exportOptions(platform, {
54771
- presetsFile: join45(project.value.path, "export_presets.cfg"),
54928
+ presetsFile: join46(project.value.path, "export_presets.cfg"),
54772
54929
  preset,
54773
54930
  asked: options
54774
54931
  });
@@ -54819,7 +54976,7 @@ ${platform}`;
54819
54976
  }
54820
54977
  const empty = scratchDirectory("export");
54821
54978
  try {
54822
- writeFileSync24(join45(empty, "project.godot"), `config_version=5
54979
+ writeFileSync24(join46(empty, "project.godot"), `config_version=5
54823
54980
  `);
54824
54981
  const outcome = await runOperation({ godotPath: engine.value, script: this.operationsScript, debug: DEBUG_MODE, editor: true }, "export_options", { platform, ...standing }, empty);
54825
54982
  const options = outcome.ok ? outcome.payload["options"] : undefined;
@@ -54858,7 +55015,7 @@ ${platform}`;
54858
55015
  }
54859
55016
  const folder = dirname25(output.absolutePath);
54860
55017
  try {
54861
- mkdirSync23(folder, { recursive: true });
55018
+ mkdirSync24(folder, { recursive: true });
54862
55019
  } catch (error) {
54863
55020
  return this.createErrorResponse(`Cannot create ${dirname25(output.relativePath)} to export into: ${errorMessage(error)}`);
54864
55021
  }
@@ -54876,7 +55033,7 @@ ${platform}`;
54876
55033
  const exportArgs = [
54877
55034
  "--headless",
54878
55035
  "--log-file",
54879
- join45(exportLogs, "engine.log"),
55036
+ join46(exportLogs, "engine.log"),
54880
55037
  "--path",
54881
55038
  project.value.path,
54882
55039
  pack ? "--export-pack" : debug ? "--export-debug" : "--export-release",
@@ -54884,7 +55041,7 @@ ${platform}`;
54884
55041
  output.absolutePath
54885
55042
  ];
54886
55043
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
54887
- const writtenAt = () => existsSync36(output.absolutePath) ? statSync20(output.absolutePath).mtimeMs : null;
55044
+ const writtenAt = () => existsSync36(output.absolutePath) ? statSync21(output.absolutePath).mtimeMs : null;
54888
55045
  const caches = editorCacheDirectory(engine.value);
54889
55046
  const exportOnce = async () => {
54890
55047
  const before = writtenAt();
@@ -54914,7 +55071,7 @@ ${platform}`;
54914
55071
  return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
54915
55072
  }
54916
55073
  const beside = writtenBeside(listedBefore, filesIn(folder), basename14(output.absolutePath), dirname25(output.relativePath).replaceAll("\\", "/"));
54917
- return exportAnswer(ending, { preset, outputPath: output.relativePath, debug, ...pack ? { pack } : {} }, before, writtenAt(), beside, docCacheNote, existsSync36(output.absolutePath) ? statSync20(output.absolutePath).size : null, importedFirstAnswer(imported));
55074
+ return exportAnswer(ending, { preset, outputPath: output.relativePath, debug, ...pack ? { pack } : {} }, before, writtenAt(), beside, docCacheNote, existsSync36(output.absolutePath) ? statSync21(output.absolutePath).size : null, importedFirstAnswer(imported));
54918
55075
  }
54919
55076
  reportTestProgress(log) {
54920
55077
  const progress = callProgress();
@@ -54975,7 +55132,7 @@ ${platform}`;
54975
55132
  if (movie !== null) {
54976
55133
  return this.createErrorResponse(movie);
54977
55134
  }
54978
- const missing = paths.value.filter((path) => !existsSync36(join45(project.value.path, path.slice("res://".length))));
55135
+ const missing = paths.value.filter((path) => !existsSync36(join46(project.value.path, path.slice("res://".length))));
54979
55136
  const scripts = new ProjectScripts(project.value.path);
54980
55137
  const present = paths.value.filter((path) => !missing.includes(path));
54981
55138
  const asked = scripts.searchedAsTold(present);
@@ -55067,10 +55224,10 @@ ${platform}`;
55067
55224
  const ignored = readStringArray(args, "ignore") ?? [];
55068
55225
  const declaredUnder = ignored.length === 0 ? [] : asked.flatMap((path) => path.endsWith(".gd") ? scripts.suite(path) ?? [] : scripts.suitesUnder([path]));
55069
55226
  const userData = scratchDirectory("tests");
55070
- const suitesFile = join45(userData, "suites.txt");
55071
- const skippingFile = join45(userData, "skipping.txt");
55072
- const engineLog = join45(userData, "engine.log");
55073
- const expectedFile = join45(userData, "expected.json");
55227
+ const suitesFile = join46(userData, "suites.txt");
55228
+ const skippingFile = join46(userData, "skipping.txt");
55229
+ const engineLog = join46(userData, "engine.log");
55230
+ const expectedFile = join46(userData, "expected.json");
55074
55231
  writeFileSync24(suitesFile, `${(selection.value?.suites ?? asked).join(`
55075
55232
  `)}
55076
55233
  `);
@@ -55111,11 +55268,11 @@ ${platform}`;
55111
55268
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? TEST_TIMEOUT_MS;
55112
55269
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
55113
55270
  const helperFiles = {
55114
- spec: join45(userData, "spec"),
55115
- pid: join45(userData, "pid"),
55116
- error: join45(userData, "error"),
55117
- stop: join45(userData, "stop"),
55118
- step: join45(userData, "step")
55271
+ spec: join46(userData, "spec"),
55272
+ pid: join46(userData, "pid"),
55273
+ error: join46(userData, "error"),
55274
+ stop: join46(userData, "stop"),
55275
+ step: join46(userData, "step")
55119
55276
  };
55120
55277
  const onDesktop = windowed && process.platform === "win32";
55121
55278
  const run = await this.spawnGame(engine.value, cmdArgs, userDataIn(userData), onDesktop ? { directory: project.value.path, files: helperFiles } : undefined);
@@ -55169,7 +55326,7 @@ ${platform}`;
55169
55326
  await run.settled;
55170
55327
  const helperFailed = onDesktop ? helperFailure(helperFiles.error) ?? (existsSync36(helperFiles.pid) ? null : `it never started, and ${helperStep(helperFiles.step)}`) : null;
55171
55328
  if (helperFailed !== null) {
55172
- discard(join45(project.value.path, ".godot", ...ours.split("/")), userData);
55329
+ discard(join46(project.value.path, ".godot", ...ours.split("/")), userData);
55173
55330
  return this.createErrorResponse(`No tests ran: ${named(needing)} ${needing.length === 1 ? "needs" : "need"} a window, and the engine could not be started on a desktop of its own: ${helperFailed}`);
55174
55331
  }
55175
55332
  const endedAt = Date.now();
@@ -55180,16 +55337,16 @@ ${platform}`;
55180
55337
  engine: endedAt - importedAt
55181
55338
  };
55182
55339
  const notCompiled = stopped.for ?? run.log.all.map((entry) => autoloadNotCompiled(entry, autoloaded)).find((found) => found !== null) ?? null;
55183
- const reportsDir = join45(project.value.path, ".godot", ...ours.split("/"));
55340
+ const reportsDir = join46(project.value.path, ".godot", ...ours.split("/"));
55184
55341
  let report = null;
55185
55342
  let reportProblem = null;
55186
- const casePrinted = withNestedErrors(readRun(existsSync36(engineLog) ? readFileSync41(engineLog, "utf8") : ""), run.log.all);
55343
+ const casePrinted = withNestedErrors(readRun(existsSync36(engineLog) ? readFileSync42(engineLog, "utf8") : ""), run.log.all);
55187
55344
  const declaredErrors = readExpected(expectedFile);
55188
55345
  try {
55189
55346
  const written = existsSync36(reportsDir) && notCompiled === null ? readdirSync21(reportsDir).filter((name) => name.startsWith("report_")).toSorted((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
55190
55347
  const newest = written.at(-1);
55191
55348
  if (newest !== undefined) {
55192
- report = parseJUnit(readFileSync41(join45(reportsDir, newest, "results.xml"), "utf8"));
55349
+ report = parseJUnit(readFileSync42(join46(reportsDir, newest, "results.xml"), "utf8"));
55193
55350
  }
55194
55351
  } catch (error) {
55195
55352
  reportProblem = errorMessage(error);
@@ -55602,7 +55759,7 @@ ${error.text}`;
55602
55759
  const scripts = projectFiles(projectPath, [".gd"]).map((script) => `res://${script}`);
55603
55760
  const wanted = `res://${relativePath}`;
55604
55761
  const folder = dirname25(relativePath.replaceAll("\\", "/"));
55605
- const inFolder = join45(projectPath, folder);
55762
+ const inFolder = join46(projectPath, folder);
55606
55763
  const beside = existsSync36(inFolder) ? readdirSync21(inFolder, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => `res://${folder === "." ? "" : `${folder}/`}${entry.name}`) : [];
55607
55764
  return this.createErrorResponse(withNearestFiles(`Script file does not exist: ${wanted}`, wanted, scripts, beside));
55608
55765
  }
@@ -55735,7 +55892,7 @@ ${error.text}`;
55735
55892
  return null;
55736
55893
  }
55737
55894
  try {
55738
- return readFileSync41(contained.absolutePath, "utf8");
55895
+ return readFileSync42(contained.absolutePath, "utf8");
55739
55896
  } catch {
55740
55897
  return null;
55741
55898
  }
@@ -55829,7 +55986,7 @@ ${error.text}`;
55829
55986
  return null;
55830
55987
  }
55831
55988
  try {
55832
- return readFileSync41(contained.absolutePath, "utf8");
55989
+ return readFileSync42(contained.absolutePath, "utf8");
55833
55990
  } catch {
55834
55991
  return null;
55835
55992
  }
@@ -56003,7 +56160,7 @@ ${error.text}`;
56003
56160
  ...Array.isArray(stack) ? {
56004
56161
  stack: stack.map((frame) => ({
56005
56162
  ...frame,
56006
- ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join45(projectPath, frame["script"].slice("res://".length)) } : {}
56163
+ ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join46(projectPath, frame["script"].slice("res://".length)) } : {}
56007
56164
  }))
56008
56165
  } : {}
56009
56166
  });
@@ -56127,7 +56284,7 @@ ${error.text}`;
56127
56284
  try {
56128
56285
  const askedAt = Date.now();
56129
56286
  const asked = this.godotBridge.invokeTool("playing_status", {}, { counted: false });
56130
- const answer = await Promise.race([asked, delay8(PLAYING_STATUS_MS).then(() => null)]);
56287
+ const answer = await Promise.race([asked, delay9(PLAYING_STATUS_MS).then(() => null)]);
56131
56288
  if (answer === null) {
56132
56289
  this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
56133
56290
  asked.then(() => {
@@ -56152,7 +56309,7 @@ ${error.text}`;
56152
56309
  try {
56153
56310
  const answer = await Promise.race([
56154
56311
  this.godotBridge.invokeTool("scan_status", {}, { counted: false }),
56155
- delay8(PLAYING_STATUS_MS).then(() => null)
56312
+ delay9(PLAYING_STATUS_MS).then(() => null)
56156
56313
  ]);
56157
56314
  if (answer === null) {
56158
56315
  return null;
@@ -56294,7 +56451,7 @@ ${error.text}`;
56294
56451
  if (answer.serversNote === undefined || adapterFailed || Date.now() >= deadline) {
56295
56452
  return answer;
56296
56453
  }
56297
- await delay8(500);
56454
+ await delay9(500);
56298
56455
  }
56299
56456
  }
56300
56457
  async rebindTheDebugAdapter(servers) {
@@ -56348,7 +56505,7 @@ ${error.text}`;
56348
56505
  return null;
56349
56506
  }
56350
56507
  try {
56351
- return settingKeys(readFileSync41(join45(projectPath, "project.godot"), "utf8"));
56508
+ return settingKeys(readFileSync42(join46(projectPath, "project.godot"), "utf8"));
56352
56509
  } catch {
56353
56510
  return null;
56354
56511
  }
@@ -56555,7 +56712,7 @@ ${error.text}`;
56555
56712
  if (Date.now() >= deadline) {
56556
56713
  return false;
56557
56714
  }
56558
- await delay8(250);
56715
+ await delay9(250);
56559
56716
  }
56560
56717
  }
56561
56718
  async handleLaunchEditor(args) {
@@ -56693,7 +56850,7 @@ ${error.text}`;
56693
56850
  if (!paths.ok) {
56694
56851
  return refused(paths.reason, PATH_SOLUTIONS);
56695
56852
  }
56696
- const missing = paths.value.filter((path) => !existsSync36(join45(projectPath, path.slice("res://".length))));
56853
+ const missing = paths.value.filter((path) => !existsSync36(join46(projectPath, path.slice("res://".length))));
56697
56854
  if (missing.length > 0) {
56698
56855
  return refused(`Nothing at ${missing.join(" and ")} in the project at ${projectPath}.`, PATH_SOLUTIONS);
56699
56856
  }
@@ -56866,7 +57023,7 @@ ${error.text}`;
56866
57023
  }
56867
57024
  async runtimeUp(projectPath, before, budget, withArgs) {
56868
57025
  const budgetMs = budget.ms;
56869
- if (!existsSync36(join45(projectPath, RUNTIME_SCRIPT))) {
57026
+ if (!existsSync36(join46(projectPath, RUNTIME_SCRIPT))) {
56870
57027
  return runtimeVerdict(null, {
56871
57028
  addon: false,
56872
57029
  budgetMs,
@@ -56969,7 +57126,7 @@ ${error.text}`;
56969
57126
  }
56970
57127
  let announcedAt;
56971
57128
  try {
56972
- announcedAt = statSync20(endpoint.file).mtimeMs;
57129
+ announcedAt = statSync21(endpoint.file).mtimeMs;
56973
57130
  } catch {
56974
57131
  return;
56975
57132
  }
@@ -57029,7 +57186,7 @@ ${error.text}`;
57029
57186
  stepFrom = now;
57030
57187
  };
57031
57188
  const transcript = openTranscript(startedAt);
57032
- closeSync6(transcript.fd);
57189
+ closeSync7(transcript.fd);
57033
57190
  step("transcript");
57034
57191
  const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
57035
57192
  const which = playOf(playAnswer);
@@ -57130,7 +57287,7 @@ ${error.text}`;
57130
57287
  }
57131
57288
  let handle;
57132
57289
  try {
57133
- handle = openSync7(game.errorReport, "r");
57290
+ handle = openSync8(game.errorReport, "r");
57134
57291
  } catch {
57135
57292
  return;
57136
57293
  }
@@ -57164,7 +57321,7 @@ ${error.text}`;
57164
57321
  game.log.append("stderr", reported);
57165
57322
  }
57166
57323
  } finally {
57167
- closeSync6(handle);
57324
+ closeSync7(handle);
57168
57325
  }
57169
57326
  }
57170
57327
  announcedPidNamedBy(run, line) {
@@ -57184,7 +57341,7 @@ ${error.text}`;
57184
57341
  return 0;
57185
57342
  }
57186
57343
  try {
57187
- return reportAlreadyInTranscript(readFileSync41(game.transcript), readFileSync41(report));
57344
+ return reportAlreadyInTranscript(readFileSync42(game.transcript), readFileSync42(report));
57188
57345
  } catch (error) {
57189
57346
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
57190
57347
  return 0;
@@ -57207,7 +57364,7 @@ ${error.text}`;
57207
57364
  if (Date.now() >= deadline) {
57208
57365
  return false;
57209
57366
  }
57210
- await delay8(100);
57367
+ await delay9(100);
57211
57368
  }
57212
57369
  }
57213
57370
  async endActiveGame(reason, going, graceful) {
@@ -57471,7 +57628,7 @@ ${error.text}`;
57471
57628
  const slot = await slotForCall(true);
57472
57629
  const startedAt = Date.now();
57473
57630
  const transcript = openTranscript(startedAt);
57474
- closeSync6(transcript.fd);
57631
+ closeSync7(transcript.fd);
57475
57632
  const profile = profiled ? profilePath(startedAt) : undefined;
57476
57633
  const gameEnv = onTheSlot(profiled ? { ...env ?? process.env, [PROFILE_FROM_START]: "1" } : env ?? process.env, slot.lock);
57477
57634
  let launched;
@@ -57531,7 +57688,7 @@ ${error.text}`;
57531
57688
  }
57532
57689
  let handle;
57533
57690
  try {
57534
- handle = openSync7(game.transcript, "r");
57691
+ handle = openSync8(game.transcript, "r");
57535
57692
  } catch {
57536
57693
  return;
57537
57694
  }
@@ -57545,7 +57702,7 @@ ${error.text}`;
57545
57702
  game.readOffset += read;
57546
57703
  game.log.append("transcript", buffer.subarray(0, read));
57547
57704
  } finally {
57548
- closeSync6(handle);
57705
+ closeSync7(handle);
57549
57706
  }
57550
57707
  }
57551
57708
  async pickUpWhatTheEditorIsPlaying() {
@@ -57942,7 +58099,7 @@ ${error.text}`;
57942
58099
  };
57943
58100
  const going = await this.runStillGoing(run);
57944
58101
  const projectPath = run.projectPath;
57945
- const projectGone = projectPath !== null && !existsSync36(join45(projectPath, "project.godot"));
58102
+ const projectGone = projectPath !== null && !existsSync36(join46(projectPath, "project.godot"));
57946
58103
  if (projectGone && callersName !== undefined) {
57947
58104
  return this.createErrorResponse(`callersOf reads the project's scripts, and the run's project at ${projectPath} has no project.godot any more.`, ["editor_run profile without callersOf answers with the profile itself"]);
57948
58105
  }
@@ -58100,7 +58257,7 @@ ${error.text}`;
58100
58257
  return true;
58101
58258
  }
58102
58259
  }
58103
- await delay8(100);
58260
+ await delay9(100);
58104
58261
  }
58105
58262
  return false;
58106
58263
  }
@@ -58130,7 +58287,7 @@ ${error.text}`;
58130
58287
  break;
58131
58288
  }
58132
58289
  }
58133
- await delay8(RUN_POLL_MS);
58290
+ await delay9(RUN_POLL_MS);
58134
58291
  }
58135
58292
  if (!await this.runStillGoing(run)) {
58136
58293
  return await this.handleGetDebugOutput(args);
@@ -58192,7 +58349,7 @@ ${error.text}`;
58192
58349
  if (Date.now() >= until) {
58193
58350
  return false;
58194
58351
  }
58195
- await delay8(50);
58352
+ await delay9(50);
58196
58353
  }
58197
58354
  }
58198
58355
  async handleEditorConsole(args) {
@@ -58349,7 +58506,7 @@ ${error.text}`;
58349
58506
  notes.push(endedWithoutACode({
58350
58507
  throughEditor: run.throughEditor,
58351
58508
  announced: announced !== undefined || run.announcedPid !== undefined,
58352
- couldAnnounce: run.projectPath !== null && existsSync36(join45(run.projectPath, RUNTIME_SCRIPT)),
58509
+ couldAnnounce: run.projectPath !== null && existsSync36(join46(run.projectPath, RUNTIME_SCRIPT)),
58353
58510
  endedHere: typeof run.endedHere === "string",
58354
58511
  pickedUp: run.pickedUp === true
58355
58512
  }));
@@ -58607,6 +58764,66 @@ ${error.text}`;
58607
58764
  return error instanceof BridgeTimeout;
58608
58765
  }
58609
58766
  }
58767
+ async handleEditorCapture(args) {
58768
+ const projectPath = readNonEmptyString(args, "projectPath") ?? "";
58769
+ const saveTo = readNonEmptyString(args, "outputPath") ?? null;
58770
+ const sendsThePicture = readBoolean(args, "inline") !== false;
58771
+ if (!sendsThePicture && saveTo === null) {
58772
+ return this.createErrorResponse("inline: false sends no picture back, so it needs outputPath to save one to.", [
58773
+ "Pass outputPath, an absolute path ending in .png outside the project",
58774
+ "Or leave inline out to have the picture sent"
58775
+ ]);
58776
+ }
58777
+ if (saveTo !== null) {
58778
+ const refusal = captureDestinationRefusal(saveTo, [
58779
+ ...projectPath === "" ? [] : [projectPath],
58780
+ ...this.ownProject === null ? [] : [this.ownProject]
58781
+ ]);
58782
+ if (refusal !== null) {
58783
+ return this.createErrorResponse(refusal);
58784
+ }
58785
+ }
58786
+ const folder = scratchDirectory("editor-capture");
58787
+ const picture = join46(folder, "capture.png");
58788
+ try {
58789
+ const asked = await this.handleViaBridge("capture_editor_view", { ...args, output_path: picture }, "editor_capture");
58790
+ if (asked.isError === true) {
58791
+ return asked;
58792
+ }
58793
+ const payload = asParams(JSON.parse(asked.content[0]?.text ?? "{}"));
58794
+ const returned = readString(payload, "path");
58795
+ if (readString(payload, "type") !== "screenshot_file" || returned === undefined) {
58796
+ return this.createErrorResponse(`The editor answered a capture with ${JSON.stringify(payload)}`);
58797
+ }
58798
+ if (normalize2(returned) !== normalize2(picture)) {
58799
+ return this.createErrorResponse(`Rejected capture file path outside the managed capture path: '${returned}'`);
58800
+ }
58801
+ if (saveTo !== null) {
58802
+ try {
58803
+ copyFileSync2(picture, saveTo, constants.COPYFILE_EXCL);
58804
+ } catch (error) {
58805
+ return this.createErrorResponse(`The capture was taken and could not be saved at ${saveTo}: ${errorMessage(error)}`);
58806
+ }
58807
+ }
58808
+ const sent = sendsThePicture ? pictureToSend(picture, readString(payload, "jpegPath")) : null;
58809
+ const view = readString(payload, "view") === "3d" ? "3D" : "2D";
58810
+ const drawnAt = payload["drawnAt"];
58811
+ const scaled = isRecord(drawnAt) ? `, scaled from ${String(drawnAt["width"])}x${String(drawnAt["height"])}` : "";
58812
+ const unsaved = payload["unsavedInEditor"] === true ? ". The picture includes the scene's unsaved changes in the editor" : "";
58813
+ const how = sent === null ? ". Not sent, as inline: false asked" : sent.mimeType === "image/jpeg" ? `. Sent as JPEG, ${kilobytes(sent.bytes)} against the PNG's ${kilobytes(sent.pngBytes)}${saveTo === null ? "" : ", and the saved PNG is lossless"}` : "";
58814
+ return {
58815
+ content: [
58816
+ {
58817
+ type: "text",
58818
+ text: `The editor's ${view} view of ${readString(payload, "scenePath") ?? "the scene"}: ${String(readNumber(payload, "width") ?? 0)}x${String(readNumber(payload, "height") ?? 0)} png${scaled}${unsaved}${saveTo === null ? "" : `. Saved to ${saveTo}`}${how}`
58819
+ },
58820
+ ...sent === null ? [] : [{ type: "image", data: sent.data, mimeType: sent.mimeType }]
58821
+ ]
58822
+ };
58823
+ } finally {
58824
+ discard(folder);
58825
+ }
58826
+ }
58610
58827
  async handleRescanFilesystem(args) {
58611
58828
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 30000;
58612
58829
  const started = Date.now();
@@ -59422,17 +59639,25 @@ ${error.text}`;
59422
59639
  ]);
59423
59640
  }
59424
59641
  }
59425
- const declaring = declaredButUncached(projectPath, typeName);
59426
- if (declaring !== null) {
59427
- const rebuilt = await this.rebuildClassCache(projectPath);
59428
- if (!rebuilt.ok) {
59429
- return this.answer(rebuilt);
59642
+ const run = async () => {
59643
+ const declaring = declaredButUncached(projectPath, typeName);
59644
+ if (declaring !== null) {
59645
+ const rebuilt = await this.rebuildClassCache(projectPath);
59646
+ if (!rebuilt.ok) {
59647
+ return this.answer(rebuilt);
59648
+ }
59430
59649
  }
59431
- }
59432
- const outcome = await this.operation("run_editor_tool", { command, arguments: contained.value }, projectPath);
59433
- if (!outcome.ok) {
59434
- return this.answer(outcome);
59435
- }
59650
+ const outcome = await this.operation("run_editor_tool", { command, arguments: contained.value }, projectPath);
59651
+ if (!outcome.ok) {
59652
+ return this.answer(outcome);
59653
+ }
59654
+ return this.headlessAnswer(outcome, effect, typeName, declaring);
59655
+ };
59656
+ const written = readNonEmptyString(contained.value, "scenePath") ?? readNonEmptyString(contained.value, "resourcePath");
59657
+ const target = effect === "writes" && written !== undefined ? resolveWithinProject(projectPath, written) : null;
59658
+ return target?.ok === true ? await inTurnFor(projectPath, target.absolutePath, run) : await run();
59659
+ }
59660
+ headlessAnswer(outcome, effect, typeName, declaring) {
59436
59661
  return this.jsonTextResponse({
59437
59662
  ...outcome.payload,
59438
59663
  ...declaring === null ? {} : {
@@ -59691,7 +59916,7 @@ ${error.text}`;
59691
59916
  }
59692
59917
  const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
59693
59918
  const declared = new RegExp(`^\\s*(?:@\\w+(?:\\([^)]*\\))?\\s+)*class_name\\s+${escaped}\\b`, "m");
59694
- if (declared.test(readFileSync41(scriptPath, "utf8"))) {
59919
+ if (declared.test(readFileSync42(scriptPath, "utf8"))) {
59695
59920
  return null;
59696
59921
  }
59697
59922
  const checked = await this.classesTheEditorCannotSee(args);
@@ -59889,7 +60114,7 @@ ${error.text}`;
59889
60114
  async ownRunOnItsWay(projectPath, budgetMs) {
59890
60115
  const run = await this.currentRun();
59891
60116
  const project = projectPath ?? run?.projectPath ?? null;
59892
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync36(join45(project, RUNTIME_SCRIPT)) || !await this.runStillGoing(run)) {
60117
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync36(join46(project, RUNTIME_SCRIPT)) || !await this.runStillGoing(run)) {
59893
60118
  return null;
59894
60119
  }
59895
60120
  const already = runtimesAnnounced().running;
@@ -59898,7 +60123,7 @@ ${error.text}`;
59898
60123
  return !run.announcedBefore.has(one.pid);
59899
60124
  }
59900
60125
  try {
59901
- return statSync20(one.file).mtimeMs >= run.startedAt;
60126
+ return statSync21(one.file).mtimeMs >= run.startedAt;
59902
60127
  } catch {
59903
60128
  return false;
59904
60129
  }
@@ -59999,7 +60224,7 @@ ${error.text}`;
59999
60224
  return this.createErrorResponse(staleNote);
60000
60225
  }
60001
60226
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
60002
- const screenshotPath = screenshotDir ? join45(screenshotDir, "capture.png") : null;
60227
+ const screenshotPath = screenshotDir ? join46(screenshotDir, "capture.png") : null;
60003
60228
  const refused = (message) => this.createErrorResponse(message);
60004
60229
  const onGame = {
60005
60230
  pid: choice.endpoint.pid,
@@ -60272,6 +60497,7 @@ var init_server = __esm(() => {
60272
60497
  init_tool_definitions();
60273
60498
  init_tool_reference();
60274
60499
  init_update_check();
60500
+ init_write_turns();
60275
60501
  TYPE_ARGUMENTS = {
60276
60502
  add_node: "nodeType",
60277
60503
  create_resource: "resourceType",
@@ -60344,7 +60570,7 @@ init_bridge_announce();
60344
60570
  init_engine_launch();
60345
60571
  init_engine_slots();
60346
60572
  import { existsSync as existsSync37 } from "node:fs";
60347
- import { dirname as dirname26, join as join46, resolve as resolve16 } from "node:path";
60573
+ import { dirname as dirname26, join as join47, resolve as resolve16 } from "node:path";
60348
60574
 
60349
60575
  // src/error-mode.ts
60350
60576
  import process8 from "node:process";
@@ -60694,6 +60920,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
60694
60920
  | End a game of the project that no server is holding | \`editor_run stop\` with its \`pid\` from \`editor_status\` \`runtimes\`, never a process kill by hand: it is ended only when it announced itself for the project and its command line is the project's engine run |
60695
60921
  | Play a game without touching the player's saves | \`editor_run start\` with \`savesIn\`, a directory for this run's \`user://\`: the editor plays with its own environment, so a run given one is started by the server, and the answer says under \`savesNote\` when the platform ignores it |
60696
60922
  | A picture, for a person who asked to see one | \`runtime_capture\` |
60923
+ | What a scene's @tool scripts draw in the editor | \`editor_capture\`, the editor's own 2D or 3D view of the scene. No game runs their editor branch |
60697
60924
 
60698
60925
  These need the runtime autoload, which \`gdharness setup\` registers. \`runtime_capture\` needs a
60699
60926
  window and refuses headless rather than handing back the last frame anything drew. On Windows a
@@ -62615,7 +62842,7 @@ class UsageError extends Refusal {
62615
62842
  function projectArgument(at) {
62616
62843
  const named = args.slice(at).find((value) => !value.startsWith("--"));
62617
62844
  const projectPath = resolve16(named ?? ".");
62618
- if (!existsSync37(join46(projectPath, "project.godot"))) {
62845
+ if (!existsSync37(join47(projectPath, "project.godot"))) {
62619
62846
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot. Run from the project directory, or name it on the command line.`);
62620
62847
  }
62621
62848
  return projectPath;
@@ -62679,7 +62906,7 @@ async function registerRuntime(godot, projectPath) {
62679
62906
  }
62680
62907
  const path = (named ?? "").replace(/^res:\/\//, "");
62681
62908
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
62682
- if (!existsSync37(join46(projectPath, path))) {
62909
+ if (!existsSync37(join47(projectPath, path))) {
62683
62910
  console.log(" that file is not in this project, so it boots with a missing script");
62684
62911
  }
62685
62912
  console.log(` ${useTheShippedLoader(RUNTIME_AUTOLOAD.name, path)}`);