gdharness 1.3.65 → 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",
@@ -51071,9 +51144,15 @@ function selectTests(projectPath, asked, given) {
51071
51144
  return { ok: false, reason: `${said}: none of them declares a func test_ at the top level.` };
51072
51145
  }
51073
51146
  const written = declared.map((one) => `${nameOf(one.suite)}:${one.test}`);
51074
- const close = nearest(suiteNamed === null ? test : `${nameOf(suiteNamed)}:${test}`, [
51075
- ...new Set(suiteNamed === null ? declared.map((one) => one.test) : written)
51076
- ]);
51147
+ const bare = (name) => name.replace(/^test_/, "");
51148
+ const inSuite = suiteNamed === null ? declared : declared.filter((one) => one.suite === suitePath || nameOf(one.suite) === suiteNamed);
51149
+ const spell = (one) => suiteNamed === null ? one.test : `${nameOf(one.suite)}:${one.test}`;
51150
+ const close = [
51151
+ ...new Set([
51152
+ ...nearest(bare(test), [...new Set(inSuite.map((one) => bare(one.test)))]).flatMap((name) => inSuite.filter((one) => bare(one.test) === name).map(spell)),
51153
+ ...suiteNamed === null ? [] : declared.filter((one) => one.test === test).map(spell)
51154
+ ])
51155
+ ];
51077
51156
  return {
51078
51157
  ok: false,
51079
51158
  reason: close.length > 0 ? `${said}. The nearest ${close.length === 1 ? "is" : "are"} ${close.join(", ")}.` : `${said}. The cases there are ${written.slice(0, 12).join(", ")}${written.length > 12 ? `, and ${written.length - 12} more` : ""}.`
@@ -51096,27 +51175,109 @@ var init_test_selection = __esm(() => {
51096
51175
  CASE_AFTER_SCRIPT = /^(.*\.gd):(\w+)$/;
51097
51176
  });
51098
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
+
51099
51260
  // src/server.ts
51100
51261
  import { randomUUID as randomUUID2 } from "node:crypto";
51101
51262
  import {
51102
51263
  appendFileSync as appendFileSync2,
51103
- closeSync as closeSync6,
51264
+ closeSync as closeSync7,
51104
51265
  constants,
51105
51266
  copyFileSync as copyFileSync2,
51106
51267
  existsSync as existsSync36,
51107
51268
  fstatSync as fstatSync3,
51108
- mkdirSync as mkdirSync23,
51109
- openSync as openSync7,
51269
+ mkdirSync as mkdirSync24,
51270
+ openSync as openSync8,
51110
51271
  readdirSync as readdirSync21,
51111
- readFileSync as readFileSync41,
51272
+ readFileSync as readFileSync42,
51112
51273
  readSync as readSync4,
51113
51274
  realpathSync as realpathSync7,
51114
51275
  rmdirSync as rmdirSync2,
51115
- statSync as statSync20,
51276
+ statSync as statSync21,
51116
51277
  writeFileSync as writeFileSync24
51117
51278
  } from "node:fs";
51118
- 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";
51119
- 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";
51120
51281
  import { fileURLToPath as fileURLToPath6 } from "node:url";
51121
51282
  function editorRestartTimeoutMs() {
51122
51283
  return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
@@ -51129,9 +51290,9 @@ function greetingWaitMs() {
51129
51290
  return waitFromEnv("GDHARNESS_GREETING_WAIT_MS", 1e4);
51130
51291
  }
51131
51292
  function pictureToSend(pngPath, jpegPath) {
51132
- const png = readFileSync41(pngPath);
51293
+ const png = readFileSync42(pngPath);
51133
51294
  const beside = pngPath.replace(/\.png$/, ".jpg");
51134
- 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;
51135
51296
  const sent = jpeg !== null && jpeg.length < png.length ? jpeg : png;
51136
51297
  return {
51137
51298
  data: sent.toString("base64"),
@@ -51175,7 +51336,7 @@ function writtenFileRefusal(path, rule) {
51175
51336
  if (!existsSync36(directory)) {
51176
51337
  return `${argument}'s directory ${dirname25(path)} does not exist. ${whatOpening} creates no directories.`;
51177
51338
  }
51178
- const where = join45(directory, basename14(path));
51339
+ const where = join46(directory, basename14(path));
51179
51340
  const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realAsFarAsItExists(project), where));
51180
51341
  if (inside !== undefined) {
51181
51342
  return `${argument} ${path} is inside the project at ${inside}. ${whatOpening} is saved outside it, so it never becomes part of the game.`;
@@ -51271,7 +51432,7 @@ function keptGameRequest(tool, op, args) {
51271
51432
  }
51272
51433
  }
51273
51434
  function projectSpelling(projectPath, absolutePath) {
51274
- return `res://${relative10(realAsFarAsItExists(projectPath), realAsFarAsItExists(absolutePath)).replaceAll("\\", "/")}`;
51435
+ return `res://${relative11(realAsFarAsItExists(projectPath), realAsFarAsItExists(absolutePath)).replaceAll("\\", "/")}`;
51275
51436
  }
51276
51437
  function whichGame(args) {
51277
51438
  return { projectPath: args["projectPath"], pid: args["pid"] };
@@ -51402,7 +51563,7 @@ async function untilGone(pids, withinMs = stopWaitMs()) {
51402
51563
  const gone = () => pids.every((pid) => !alive(pid));
51403
51564
  const deadline = Date.now() + withinMs;
51404
51565
  while (!gone() && Date.now() < deadline) {
51405
- await delay8(50);
51566
+ await delay9(50);
51406
51567
  }
51407
51568
  return gone();
51408
51569
  }
@@ -51606,7 +51767,7 @@ function endedToStartThis(ended) {
51606
51767
  return ` ${which} was ended to start this one. Its output is no longer what editor_output answers about.`;
51607
51768
  }
51608
51769
  function lineNothingStopsOn(absolutePath, relativePath, line) {
51609
- const text = readFileSync41(absolutePath, "utf8");
51770
+ const text = readFileSync42(absolutePath, "utf8");
51610
51771
  const lines = text === "" ? 0 : text.split(`
51611
51772
  `).length - (text.endsWith(`
51612
51773
  `) ? 1 : 0);
@@ -51814,7 +51975,7 @@ function notRunNote(notRun, failFast) {
51814
51975
  function presetsIn(projectPath) {
51815
51976
  let text;
51816
51977
  try {
51817
- text = readFileSync41(join45(projectPath, "export_presets.cfg"), "utf8");
51978
+ text = readFileSync42(join46(projectPath, "export_presets.cfg"), "utf8");
51818
51979
  } catch {
51819
51980
  return null;
51820
51981
  }
@@ -51921,7 +52082,7 @@ function filesIn(folder) {
51921
52082
  try {
51922
52083
  for (const entry of readdirSync21(folder, { withFileTypes: true })) {
51923
52084
  if (entry.isFile()) {
51924
- found.set(entry.name, statSync20(join45(folder, entry.name)).mtimeMs);
52085
+ found.set(entry.name, statSync21(join46(folder, entry.name)).mtimeMs);
51925
52086
  }
51926
52087
  }
51927
52088
  } catch {}
@@ -51961,7 +52122,7 @@ function unsavedInEditorNote(scripts) {
51961
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.`;
51962
52123
  }
51963
52124
  function editorAddonMissing(projectPath) {
51964
- if (!existsSync36(join45(projectPath, "project.godot"))) {
52125
+ if (!existsSync36(join46(projectPath, "project.godot"))) {
51965
52126
  return null;
51966
52127
  }
51967
52128
  const report = inspectProject(projectPath);
@@ -51989,8 +52150,8 @@ function rewriteBusNamed(projectPath, from, to) {
51989
52150
  const giving = [`bus = &"${quoted(from)}"`, `bus = "${quoted(from)}"`];
51990
52151
  const rewritten = [];
51991
52152
  for (const file of projectFiles(projectPath, [".tscn", ".tres"])) {
51992
- const absolute = join45(projectPath, file);
51993
- const lines = readFileSync41(absolute, "utf8").split(`
52153
+ const absolute = join46(projectPath, file);
52154
+ const lines = readFileSync42(absolute, "utf8").split(`
51994
52155
  `);
51995
52156
  const at = [];
51996
52157
  const after = lines.map((line, index) => {
@@ -52009,7 +52170,7 @@ function rewriteBusNamed(projectPath, from, to) {
52009
52170
  }
52010
52171
  const namedInScripts = [];
52011
52172
  for (const file of projectFiles(projectPath, [".gd"])) {
52012
- readFileSync41(join45(projectPath, file), "utf8").split(`
52173
+ readFileSync42(join46(projectPath, file), "utf8").split(`
52013
52174
  `).forEach((line, index) => {
52014
52175
  if (line.includes(`"${from}"`)) {
52015
52176
  namedInScripts.push({ file: `res://${file}`, line: index + 1, text: line.trim() });
@@ -52300,7 +52461,7 @@ function profileFrameFunctions(projectPath) {
52300
52461
  let projectGodot = null;
52301
52462
  if (projectPath !== null) {
52302
52463
  try {
52303
- projectGodot = readFileSync41(join45(projectPath, "project.godot"), "utf8");
52464
+ projectGodot = readFileSync42(join46(projectPath, "project.godot"), "utf8");
52304
52465
  } catch {}
52305
52466
  }
52306
52467
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -52324,7 +52485,7 @@ function projectSourceOf(projectPath) {
52324
52485
  let source = null;
52325
52486
  if (inside.ok) {
52326
52487
  try {
52327
- source = readFileSync41(inside.absolutePath, "utf8");
52488
+ source = readFileSync42(inside.absolutePath, "utf8");
52328
52489
  } catch {}
52329
52490
  }
52330
52491
  read.set(script, source);
@@ -52496,7 +52657,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
52496
52657
  }
52497
52658
  function gdUnitFailsOnScriptErrors(projectPath) {
52498
52659
  try {
52499
- 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;
52500
52661
  } catch {
52501
52662
  return true;
52502
52663
  }
@@ -52511,7 +52672,7 @@ function uncaughtNote(uncaught) {
52511
52672
  }
52512
52673
  function elsewhereIn(projectPath, asked) {
52513
52674
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
52514
- if (wanted === "" || existsSync36(join45(projectPath, wanted))) {
52675
+ if (wanted === "" || existsSync36(join46(projectPath, wanted))) {
52515
52676
  return "";
52516
52677
  }
52517
52678
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -52534,7 +52695,7 @@ function windowNoteFor(needing, sharing) {
52534
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}`;
52535
52696
  }
52536
52697
  function testPathsIn(projectPath, given, argument = "path") {
52537
- const defaults = TEST_DIRECTORIES.filter((directory) => existsSync36(join45(projectPath, directory)));
52698
+ const defaults = TEST_DIRECTORIES.filter((directory) => existsSync36(join46(projectPath, directory)));
52538
52699
  const entries = given === undefined || given === null || given === "" ? defaults.length > 0 ? defaults : TEST_DIRECTORIES : given;
52539
52700
  const listed = Array.isArray(entries) ? entries : [entries];
52540
52701
  if (listed.length === 0) {
@@ -52637,17 +52798,17 @@ function asWarning(raised) {
52637
52798
  }
52638
52799
  function isDirectory(path) {
52639
52800
  try {
52640
- return statSync20(path).isDirectory();
52801
+ return statSync21(path).isDirectory();
52641
52802
  } catch {
52642
52803
  return false;
52643
52804
  }
52644
52805
  }
52645
52806
  function mainSceneOf(projectFile) {
52646
- const scene = parseProjectGodot(readFileSync41(projectFile, "utf8"))["application"]?.["run/main_scene"];
52807
+ const scene = parseProjectGodot(readFileSync42(projectFile, "utf8"))["application"]?.["run/main_scene"];
52647
52808
  return typeof scene === "string" && scene !== "" ? scene : null;
52648
52809
  }
52649
52810
  function editorPlaysHeadless(projectFile) {
52650
- const runArgs = parseProjectGodot(readFileSync41(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
52811
+ const runArgs = parseProjectGodot(readFileSync42(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
52651
52812
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
52652
52813
  }
52653
52814
  function ownArguments(params) {
@@ -52679,7 +52840,7 @@ function unheldNote(changed) {
52679
52840
  class GodotServer {
52680
52841
  stdio = null;
52681
52842
  locator = new GodotLocator;
52682
- operationsScript = join45(__dirname2, "godot", "operations", "godot_operations.gd");
52843
+ operationsScript = join46(__dirname2, "godot", "operations", "godot_operations.gd");
52683
52844
  exportOptionsRead = new Map;
52684
52845
  heldWhilePending = new Set;
52685
52846
  evaluatedWhilePending = new Set;
@@ -52838,7 +52999,7 @@ class GodotServer {
52838
52999
  taken = true;
52839
53000
  this.logDebug(`Took port ${wanted} over from pid ${announced.pid}`);
52840
53001
  } catch {
52841
- await delay8(HANDOVER_POLL_MS);
53002
+ await delay9(HANDOVER_POLL_MS);
52842
53003
  }
52843
53004
  }
52844
53005
  this.notHandedOverBy = taken ? null : announced.pid;
@@ -52969,7 +53130,7 @@ class GodotServer {
52969
53130
  }
52970
53131
  noteTheStart() {
52971
53132
  const cwd = process.cwd();
52972
- const mcpJson = join45(cwd, ".mcp.json");
53133
+ const mcpJson = join46(cwd, ".mcp.json");
52973
53134
  const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
52974
53135
  const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
52975
53136
  writeLifecycle("started", {
@@ -53118,7 +53279,7 @@ class GodotServer {
53118
53279
  return checked.response;
53119
53280
  }
53120
53281
  const project = args["projectPath"];
53121
- if (typeof project === "string" && existsSync36(join45(project, "project.godot"))) {
53282
+ if (typeof project === "string" && existsSync36(join46(project, "project.godot"))) {
53122
53283
  this.lastProjectPath = project;
53123
53284
  }
53124
53285
  this.updates.refresh();
@@ -53161,7 +53322,7 @@ class GodotServer {
53161
53322
  let notSaved;
53162
53323
  try {
53163
53324
  savedAt = answer.content.map((item, index) => {
53164
- 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"}`);
53165
53326
  writeFileSync24(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
53166
53327
  return file;
53167
53328
  });
@@ -53625,6 +53786,8 @@ class GodotServer {
53625
53786
  return await this.handleEditorStatus();
53626
53787
  case "editor_rescan":
53627
53788
  return await this.handleRescanFilesystem(args);
53789
+ case "editor_capture":
53790
+ return await this.handleEditorCapture(args);
53628
53791
  case "runtime_inspect": {
53629
53792
  const patience = readPositiveNumber(args, "timeoutMs") ?? this.runtimeTimeoutMs();
53630
53793
  switch (op) {
@@ -53858,7 +54021,7 @@ class GodotServer {
53858
54021
  if (path === undefined) {
53859
54022
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
53860
54023
  }
53861
- const file = join45(path, "project.godot");
54024
+ const file = join46(path, "project.godot");
53862
54025
  if (!existsSync36(file)) {
53863
54026
  return {
53864
54027
  ok: false,
@@ -54608,7 +54771,7 @@ class GodotServer {
54608
54771
  if (!engine.ok) {
54609
54772
  return engine.response;
54610
54773
  }
54611
- const application = parseProjectGodot(readFileSync41(project.value.file, "utf8"))["application"] ?? {};
54774
+ const application = parseProjectGodot(readFileSync42(project.value.file, "utf8"))["application"] ?? {};
54612
54775
  const name = application["config/name"];
54613
54776
  const mainScene = application["run/main_scene"];
54614
54777
  const info = {
@@ -54762,7 +54925,7 @@ class GodotServer {
54762
54925
  const platform = presetPlatform(project.value.path, preset);
54763
54926
  if (platform !== null) {
54764
54927
  const known = await this.exportOptions(platform, {
54765
- presetsFile: join45(project.value.path, "export_presets.cfg"),
54928
+ presetsFile: join46(project.value.path, "export_presets.cfg"),
54766
54929
  preset,
54767
54930
  asked: options
54768
54931
  });
@@ -54813,7 +54976,7 @@ ${platform}`;
54813
54976
  }
54814
54977
  const empty = scratchDirectory("export");
54815
54978
  try {
54816
- writeFileSync24(join45(empty, "project.godot"), `config_version=5
54979
+ writeFileSync24(join46(empty, "project.godot"), `config_version=5
54817
54980
  `);
54818
54981
  const outcome = await runOperation({ godotPath: engine.value, script: this.operationsScript, debug: DEBUG_MODE, editor: true }, "export_options", { platform, ...standing }, empty);
54819
54982
  const options = outcome.ok ? outcome.payload["options"] : undefined;
@@ -54852,7 +55015,7 @@ ${platform}`;
54852
55015
  }
54853
55016
  const folder = dirname25(output.absolutePath);
54854
55017
  try {
54855
- mkdirSync23(folder, { recursive: true });
55018
+ mkdirSync24(folder, { recursive: true });
54856
55019
  } catch (error) {
54857
55020
  return this.createErrorResponse(`Cannot create ${dirname25(output.relativePath)} to export into: ${errorMessage(error)}`);
54858
55021
  }
@@ -54870,7 +55033,7 @@ ${platform}`;
54870
55033
  const exportArgs = [
54871
55034
  "--headless",
54872
55035
  "--log-file",
54873
- join45(exportLogs, "engine.log"),
55036
+ join46(exportLogs, "engine.log"),
54874
55037
  "--path",
54875
55038
  project.value.path,
54876
55039
  pack ? "--export-pack" : debug ? "--export-debug" : "--export-release",
@@ -54878,7 +55041,7 @@ ${platform}`;
54878
55041
  output.absolutePath
54879
55042
  ];
54880
55043
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
54881
- const writtenAt = () => existsSync36(output.absolutePath) ? statSync20(output.absolutePath).mtimeMs : null;
55044
+ const writtenAt = () => existsSync36(output.absolutePath) ? statSync21(output.absolutePath).mtimeMs : null;
54882
55045
  const caches = editorCacheDirectory(engine.value);
54883
55046
  const exportOnce = async () => {
54884
55047
  const before = writtenAt();
@@ -54908,7 +55071,7 @@ ${platform}`;
54908
55071
  return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
54909
55072
  }
54910
55073
  const beside = writtenBeside(listedBefore, filesIn(folder), basename14(output.absolutePath), dirname25(output.relativePath).replaceAll("\\", "/"));
54911
- 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));
54912
55075
  }
54913
55076
  reportTestProgress(log) {
54914
55077
  const progress = callProgress();
@@ -54969,7 +55132,7 @@ ${platform}`;
54969
55132
  if (movie !== null) {
54970
55133
  return this.createErrorResponse(movie);
54971
55134
  }
54972
- 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))));
54973
55136
  const scripts = new ProjectScripts(project.value.path);
54974
55137
  const present = paths.value.filter((path) => !missing.includes(path));
54975
55138
  const asked = scripts.searchedAsTold(present);
@@ -55061,10 +55224,10 @@ ${platform}`;
55061
55224
  const ignored = readStringArray(args, "ignore") ?? [];
55062
55225
  const declaredUnder = ignored.length === 0 ? [] : asked.flatMap((path) => path.endsWith(".gd") ? scripts.suite(path) ?? [] : scripts.suitesUnder([path]));
55063
55226
  const userData = scratchDirectory("tests");
55064
- const suitesFile = join45(userData, "suites.txt");
55065
- const skippingFile = join45(userData, "skipping.txt");
55066
- const engineLog = join45(userData, "engine.log");
55067
- 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");
55068
55231
  writeFileSync24(suitesFile, `${(selection.value?.suites ?? asked).join(`
55069
55232
  `)}
55070
55233
  `);
@@ -55105,11 +55268,11 @@ ${platform}`;
55105
55268
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? TEST_TIMEOUT_MS;
55106
55269
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
55107
55270
  const helperFiles = {
55108
- spec: join45(userData, "spec"),
55109
- pid: join45(userData, "pid"),
55110
- error: join45(userData, "error"),
55111
- stop: join45(userData, "stop"),
55112
- 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")
55113
55276
  };
55114
55277
  const onDesktop = windowed && process.platform === "win32";
55115
55278
  const run = await this.spawnGame(engine.value, cmdArgs, userDataIn(userData), onDesktop ? { directory: project.value.path, files: helperFiles } : undefined);
@@ -55163,7 +55326,7 @@ ${platform}`;
55163
55326
  await run.settled;
55164
55327
  const helperFailed = onDesktop ? helperFailure(helperFiles.error) ?? (existsSync36(helperFiles.pid) ? null : `it never started, and ${helperStep(helperFiles.step)}`) : null;
55165
55328
  if (helperFailed !== null) {
55166
- discard(join45(project.value.path, ".godot", ...ours.split("/")), userData);
55329
+ discard(join46(project.value.path, ".godot", ...ours.split("/")), userData);
55167
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}`);
55168
55331
  }
55169
55332
  const endedAt = Date.now();
@@ -55174,16 +55337,16 @@ ${platform}`;
55174
55337
  engine: endedAt - importedAt
55175
55338
  };
55176
55339
  const notCompiled = stopped.for ?? run.log.all.map((entry) => autoloadNotCompiled(entry, autoloaded)).find((found) => found !== null) ?? null;
55177
- const reportsDir = join45(project.value.path, ".godot", ...ours.split("/"));
55340
+ const reportsDir = join46(project.value.path, ".godot", ...ours.split("/"));
55178
55341
  let report = null;
55179
55342
  let reportProblem = null;
55180
- 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);
55181
55344
  const declaredErrors = readExpected(expectedFile);
55182
55345
  try {
55183
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))) : [];
55184
55347
  const newest = written.at(-1);
55185
55348
  if (newest !== undefined) {
55186
- report = parseJUnit(readFileSync41(join45(reportsDir, newest, "results.xml"), "utf8"));
55349
+ report = parseJUnit(readFileSync42(join46(reportsDir, newest, "results.xml"), "utf8"));
55187
55350
  }
55188
55351
  } catch (error) {
55189
55352
  reportProblem = errorMessage(error);
@@ -55596,7 +55759,7 @@ ${error.text}`;
55596
55759
  const scripts = projectFiles(projectPath, [".gd"]).map((script) => `res://${script}`);
55597
55760
  const wanted = `res://${relativePath}`;
55598
55761
  const folder = dirname25(relativePath.replaceAll("\\", "/"));
55599
- const inFolder = join45(projectPath, folder);
55762
+ const inFolder = join46(projectPath, folder);
55600
55763
  const beside = existsSync36(inFolder) ? readdirSync21(inFolder, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => `res://${folder === "." ? "" : `${folder}/`}${entry.name}`) : [];
55601
55764
  return this.createErrorResponse(withNearestFiles(`Script file does not exist: ${wanted}`, wanted, scripts, beside));
55602
55765
  }
@@ -55729,7 +55892,7 @@ ${error.text}`;
55729
55892
  return null;
55730
55893
  }
55731
55894
  try {
55732
- return readFileSync41(contained.absolutePath, "utf8");
55895
+ return readFileSync42(contained.absolutePath, "utf8");
55733
55896
  } catch {
55734
55897
  return null;
55735
55898
  }
@@ -55823,7 +55986,7 @@ ${error.text}`;
55823
55986
  return null;
55824
55987
  }
55825
55988
  try {
55826
- return readFileSync41(contained.absolutePath, "utf8");
55989
+ return readFileSync42(contained.absolutePath, "utf8");
55827
55990
  } catch {
55828
55991
  return null;
55829
55992
  }
@@ -55997,7 +56160,7 @@ ${error.text}`;
55997
56160
  ...Array.isArray(stack) ? {
55998
56161
  stack: stack.map((frame) => ({
55999
56162
  ...frame,
56000
- ...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)) } : {}
56001
56164
  }))
56002
56165
  } : {}
56003
56166
  });
@@ -56121,7 +56284,7 @@ ${error.text}`;
56121
56284
  try {
56122
56285
  const askedAt = Date.now();
56123
56286
  const asked = this.godotBridge.invokeTool("playing_status", {}, { counted: false });
56124
- 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)]);
56125
56288
  if (answer === null) {
56126
56289
  this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
56127
56290
  asked.then(() => {
@@ -56146,7 +56309,7 @@ ${error.text}`;
56146
56309
  try {
56147
56310
  const answer = await Promise.race([
56148
56311
  this.godotBridge.invokeTool("scan_status", {}, { counted: false }),
56149
- delay8(PLAYING_STATUS_MS).then(() => null)
56312
+ delay9(PLAYING_STATUS_MS).then(() => null)
56150
56313
  ]);
56151
56314
  if (answer === null) {
56152
56315
  return null;
@@ -56288,7 +56451,7 @@ ${error.text}`;
56288
56451
  if (answer.serversNote === undefined || adapterFailed || Date.now() >= deadline) {
56289
56452
  return answer;
56290
56453
  }
56291
- await delay8(500);
56454
+ await delay9(500);
56292
56455
  }
56293
56456
  }
56294
56457
  async rebindTheDebugAdapter(servers) {
@@ -56342,7 +56505,7 @@ ${error.text}`;
56342
56505
  return null;
56343
56506
  }
56344
56507
  try {
56345
- return settingKeys(readFileSync41(join45(projectPath, "project.godot"), "utf8"));
56508
+ return settingKeys(readFileSync42(join46(projectPath, "project.godot"), "utf8"));
56346
56509
  } catch {
56347
56510
  return null;
56348
56511
  }
@@ -56549,7 +56712,7 @@ ${error.text}`;
56549
56712
  if (Date.now() >= deadline) {
56550
56713
  return false;
56551
56714
  }
56552
- await delay8(250);
56715
+ await delay9(250);
56553
56716
  }
56554
56717
  }
56555
56718
  async handleLaunchEditor(args) {
@@ -56687,7 +56850,7 @@ ${error.text}`;
56687
56850
  if (!paths.ok) {
56688
56851
  return refused(paths.reason, PATH_SOLUTIONS);
56689
56852
  }
56690
- 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))));
56691
56854
  if (missing.length > 0) {
56692
56855
  return refused(`Nothing at ${missing.join(" and ")} in the project at ${projectPath}.`, PATH_SOLUTIONS);
56693
56856
  }
@@ -56860,7 +57023,7 @@ ${error.text}`;
56860
57023
  }
56861
57024
  async runtimeUp(projectPath, before, budget, withArgs) {
56862
57025
  const budgetMs = budget.ms;
56863
- if (!existsSync36(join45(projectPath, RUNTIME_SCRIPT))) {
57026
+ if (!existsSync36(join46(projectPath, RUNTIME_SCRIPT))) {
56864
57027
  return runtimeVerdict(null, {
56865
57028
  addon: false,
56866
57029
  budgetMs,
@@ -56963,7 +57126,7 @@ ${error.text}`;
56963
57126
  }
56964
57127
  let announcedAt;
56965
57128
  try {
56966
- announcedAt = statSync20(endpoint.file).mtimeMs;
57129
+ announcedAt = statSync21(endpoint.file).mtimeMs;
56967
57130
  } catch {
56968
57131
  return;
56969
57132
  }
@@ -57023,7 +57186,7 @@ ${error.text}`;
57023
57186
  stepFrom = now;
57024
57187
  };
57025
57188
  const transcript = openTranscript(startedAt);
57026
- closeSync6(transcript.fd);
57189
+ closeSync7(transcript.fd);
57027
57190
  step("transcript");
57028
57191
  const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
57029
57192
  const which = playOf(playAnswer);
@@ -57124,7 +57287,7 @@ ${error.text}`;
57124
57287
  }
57125
57288
  let handle;
57126
57289
  try {
57127
- handle = openSync7(game.errorReport, "r");
57290
+ handle = openSync8(game.errorReport, "r");
57128
57291
  } catch {
57129
57292
  return;
57130
57293
  }
@@ -57158,7 +57321,7 @@ ${error.text}`;
57158
57321
  game.log.append("stderr", reported);
57159
57322
  }
57160
57323
  } finally {
57161
- closeSync6(handle);
57324
+ closeSync7(handle);
57162
57325
  }
57163
57326
  }
57164
57327
  announcedPidNamedBy(run, line) {
@@ -57178,7 +57341,7 @@ ${error.text}`;
57178
57341
  return 0;
57179
57342
  }
57180
57343
  try {
57181
- return reportAlreadyInTranscript(readFileSync41(game.transcript), readFileSync41(report));
57344
+ return reportAlreadyInTranscript(readFileSync42(game.transcript), readFileSync42(report));
57182
57345
  } catch (error) {
57183
57346
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
57184
57347
  return 0;
@@ -57201,7 +57364,7 @@ ${error.text}`;
57201
57364
  if (Date.now() >= deadline) {
57202
57365
  return false;
57203
57366
  }
57204
- await delay8(100);
57367
+ await delay9(100);
57205
57368
  }
57206
57369
  }
57207
57370
  async endActiveGame(reason, going, graceful) {
@@ -57465,7 +57628,7 @@ ${error.text}`;
57465
57628
  const slot = await slotForCall(true);
57466
57629
  const startedAt = Date.now();
57467
57630
  const transcript = openTranscript(startedAt);
57468
- closeSync6(transcript.fd);
57631
+ closeSync7(transcript.fd);
57469
57632
  const profile = profiled ? profilePath(startedAt) : undefined;
57470
57633
  const gameEnv = onTheSlot(profiled ? { ...env ?? process.env, [PROFILE_FROM_START]: "1" } : env ?? process.env, slot.lock);
57471
57634
  let launched;
@@ -57525,7 +57688,7 @@ ${error.text}`;
57525
57688
  }
57526
57689
  let handle;
57527
57690
  try {
57528
- handle = openSync7(game.transcript, "r");
57691
+ handle = openSync8(game.transcript, "r");
57529
57692
  } catch {
57530
57693
  return;
57531
57694
  }
@@ -57539,7 +57702,7 @@ ${error.text}`;
57539
57702
  game.readOffset += read;
57540
57703
  game.log.append("transcript", buffer.subarray(0, read));
57541
57704
  } finally {
57542
- closeSync6(handle);
57705
+ closeSync7(handle);
57543
57706
  }
57544
57707
  }
57545
57708
  async pickUpWhatTheEditorIsPlaying() {
@@ -57936,7 +58099,7 @@ ${error.text}`;
57936
58099
  };
57937
58100
  const going = await this.runStillGoing(run);
57938
58101
  const projectPath = run.projectPath;
57939
- const projectGone = projectPath !== null && !existsSync36(join45(projectPath, "project.godot"));
58102
+ const projectGone = projectPath !== null && !existsSync36(join46(projectPath, "project.godot"));
57940
58103
  if (projectGone && callersName !== undefined) {
57941
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"]);
57942
58105
  }
@@ -58094,7 +58257,7 @@ ${error.text}`;
58094
58257
  return true;
58095
58258
  }
58096
58259
  }
58097
- await delay8(100);
58260
+ await delay9(100);
58098
58261
  }
58099
58262
  return false;
58100
58263
  }
@@ -58124,7 +58287,7 @@ ${error.text}`;
58124
58287
  break;
58125
58288
  }
58126
58289
  }
58127
- await delay8(RUN_POLL_MS);
58290
+ await delay9(RUN_POLL_MS);
58128
58291
  }
58129
58292
  if (!await this.runStillGoing(run)) {
58130
58293
  return await this.handleGetDebugOutput(args);
@@ -58186,7 +58349,7 @@ ${error.text}`;
58186
58349
  if (Date.now() >= until) {
58187
58350
  return false;
58188
58351
  }
58189
- await delay8(50);
58352
+ await delay9(50);
58190
58353
  }
58191
58354
  }
58192
58355
  async handleEditorConsole(args) {
@@ -58343,7 +58506,7 @@ ${error.text}`;
58343
58506
  notes.push(endedWithoutACode({
58344
58507
  throughEditor: run.throughEditor,
58345
58508
  announced: announced !== undefined || run.announcedPid !== undefined,
58346
- couldAnnounce: run.projectPath !== null && existsSync36(join45(run.projectPath, RUNTIME_SCRIPT)),
58509
+ couldAnnounce: run.projectPath !== null && existsSync36(join46(run.projectPath, RUNTIME_SCRIPT)),
58347
58510
  endedHere: typeof run.endedHere === "string",
58348
58511
  pickedUp: run.pickedUp === true
58349
58512
  }));
@@ -58601,6 +58764,66 @@ ${error.text}`;
58601
58764
  return error instanceof BridgeTimeout;
58602
58765
  }
58603
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
+ }
58604
58827
  async handleRescanFilesystem(args) {
58605
58828
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 30000;
58606
58829
  const started = Date.now();
@@ -59416,17 +59639,25 @@ ${error.text}`;
59416
59639
  ]);
59417
59640
  }
59418
59641
  }
59419
- const declaring = declaredButUncached(projectPath, typeName);
59420
- if (declaring !== null) {
59421
- const rebuilt = await this.rebuildClassCache(projectPath);
59422
- if (!rebuilt.ok) {
59423
- 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
+ }
59424
59649
  }
59425
- }
59426
- const outcome = await this.operation("run_editor_tool", { command, arguments: contained.value }, projectPath);
59427
- if (!outcome.ok) {
59428
- return this.answer(outcome);
59429
- }
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) {
59430
59661
  return this.jsonTextResponse({
59431
59662
  ...outcome.payload,
59432
59663
  ...declaring === null ? {} : {
@@ -59685,7 +59916,7 @@ ${error.text}`;
59685
59916
  }
59686
59917
  const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
59687
59918
  const declared = new RegExp(`^\\s*(?:@\\w+(?:\\([^)]*\\))?\\s+)*class_name\\s+${escaped}\\b`, "m");
59688
- if (declared.test(readFileSync41(scriptPath, "utf8"))) {
59919
+ if (declared.test(readFileSync42(scriptPath, "utf8"))) {
59689
59920
  return null;
59690
59921
  }
59691
59922
  const checked = await this.classesTheEditorCannotSee(args);
@@ -59883,7 +60114,7 @@ ${error.text}`;
59883
60114
  async ownRunOnItsWay(projectPath, budgetMs) {
59884
60115
  const run = await this.currentRun();
59885
60116
  const project = projectPath ?? run?.projectPath ?? null;
59886
- 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)) {
59887
60118
  return null;
59888
60119
  }
59889
60120
  const already = runtimesAnnounced().running;
@@ -59892,7 +60123,7 @@ ${error.text}`;
59892
60123
  return !run.announcedBefore.has(one.pid);
59893
60124
  }
59894
60125
  try {
59895
- return statSync20(one.file).mtimeMs >= run.startedAt;
60126
+ return statSync21(one.file).mtimeMs >= run.startedAt;
59896
60127
  } catch {
59897
60128
  return false;
59898
60129
  }
@@ -59993,7 +60224,7 @@ ${error.text}`;
59993
60224
  return this.createErrorResponse(staleNote);
59994
60225
  }
59995
60226
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
59996
- const screenshotPath = screenshotDir ? join45(screenshotDir, "capture.png") : null;
60227
+ const screenshotPath = screenshotDir ? join46(screenshotDir, "capture.png") : null;
59997
60228
  const refused = (message) => this.createErrorResponse(message);
59998
60229
  const onGame = {
59999
60230
  pid: choice.endpoint.pid,
@@ -60266,6 +60497,7 @@ var init_server = __esm(() => {
60266
60497
  init_tool_definitions();
60267
60498
  init_tool_reference();
60268
60499
  init_update_check();
60500
+ init_write_turns();
60269
60501
  TYPE_ARGUMENTS = {
60270
60502
  add_node: "nodeType",
60271
60503
  create_resource: "resourceType",
@@ -60338,7 +60570,7 @@ init_bridge_announce();
60338
60570
  init_engine_launch();
60339
60571
  init_engine_slots();
60340
60572
  import { existsSync as existsSync37 } from "node:fs";
60341
- 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";
60342
60574
 
60343
60575
  // src/error-mode.ts
60344
60576
  import process8 from "node:process";
@@ -60688,6 +60920,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
60688
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 |
60689
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 |
60690
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 |
60691
60924
 
60692
60925
  These need the runtime autoload, which \`gdharness setup\` registers. \`runtime_capture\` needs a
60693
60926
  window and refuses headless rather than handing back the last frame anything drew. On Windows a
@@ -62609,7 +62842,7 @@ class UsageError extends Refusal {
62609
62842
  function projectArgument(at) {
62610
62843
  const named = args.slice(at).find((value) => !value.startsWith("--"));
62611
62844
  const projectPath = resolve16(named ?? ".");
62612
- if (!existsSync37(join46(projectPath, "project.godot"))) {
62845
+ if (!existsSync37(join47(projectPath, "project.godot"))) {
62613
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.`);
62614
62847
  }
62615
62848
  return projectPath;
@@ -62673,7 +62906,7 @@ async function registerRuntime(godot, projectPath) {
62673
62906
  }
62674
62907
  const path = (named ?? "").replace(/^res:\/\//, "");
62675
62908
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
62676
- if (!existsSync37(join46(projectPath, path))) {
62909
+ if (!existsSync37(join47(projectPath, path))) {
62677
62910
  console.log(" that file is not in this project, so it boots with a missing script");
62678
62911
  }
62679
62912
  console.log(` ${useTheShippedLoader(RUNTIME_AUTOLOAD.name, path)}`);