gdharness 1.2.42 → 1.3.0

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
@@ -504,16 +504,23 @@ function readAnnouncement(path) {
504
504
  return null;
505
505
  }
506
506
  const said = parsed;
507
- if (typeof said.port !== "number" || typeof said.pid !== "number") {
507
+ if (said.protocol !== BRIDGE_ANNOUNCE_PROTOCOL || typeof said.host !== "string" || typeof said.port !== "number" || typeof said.pid !== "number" || typeof said.version !== "string" || typeof said.startedAt !== "string") {
508
508
  return null;
509
509
  }
510
- return said;
510
+ return {
511
+ protocol: said.protocol,
512
+ host: said.host,
513
+ port: said.port,
514
+ pid: said.pid,
515
+ version: said.version,
516
+ startedAt: said.startedAt
517
+ };
511
518
  } catch {
512
519
  return null;
513
520
  }
514
521
  }
515
522
  function serverBehind(announcement, startedAt) {
516
- if (announcement === null || typeof announcement.version !== "string" || startedAt === undefined) {
523
+ if (announcement === null || startedAt === undefined) {
517
524
  return null;
518
525
  }
519
526
  const announcedAt = Date.parse(announcement.startedAt);
@@ -668,12 +675,8 @@ function resolveHeadless(requested, host) {
668
675
  }
669
676
  return envValue("DISPLAY", host.variables) === undefined && envValue("WAYLAND_DISPLAY", host.variables) === undefined;
670
677
  }
671
- function resolveSilent(requested, variables) {
672
- if (typeof requested === "boolean") {
673
- return requested;
674
- }
675
- const said = envValue("GDHARNESS_SILENT", variables)?.toLowerCase();
676
- return said !== undefined && said !== "0" && said !== "false";
678
+ function resolveSilent(requested) {
679
+ return requested !== false;
677
680
  }
678
681
  function engineFlags(given) {
679
682
  const offered = Object.keys(ENGINE_FLAGS).join(", ");
@@ -727,10 +730,11 @@ function runArguments(options) {
727
730
  }
728
731
  return args;
729
732
  }
730
- function editorArguments(projectPath, ports, logFile, hidden) {
733
+ function editorArguments(projectPath, ports, logFile, hidden, silent) {
731
734
  return [
732
735
  "-e",
733
736
  ...hidden ? ["--headless"] : [],
737
+ ...silent && !hidden ? ["--audio-driver", "Dummy"] : [],
734
738
  "--path",
735
739
  projectPath,
736
740
  "--lsp-port",
@@ -9653,32 +9657,30 @@ var init_update_check = __esm(() => {
9653
9657
 
9654
9658
  // src/server-version.ts
9655
9659
  import { readFileSync as readFileSync7 } from "node:fs";
9656
- function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
9657
- if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
9658
- return addonDigest !== shippedDigest;
9659
- }
9660
- return addonVersion !== serverVersion;
9660
+ function staleAddon(addonDigest, shippedDigest) {
9661
+ return addonDigest !== shippedDigest;
9661
9662
  }
9662
- function sameCodeNote(addonVersion, serverVersion, addonDigest, shippedDigest) {
9663
- if (addonVersion === undefined || addonVersion === serverVersion || addonDigest === undefined || addonDigest === "" || addonDigest !== shippedDigest) {
9663
+ function sameCodeNote(addonVersion, serverVersion) {
9664
+ if (addonVersion === "" || addonVersion === serverVersion) {
9664
9665
  return;
9665
9666
  }
9666
9667
  const same = `The editor loaded the ${addonVersion} addons, which are the same code this ${serverVersion} server ships, so nothing needs restarting`;
9667
9668
  return isNewer(addonVersion, serverVersion) ? `${same}. This server is the older of the two, which a reconnect in your harness settles once the project pins ${addonVersion}.` : `${same}; addonVersion is the version the editor loaded when it started.`;
9668
9669
  }
9669
9670
  function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
9670
- if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
9671
+ if (!staleAddon(addonDigest, shippedDigest)) {
9671
9672
  return;
9672
9673
  }
9673
- const reported = addonVersion ?? "";
9674
+ const reported = addonVersion;
9674
9675
  const restart = onDisk !== undefined && onDisk !== shippedDigest ? `The project's own addons are not this server's either, so a restart would load the same code again: run ${gdharnessCommand(serverVersion, "upgrade")} in a shell in the project directory, which installs the ${serverVersion} addons, and editor_launch restart then picks them up.` : "Restart it with editor_launch restart to pick this one up.";
9676
+ const asked = "so this server asks it nothing but to quit:";
9675
9677
  if (reported === serverVersion) {
9676
- return `The editor is running a different build of the ${reported} addon from the one this server ships. ${restart}`;
9678
+ return `The editor is running a different build of the ${reported} addon from the one this server ships, ${asked} ${restart}`;
9677
9679
  }
9678
- const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
9679
- const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
9680
+ const editor = reported === "" ? "an addon with no version marker" : `the ${reported} addon`;
9681
+ const both = `The editor is running ${editor} while this server ships ${serverVersion}, ${asked}`;
9680
9682
  if (reported !== "" && isNewer(reported, serverVersion)) {
9681
- return `${both} This server is the older half: reconnect it in your harness so it spawns ${reported}.`;
9683
+ return `${both} this server is the older half; reconnect it in your harness so it spawns ${reported}.`;
9682
9684
  }
9683
9685
  return `${both} ${restart}`;
9684
9686
  }
@@ -9686,23 +9688,18 @@ function gdharnessCommand(version, args) {
9686
9688
  return `npx -y gdharness@${version} ${args}`;
9687
9689
  }
9688
9690
  function runtimeAddonUnknown(addon) {
9689
- return addon?.version === "" && addon.digest === "";
9691
+ return addon.version === "" && addon.digest === "";
9690
9692
  }
9691
9693
  function runtimeMismatch(pid, addon, serverVersion, shippedDigest, onDisk, projectPath) {
9692
- if (runtimeAddonUnknown(addon)) {
9693
- return;
9694
- }
9695
- if (addon !== undefined && !editorIsStale(addon.version, serverVersion, addon.digest, shippedDigest)) {
9694
+ if (runtimeAddonUnknown(addon) || !staleAddon(addon.digest, shippedDigest)) {
9696
9695
  return;
9697
9696
  }
9698
9697
  const game = `The game (pid ${pid})`;
9699
- if (addon !== undefined && addon.version !== "" && isNewer(addon.version, serverVersion)) {
9700
- return `${game} is running the ${addon.version} runtime addon while this server ships ${serverVersion}. This server is the older half: reconnect it in your harness so it spawns ${addon.version}.`;
9698
+ if (addon.version !== "" && isNewer(addon.version, serverVersion)) {
9699
+ return `${game} is running the ${addon.version} runtime addon while this server ships ${serverVersion}, so this server asks it nothing. This server is the older half: reconnect it in your harness so it spawns ${addon.version}.`;
9701
9700
  }
9702
9701
  let loaded;
9703
- if (addon === undefined) {
9704
- loaded = `a runtime addon from before games reported theirs, while this server ships ${serverVersion}`;
9705
- } else if (addon.version === serverVersion) {
9702
+ if (addon.version === serverVersion) {
9706
9703
  loaded = `a different build of the ${serverVersion} runtime addon from the one this server ships`;
9707
9704
  } else if (addon.version === "") {
9708
9705
  loaded = `a runtime addon with no version marker, while this server ships ${serverVersion}`;
@@ -9710,20 +9707,12 @@ function runtimeMismatch(pid, addon, serverVersion, shippedDigest, onDisk, proje
9710
9707
  loaded = `the ${addon.version} runtime addon, while this server ships ${serverVersion}`;
9711
9708
  }
9712
9709
  const remedy = onDisk !== undefined && onDisk !== shippedDigest ? `The project's own runtime addon is not this server's either, so starting the game again loads the same code: run ${gdharnessCommand(serverVersion, `upgrade "${projectPath}"`)} in a shell, which installs the ${serverVersion} addons, and the game picks them up when it next starts.` : `The project holds this server's runtime addon, so the game picks it up when it next starts.`;
9713
- return `${game} is running ${loaded}, and its answers come from that code. ${remedy}`;
9714
- }
9715
- function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
9716
- const staleNote = addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk);
9717
- if (staleNote === undefined || typeof answer !== "object" || answer === null || Array.isArray(answer)) {
9718
- return answer;
9719
- }
9720
- return { ...answer, addonIsStale: true, staleNote };
9710
+ return `${game} is running ${loaded}, so this server asks it nothing: its answers would come from that code. ${remedy}`;
9721
9711
  }
9722
- var DEBUG_MODE, GODOT_DEBUG_MODE_DEFAULT, SERVER_VERSION, UNVERSIONED = "an addon from before versions were reported";
9712
+ var DEBUG_MODE, SERVER_VERSION;
9723
9713
  var init_server_version = __esm(() => {
9724
9714
  init_update_check();
9725
- DEBUG_MODE = process.env["DEBUG"] === "true";
9726
- GODOT_DEBUG_MODE_DEFAULT = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE;
9715
+ DEBUG_MODE = process.env["GDHARNESS_DEBUG"] === "1";
9727
9716
  SERVER_VERSION = (() => {
9728
9717
  try {
9729
9718
  const pkg = JSON.parse(readFileSync7(new URL("../package.json", import.meta.url), "utf8"));
@@ -10812,9 +10801,7 @@ function failedReloadNote(failed) {
10812
10801
  const answered = failed.codeName === "" ? `error ${failed.code}` : `error ${failed.code}, ${failed.codeName}`;
10813
10802
  const unusable = "A failed compile leaves the copy the editor holds unusable, measured on 4.7.2: the editor can make no instance of it, and one it already made has lost its methods, while its member lists read as before.";
10814
10803
  let copy;
10815
- if (failed.restored === null) {
10816
- copy = `${unusable} This editor's addon does not put it back, so it stays that way until the script compiles: reload it then, or restart the editor with editor_launch restart.`;
10817
- } else if (!failed.restored) {
10804
+ if (!failed.restored) {
10818
10805
  copy = `${unusable} The editor compiled it again from the text it was built from, and that failed too${failed.restoredAs === "" ? "" : ` (${failed.restoredAs})`}, so it stays that way until the script compiles: reload it then, or restart the editor with editor_launch restart.`;
10819
10806
  } else if (unchanged) {
10820
10807
  copy = "The copy the editor holds was compiled again from the text it was built from, which puts it back as it was, with the members it held before, which listMembers lists.";
@@ -11568,24 +11555,12 @@ function addonsDigest(names, from) {
11568
11555
  return hash.digest("hex");
11569
11556
  }
11570
11557
  function shippedEditorDigest() {
11571
- if (shippedDigest === undefined) {
11572
- try {
11573
- shippedDigest = editorAddonDigest();
11574
- } catch {
11575
- shippedDigest = null;
11576
- }
11577
- }
11578
- return shippedDigest ?? undefined;
11558
+ shippedDigest ??= editorAddonDigest();
11559
+ return shippedDigest;
11579
11560
  }
11580
11561
  function shippedRuntimeDigest() {
11581
- if (shippedRuntime === undefined) {
11582
- try {
11583
- shippedRuntime = runtimeAddonDigest();
11584
- } catch {
11585
- shippedRuntime = null;
11586
- }
11587
- }
11588
- return shippedRuntime ?? undefined;
11562
+ shippedRuntime ??= runtimeAddonDigest();
11563
+ return shippedRuntime;
11589
11564
  }
11590
11565
  function installedEditorDigest(projectPath) {
11591
11566
  return digestMarker(projectPath, ADDONS[0]);
@@ -11899,7 +11874,7 @@ function annotationsOf(spec) {
11899
11874
  openWorldHint: false
11900
11875
  };
11901
11876
  }
11902
- var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "Through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. Each file the written one refers to is named by its uid, as the editor names it, except a file the project's uid cache has no uid for yet (one made since the project was last scanned), which is named by path and listed under namedByPath with the call that scans it. An editor of the project that is running and has not reached this server is waited for rather than written around.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR2, TOOL_SPECS, SPECS_BY_NAME;
11877
+ var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "Through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. Each file the written one refers to is named by its uid, as the editor names it, except a file the project's uid cache has no uid for yet (one made since the project was last scanned), which is named by path and listed under namedByPath with the call that scans it. An editor of the project that is running and has not reached this server is waited for rather than written around.", IMPORTED_FIRST = "importedFirst names the files whose import was behind them on disk, such as a picture changed since it was imported or one never imported, which were imported before the run through project_import reimport, so the run reads them as they are now; it is left out when every import was current. notImportedFirst lists, as project_import reimport does, any it could not import, and importNote says that the run reads those as they were. The import gives every script without a .uid sidecar one, as the editor's scan does, and uidsWrittenFirst names those scripts. importTookMs is how long the import took; it is outside the run's own time limit, so a call that imported can take that much longer than the limit it was given.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR2, TOOL_SPECS, SPECS_BY_NAME;
11903
11878
  var init_tool_definitions = __esm(() => {
11904
11879
  init_launch();
11905
11880
  PROJECT_INFO_SECTIONS = [
@@ -12090,7 +12065,7 @@ var init_tool_definitions = __esm(() => {
12090
12065
  {
12091
12066
  name: "project_settings",
12092
12067
  title: "Project settings",
12093
- description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events and deadzone, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change moves no other line; the save is the engine's, and like the editor's it writes Godot's header and config/features into a file that lacks them. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
12068
+ description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, or one on another release's addon that was not asked, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events and deadzone, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change moves no other line; the save is the engine's, and like the editor's it writes Godot's header and config/features into a file that lacks them. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
12094
12069
  parameters: {
12095
12070
  projectPath: PROJECT_PATH,
12096
12071
  setting: {
@@ -12372,7 +12347,7 @@ var init_tool_definitions = __esm(() => {
12372
12347
  title: "Run tests",
12373
12348
  effect: "additive",
12374
12349
  description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
12375
- answer: "time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for.",
12350
+ answer: "time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for. " + IMPORTED_FIRST,
12376
12351
  parameters: {
12377
12352
  projectPath: PROJECT_PATH,
12378
12353
  path: {
@@ -12807,7 +12782,7 @@ var init_tool_definitions = __esm(() => {
12807
12782
  scriptPath: {
12808
12783
  type: ["string", "array"],
12809
12784
  items: { type: "string" },
12810
- description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered as it always was. Several are answered together, with clean, errors and warnings for all of them: a script with nothing to report is named in nothingReported, and every other is answered under scripts in that same shape, including a script that could not be read, named with why while the rest are still asked about. Any number of scripts is one call, which reports progress as it goes.'
12785
+ description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered on its own. Several are answered together, with clean, errors and warnings for all of them: a script with nothing to report is named in nothingReported, and every other is answered under scripts in that same shape, including a script that could not be read, named with why while the rest are still asked about. Any number of scripts is one call, which reports progress as it goes.'
12811
12786
  }
12812
12787
  },
12813
12788
  requires: ["projectPath", "scriptPath"]
@@ -12924,6 +12899,11 @@ var init_tool_definitions = __esm(() => {
12924
12899
  type: "boolean",
12925
12900
  ops: ["open", "restart"],
12926
12901
  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: one visible editor, shown without taking the focus. On restart only for an editor a server opened; one opened by hand comes back as it was, so hidden is refused there."
12902
+ },
12903
+ silent: {
12904
+ type: "boolean",
12905
+ ops: ["open", "restart"],
12906
+ 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: 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. On restart only for an editor a server opened; one opened by hand comes back as it was."
12927
12907
  }
12928
12908
  },
12929
12909
  requires: [],
@@ -12978,7 +12958,7 @@ var init_tool_definitions = __esm(() => {
12978
12958
  silent: {
12979
12959
  type: "boolean",
12980
12960
  ops: ["start"],
12981
- description: "start: run the game with no sound out of the machine, by starting the engine with --audio-driver Dummy: every stream still plays and every playback position still advances, so what is playing can still be asked. The editor takes no engine flags for a game it plays, so a silent windowed run is started by this server, whose keeper is its debugger rather than the editor. A headless run, check's included, is silent already. Default false, or true when GDHARNESS_SILENT is set in the server's environment to anything but 0 or false; silent: false then plays through the editor as before."
12961
+ description: "start: run the game with no sound out of the machine, by starting the engine with --audio-driver Dummy: every stream still plays and every playback position still advances, so what is playing can still be asked. The editor takes no engine flags for a game it plays, so a silent windowed run is started by this server, whose keeper is its debugger rather than the editor. A headless run, check's included, is silent already, and so is a test run. Default true, for an exported build too: pass false to hear the game, which a windowed run with the editor connected then plays through the editor."
12982
12962
  },
12983
12963
  executable: {
12984
12964
  type: "string",
@@ -13048,7 +13028,7 @@ var init_tool_definitions = __esm(() => {
13048
13028
  graceful: {
13049
13029
  type: "boolean",
13050
13030
  ops: ["start", "stop"],
13051
- description: "stop, and a start that ends the run going before it: ask the game to close first, the way a player's close asks it, so its own handling of a close runs, which a game keeping a crash mark needs to take the mark down. The runtime addon delivers the close request to every node and then the root window's close_requested, and a tree with auto_accept_quit on then quits. The game is given ten seconds to exit, GDHARNESS_CLOSE_WAIT_MS sets another, and is ended the ordinary way when it does not, or when it has no runtime listening, an older addon, or does not answer within three seconds, as a game held at a breakpoint does not; notClosedBecause (previousRunNotClosedBecause on a start) then says which. Default true; false ends the process at once."
13031
+ description: "stop, and a start that ends the run going before it: ask the game to close first, the way a player's close asks it, so its own handling of a close runs, which a game keeping a crash mark needs to take the mark down. The runtime addon delivers the close request to every node and then the root window's close_requested, and a tree with auto_accept_quit on then quits. The game is given ten seconds to exit, GDHARNESS_CLOSE_WAIT_MS sets another, and is ended the ordinary way when it does not, or when it has no runtime listening, refuses the close, or does not answer within three seconds, as a game held at a breakpoint does not; notClosedBecause (previousRunNotClosedBecause on a start) then says which. Default true; false ends the process at once."
13052
13032
  },
13053
13033
  pid: {
13054
13034
  type: "integer",
@@ -13085,7 +13065,7 @@ var init_tool_definitions = __esm(() => {
13085
13065
  requires: []
13086
13066
  }
13087
13067
  },
13088
- answer: "start: through is gdharness for a game this server started itself, and editor for one the editor plays. scene is the scene the game started in, the project's main scene when none was given. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
13068
+ answer: "start: through is gdharness for a game this server started itself, and editor for one the editor plays. scene is the scene the game started in, the project's main scene when none was given. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. start and check: " + IMPORTED_FIRST + " An exported build carries its own imports and is not asked. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
13089
13069
  defaultOperation: "start"
13090
13070
  },
13091
13071
  {
@@ -13148,7 +13128,7 @@ var init_tool_definitions = __esm(() => {
13148
13128
  title: "Editor status",
13149
13129
  effect: "read",
13150
13130
  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, and editor_launch restart binds the port again. 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.",
13151
- 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, 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. 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; 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, and editor_launch restart binds it again. 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. 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. Every runtime_* answer from such a game carries addonIsStale and staleNote too. 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, 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. 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 while that is read 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, and syncsBreakpoints that it keeps its breakpoints when a debug session opens, which Godot otherwise clears. 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.",
13131
+ 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, 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. 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; 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, and editor_launch restart binds it again. 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. 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, 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 while that is read 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, and syncsBreakpoints that it keeps its breakpoints when a debug session opens, which Godot otherwise clears. 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.",
13152
13132
  parameters: {},
13153
13133
  requires: []
13154
13134
  },
@@ -13429,7 +13409,7 @@ var init_tool_definitions = __esm(() => {
13429
13409
  viewportPath: {
13430
13410
  type: "string",
13431
13411
  ops: ["viewport"],
13432
- description: "viewport: the Viewport node. Default the root viewport."
13412
+ description: "viewport: the Viewport node. Default the root viewport. A Window of the game that is hidden or minimised has nothing drawn to it, and is refused naming which."
13433
13413
  },
13434
13414
  width: {
13435
13415
  type: "integer",
@@ -13735,7 +13715,7 @@ var init_tool_definitions = __esm(() => {
13735
13715
  {
13736
13716
  name: "debug_breakpoint",
13737
13717
  title: "Breakpoints",
13738
- description: "Sets or removes a breakpoint. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it at once (runningGame says so). The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor running an older addon is said so under breakpointsAtRisk, here and in editor_status.",
13718
+ description: "Sets or removes a breakpoint. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it at once (runningGame says so). The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor where it was turned off again is said so under breakpointsAtRisk, here and in editor_status.",
13739
13719
  parameters: {
13740
13720
  projectPath: PROJECT_PATH,
13741
13721
  scriptPath: SCRIPT_PATH,
@@ -35683,6 +35663,10 @@ function parseAnnouncement(file, pid) {
35683
35663
  }
35684
35664
  const editorPid = readNumber(fields, "editor_pid");
35685
35665
  const addonVersion = readString(fields, "addon_version");
35666
+ const addonDigest = readString(fields, "addon_digest");
35667
+ if (addonVersion === undefined || addonDigest === undefined) {
35668
+ return { kind: "rubbish" };
35669
+ }
35686
35670
  return {
35687
35671
  kind: "runtime",
35688
35672
  endpoint: {
@@ -35693,7 +35677,7 @@ function parseAnnouncement(file, pid) {
35693
35677
  file,
35694
35678
  ...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {},
35695
35679
  ...when,
35696
- ...addonVersion === undefined ? {} : { addon: { version: addonVersion, digest: readString(fields, "addon_digest") ?? "" } }
35680
+ addon: { version: addonVersion, digest: addonDigest }
35697
35681
  }
35698
35682
  };
35699
35683
  }
@@ -36148,18 +36132,16 @@ function runtimeRequest(endpoint, command, params, timeoutMs, afterTheWait = "dr
36148
36132
  if (!welcomed) {
36149
36133
  welcomed = true;
36150
36134
  taken();
36151
- if (readString(message, "type") === "welcome") {
36152
- const spoken = readNumber(message, "protocol");
36153
- if (spoken !== RUNTIME_PROTOCOL) {
36154
- finish({
36155
- ok: false,
36156
- reason: "protocol",
36157
- message: `The game (${describe2(endpoint)}) speaks runtime protocol ${spoken ?? "unknown"} and this server speaks ${RUNTIME_PROTOCOL}. Reinstall the runtime addon into the project.`
36158
- });
36159
- return;
36160
- }
36161
- continue;
36135
+ const spoken = readString(message, "type") === "welcome" ? readNumber(message, "protocol") : undefined;
36136
+ if (spoken !== RUNTIME_PROTOCOL) {
36137
+ finish({
36138
+ ok: false,
36139
+ reason: "protocol",
36140
+ message: `The game (${describe2(endpoint)}) speaks runtime protocol ${spoken ?? "unknown"} and this server speaks ${RUNTIME_PROTOCOL}. Reinstall the runtime addon into the project.`
36141
+ });
36142
+ return;
36162
36143
  }
36144
+ continue;
36163
36145
  }
36164
36146
  if (message["id"] !== id) {
36165
36147
  continue;
@@ -36224,7 +36206,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, afterTheWait = "dr
36224
36206
  });
36225
36207
  });
36226
36208
  }
36227
- var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, CONSOLE_BOUNDARY = "[gdharness] runtime listening on ", ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, judging = null, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1, LATE_ANSWER_MS, LATE_KEPT = 50, lateAnswers, UNTAKEN_PER_GAME = 4, REDIAL_MS = 50, untaken;
36209
+ var RUNTIME_PROTOCOL = 3, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, CONSOLE_BOUNDARY = "[gdharness] runtime listening on ", ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, judging = null, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1, LATE_ANSWER_MS, LATE_KEPT = 50, lateAnswers, UNTAKEN_PER_GAME = 4, REDIAL_MS = 50, untaken;
36228
36210
  var init_runtime_client = __esm(() => {
36229
36211
  init_alive();
36230
36212
  init_launch();
@@ -36890,8 +36872,8 @@ class GodotDAPClient {
36890
36872
  }
36891
36873
  const response = await this.sendRequest("initialize", {
36892
36874
  adapterID: "godot",
36893
- clientID: "godot-mcp",
36894
- clientName: "godot-mcp",
36875
+ clientID: "gdharness",
36876
+ clientName: "gdharness",
36895
36877
  locale: "en",
36896
36878
  linesStartAt1: true,
36897
36879
  columnsStartAt1: true,
@@ -37598,8 +37580,8 @@ function theConsoleWasNotCaptured(why, projectPath) {
37598
37580
  if (why.kind === "not ours") {
37599
37581
  return "This editor was not opened by gdharness, so its console was never captured and nothing " + "here has it: an editor prints to a console no plugin can read, and the addon cannot get " + "at it from inside. This is not an editor that printed nothing.";
37600
37582
  }
37601
- if (why.kind === "older server") {
37602
- return `This editor was opened by a gdharness server, but the console beside ${projectPath} belongs ` + `to editor ${why.notedPid ?? "none recorded"} rather than to this one, so it holds an ` + "earlier session and is not offered as this one. A server from before consoles were " + "captured leaves exactly this.";
37583
+ if (why.kind === "another editor") {
37584
+ return `This editor was opened by a gdharness server, but the console beside ${projectPath} belongs ` + `to editor ${why.notedPid ?? "none recorded"} rather than to this one, so it holds an ` + "earlier session and is not offered as this one.";
37603
37585
  }
37604
37586
  return "This editor was opened by gdharness and has written nothing to its console file yet, not " + "even the engine version it prints first, so the file is missing rather than empty. An " + "editor that could not open the file for writing leaves this.";
37605
37587
  }
@@ -37609,7 +37591,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
37609
37591
  }
37610
37592
  const noted = readEditorLogNote(projectPath);
37611
37593
  if (noted === null || editorPid === null || noted.pid !== editorPid) {
37612
- return { kind: "older server", notedPid: noted?.pid ?? null };
37594
+ return { kind: "another editor", notedPid: noted?.pid ?? null };
37613
37595
  }
37614
37596
  const path = editorLogPath(projectPath);
37615
37597
  if (!existsSync20(path)) {
@@ -43172,8 +43154,11 @@ function resolveDefaultBridgeHost() {
43172
43154
  const host = process.env["GDHARNESS_BRIDGE_HOST"]?.trim();
43173
43155
  return host === undefined || host === "" ? DEFAULT_HOST : host;
43174
43156
  }
43157
+ function greetingFaults(message) {
43158
+ return Object.entries(GREETING_FIELDS).filter(([field, type]) => typeof message[field] !== type).map(([field]) => field);
43159
+ }
43175
43160
  function servedPort(said) {
43176
- return typeof said === "number" && Number.isInteger(said) && said > 0 && said <= 65535 ? said : undefined;
43161
+ return Number.isInteger(said) && said > 0 && said <= 65535 ? said : undefined;
43177
43162
  }
43178
43163
  function mayYetConnect(listeningSince, now = Date.now()) {
43179
43164
  return listeningSince === undefined || now - listeningSince.getTime() < CONNECT_WINDOW_MS;
@@ -43201,13 +43186,27 @@ function getDefaultBridge(ownProject = null) {
43201
43186
  defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
43202
43187
  return defaultBridge;
43203
43188
  }
43204
- var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, MOST_CLOSE_REASON_BYTES = 123, OTHER_PROJECT_CLOSE_CODE = 4001, ONE_SHOT_CLOSE_CODE = 4002, CONNECT_WINDOW_MS = 35000, BridgeTimeout, EditorBusy, BUSY_RETRY_MS = 100, GodotBridge, defaultBridge = null;
43189
+ var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, MOST_CLOSE_REASON_BYTES = 123, OTHER_PROJECT_CLOSE_CODE = 4001, ONE_SHOT_CLOSE_CODE = 4002, GREETING_FIELDS, CONNECT_WINDOW_MS = 35000, BridgeTimeout, AddonOfAnotherRelease, ASKED_OF_ANY_RELEASE, EditorBusy, BUSY_RETRY_MS = 100, GodotBridge, defaultBridge = null;
43205
43190
  var init_godot_bridge = __esm(() => {
43206
43191
  init_wrapper();
43207
43192
  init_errors();
43208
43193
  init_launch();
43209
43194
  init_paths();
43210
43195
  init_ports();
43196
+ init_server_version();
43197
+ init_setup();
43198
+ GREETING_FIELDS = {
43199
+ project_path: "string",
43200
+ addon_version: "string",
43201
+ addon_digest: "string",
43202
+ editor_pid: "number",
43203
+ lsp_port: "number",
43204
+ dap_port: "number",
43205
+ debug_port: "number",
43206
+ syncs_breakpoints: "boolean",
43207
+ opened_by_a_server: "boolean",
43208
+ started_by_an_editor: "boolean"
43209
+ };
43211
43210
  BridgeTimeout = class BridgeTimeout extends Error {
43212
43211
  toolName;
43213
43212
  waitedMs;
@@ -43217,6 +43216,9 @@ var init_godot_bridge = __esm(() => {
43217
43216
  this.waitedMs = waitedMs;
43218
43217
  }
43219
43218
  };
43219
+ AddonOfAnotherRelease = class AddonOfAnotherRelease extends Error {
43220
+ };
43221
+ ASKED_OF_ANY_RELEASE = new Set(["quit_editor", "restart_editor"]);
43220
43222
  EditorBusy = class EditorBusy extends Error {
43221
43223
  constructor(toolName) {
43222
43224
  super(`The editor was scanning or importing its files, so ${toolName} was not run.`);
@@ -43391,6 +43393,14 @@ var init_godot_bridge = __esm(() => {
43391
43393
  portWanted: this.wantedPort !== 0 && this.boundPort !== null && this.boundPort !== this.wantedPort ? this.wantedPort : undefined
43392
43394
  };
43393
43395
  }
43396
+ anotherRelease() {
43397
+ const info = this.connectionInfo;
43398
+ if (info?.greeted !== true || info.addonVersion === undefined || info.addonDigest === undefined) {
43399
+ return;
43400
+ }
43401
+ const project = info.projectPath;
43402
+ return addonMismatch(info.addonVersion, SERVER_VERSION, info.addonDigest, shippedEditorDigest(), project === undefined || project === "" ? undefined : installedEditorDigest(project));
43403
+ }
43394
43404
  invokeTool(toolName, args, options = {}) {
43395
43405
  const counted = options.counted ?? true;
43396
43406
  const resourceKey = this.getResourceKey(args);
@@ -43538,24 +43548,33 @@ var init_godot_bridge = __esm(() => {
43538
43548
  case "godot_ready":
43539
43549
  if (this.connectionInfo) {
43540
43550
  if (this.belongsElsewhere(message.project_path)) {
43541
- this.elsewhereUnlessOneShot(message.project_path, message.editor_pid, message.opened_by_a_server === true);
43551
+ this.elsewhereUnlessOneShot(message.project_path, message.editor_pid, message.opened_by_a_server);
43542
43552
  return;
43543
43553
  }
43544
43554
  this.connectionInfo.projectPath = withoutTrailingSeparator(message.project_path);
43545
- this.connectionInfo.addonVersion = message.addon_version ?? "";
43555
+ const faults = greetingFaults(message);
43556
+ if (faults.length > 0) {
43557
+ this.log("info", `Godot ready from another release's addon, which sent no ${faults.join(", ")}`);
43558
+ this.connectionInfo.addonVersion = typeof message.addon_version === "string" ? message.addon_version : "";
43559
+ this.connectionInfo.addonDigest = "";
43560
+ this.connectionInfo.editorPid = typeof message.editor_pid === "number" ? message.editor_pid : undefined;
43561
+ this.connectionInfo.greeted = true;
43562
+ this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
43563
+ return;
43564
+ }
43565
+ this.connectionInfo.addonVersion = message.addon_version;
43546
43566
  this.connectionInfo.addonDigest = message.addon_digest;
43547
43567
  this.connectionInfo.editorPid = message.editor_pid;
43548
43568
  this.connectionInfo.lspPort = servedPort(message.lsp_port);
43549
43569
  this.connectionInfo.dapPort = servedPort(message.dap_port);
43550
43570
  this.connectionInfo.debugPort = servedPort(message.debug_port);
43551
- this.connectionInfo.openedByAServer = message.opened_by_a_server === true;
43552
- this.connectionInfo.startedByAnEditor = typeof message.started_by_an_editor === "boolean" ? message.started_by_an_editor : undefined;
43553
- this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
43571
+ this.connectionInfo.openedByAServer = message.opened_by_a_server;
43572
+ this.connectionInfo.startedByAnEditor = message.started_by_an_editor;
43573
+ this.connectionInfo.syncsBreakpoints = message.syncs_breakpoints;
43554
43574
  this.log("info", `Godot ready: ${message.project_path}`);
43555
43575
  this.connectionInfo.greeted = true;
43556
- const pid = this.connectionInfo.editorPid;
43557
- if (!this.connectionInfo.openedByAServer && pid !== undefined && this.oneShotReader !== null) {
43558
- this.identify(pid);
43576
+ if (!message.opened_by_a_server && this.oneShotReader !== null) {
43577
+ this.identify(message.editor_pid);
43559
43578
  return;
43560
43579
  }
43561
43580
  this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
@@ -43572,6 +43591,10 @@ var init_godot_bridge = __esm(() => {
43572
43591
  if (!this.isConnected()) {
43573
43592
  return Promise.reject(new Error("Godot is not connected"));
43574
43593
  }
43594
+ const otherRelease = ASKED_OF_ANY_RELEASE.has(toolName) ? undefined : this.anotherRelease();
43595
+ if (otherRelease !== undefined) {
43596
+ return Promise.reject(new AddonOfAnotherRelease(otherRelease));
43597
+ }
43575
43598
  const requestId = randomUUID();
43576
43599
  const message = {
43577
43600
  type: "tool_invoke",
@@ -43794,6 +43817,168 @@ var init_godot_bridge = __esm(() => {
43794
43817
  };
43795
43818
  });
43796
43819
 
43820
+ // src/import-freshness.ts
43821
+ import { createHash as createHash5 } from "node:crypto";
43822
+ import { existsSync as existsSync25, readdirSync as readdirSync13, readFileSync as readFileSync24, statSync as statSync12 } from "node:fs";
43823
+ import { join as join29 } from "node:path";
43824
+ function md5Of(file) {
43825
+ let stats;
43826
+ try {
43827
+ stats = statSync12(file);
43828
+ } catch {
43829
+ return null;
43830
+ }
43831
+ const known = hashes.get(file);
43832
+ if (known !== undefined && known.size === stats.size && known.mtimeMs === stats.mtimeMs) {
43833
+ return known.md5;
43834
+ }
43835
+ let md5;
43836
+ try {
43837
+ md5 = createHash5("md5").update(readFileSync24(file)).digest("hex");
43838
+ } catch {
43839
+ return null;
43840
+ }
43841
+ hashes.set(file, { size: stats.size, mtimeMs: stats.mtimeMs, md5 });
43842
+ return md5;
43843
+ }
43844
+ function sectionOf(text, section) {
43845
+ const keys = new Map;
43846
+ let inside = false;
43847
+ for (const line of text.split(/\r?\n/)) {
43848
+ const header = /^\[([^\]]+)\]\s*$/.exec(line);
43849
+ if (header !== null) {
43850
+ inside = header[1] === section;
43851
+ continue;
43852
+ }
43853
+ const pair = inside ? /^([A-Za-z0-9_./]+)=(.*)$/.exec(line) : null;
43854
+ if (pair?.[1] !== undefined && pair[2] !== undefined) {
43855
+ keys.set(pair[1], pair[2]);
43856
+ }
43857
+ }
43858
+ return keys;
43859
+ }
43860
+ function unquoted(value) {
43861
+ return value === undefined ? "" : value.replace(/^"(.*)"$/, "$1");
43862
+ }
43863
+ function listed2(value) {
43864
+ return [...(value ?? "").matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1] ?? "");
43865
+ }
43866
+ function onDisk(projectPath, resPath) {
43867
+ return join29(projectPath, resPath.slice("res://".length));
43868
+ }
43869
+ function behind(projectPath, resPath, sidecar) {
43870
+ const remap = sectionOf(sidecar, "remap");
43871
+ const importer = unquoted(remap.get("importer"));
43872
+ if (importer === "keep" || importer === "skip") {
43873
+ return null;
43874
+ }
43875
+ const outputs = listed2(sectionOf(sidecar, "deps").get("dest_files"));
43876
+ if (outputs.some((output) => !existsSync25(onDisk(projectPath, output)))) {
43877
+ return "its imported output is not on disk";
43878
+ }
43879
+ const name = resPath.slice(resPath.lastIndexOf("/") + 1);
43880
+ const record = join29(projectPath, ".godot", "imported", `${name}-${createHash5("md5").update(resPath, "utf8").digest("hex")}.md5`);
43881
+ let recorded = "";
43882
+ try {
43883
+ recorded = /^source_md5="([0-9a-f]*)"/m.exec(readFileSync24(record, "utf8"))?.[1] ?? "";
43884
+ } catch {
43885
+ recorded = "";
43886
+ }
43887
+ if (recorded === "") {
43888
+ return "its import recorded no hash of its source";
43889
+ }
43890
+ const current = md5Of(onDisk(projectPath, resPath));
43891
+ return current === null || current === recorded ? null : "the source changed after it was imported";
43892
+ }
43893
+ function staleImports(projectPath) {
43894
+ const stale = [];
43895
+ const visit = (directory, resDirectory) => {
43896
+ if (existsSync25(join29(directory, ".gdignore")) || existsSync25(join29(directory, VERSION_MARKER))) {
43897
+ return;
43898
+ }
43899
+ let entries;
43900
+ try {
43901
+ entries = readdirSync13(directory, { withFileTypes: true });
43902
+ } catch {
43903
+ return;
43904
+ }
43905
+ const names = new Set(entries.map((entry) => entry.name));
43906
+ for (const entry of entries) {
43907
+ if (entry.name.startsWith(".")) {
43908
+ continue;
43909
+ }
43910
+ const resPath = `${resDirectory}${entry.name}`;
43911
+ if (entry.isDirectory()) {
43912
+ visit(join29(directory, entry.name), `${resPath}/`);
43913
+ continue;
43914
+ }
43915
+ if (!entry.isFile()) {
43916
+ continue;
43917
+ }
43918
+ if (entry.name.endsWith(".import")) {
43919
+ const source = resPath.slice(0, -".import".length);
43920
+ if (!names.has(entry.name.slice(0, -".import".length))) {
43921
+ continue;
43922
+ }
43923
+ let sidecar;
43924
+ try {
43925
+ sidecar = readFileSync24(join29(directory, entry.name), "utf8");
43926
+ } catch {
43927
+ continue;
43928
+ }
43929
+ const reason = behind(projectPath, source, sidecar);
43930
+ if (reason !== null) {
43931
+ stale.push({ path: source, reason });
43932
+ }
43933
+ continue;
43934
+ }
43935
+ const extension = entry.name.slice(entry.name.lastIndexOf(".") + 1).toLowerCase();
43936
+ if (IMPORTABLE.has(extension) && !names.has(`${entry.name}.import`)) {
43937
+ stale.push({ path: resPath, reason: "it has not been imported" });
43938
+ }
43939
+ }
43940
+ };
43941
+ visit(projectPath, "res://");
43942
+ return stale.sort((one, other) => one.path.localeCompare(other.path));
43943
+ }
43944
+ var IMPORTABLE_EXTENSIONS, IMPORTABLE, hashes;
43945
+ var init_import_freshness = __esm(() => {
43946
+ init_project_scan();
43947
+ IMPORTABLE_EXTENSIONS = [
43948
+ "png",
43949
+ "jpg",
43950
+ "jpeg",
43951
+ "webp",
43952
+ "svg",
43953
+ "bmp",
43954
+ "tga",
43955
+ "exr",
43956
+ "hdr",
43957
+ "dds",
43958
+ "ktx",
43959
+ "wav",
43960
+ "mp3",
43961
+ "ogg",
43962
+ "ttf",
43963
+ "otf",
43964
+ "ttc",
43965
+ "otc",
43966
+ "woff",
43967
+ "woff2",
43968
+ "pfb",
43969
+ "pfm",
43970
+ "fnt",
43971
+ "font",
43972
+ "glb",
43973
+ "gltf",
43974
+ "fbx",
43975
+ "obj",
43976
+ "dae"
43977
+ ];
43978
+ IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
43979
+ hashes = new Map;
43980
+ });
43981
+
43797
43982
  // src/keeper-channel.ts
43798
43983
  import { connect as connect2, createServer as createServer2 } from "node:net";
43799
43984
  async function askTheKeeperToStop(address, withinMs) {
@@ -43808,9 +43993,6 @@ async function askTheKeepersDebugger(address, request, withinMs) {
43808
43993
  if (typeof answered !== "string") {
43809
43994
  return answered;
43810
43995
  }
43811
- if (answered === "unknown") {
43812
- return { unreached: "its keeper was started by a gdharness from before game debuggers, and has none" };
43813
- }
43814
43996
  try {
43815
43997
  const parsed = JSON.parse(answered);
43816
43998
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
@@ -43860,17 +44042,17 @@ var DEBUG_PREFIX = "debug ";
43860
44042
  var init_keeper_channel = () => {};
43861
44043
 
43862
44044
  // src/lifecycle-log.ts
43863
- import { appendFileSync, mkdirSync as mkdirSync14, renameSync as renameSync5, statSync as statSync12 } from "node:fs";
43864
- import { dirname as dirname20, join as join29 } from "node:path";
44045
+ import { appendFileSync, mkdirSync as mkdirSync14, renameSync as renameSync5, statSync as statSync13 } from "node:fs";
44046
+ import { dirname as dirname20, join as join30 } from "node:path";
43865
44047
  import process11 from "node:process";
43866
44048
  function lifecycleLogPath() {
43867
- return join29(runtimeDirectory(), "servers.log");
44049
+ return join30(runtimeDirectory(), "servers.log");
43868
44050
  }
43869
44051
  function writeLifecycle(event, fields = {}) {
43870
44052
  const path = lifecycleLogPath();
43871
44053
  try {
43872
44054
  mkdirSync14(dirname20(path), { recursive: true });
43873
- if ((statSync12(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
44055
+ if ((statSync13(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
43874
44056
  try {
43875
44057
  renameSync5(path, `${path}.1`);
43876
44058
  } catch {}
@@ -44184,10 +44366,10 @@ var init_prompts = __esm(() => {
44184
44366
  });
44185
44367
 
44186
44368
  // src/restart-note.ts
44187
- import { mkdirSync as mkdirSync15, readFileSync as readFileSync24, rmSync as rmSync13, writeFileSync as writeFileSync17 } from "node:fs";
44188
- import { dirname as dirname21, join as join30 } from "node:path";
44369
+ import { mkdirSync as mkdirSync15, readFileSync as readFileSync25, rmSync as rmSync13, writeFileSync as writeFileSync17 } from "node:fs";
44370
+ import { dirname as dirname21, join as join31 } from "node:path";
44189
44371
  function restartNotePath(projectPath) {
44190
- return join30(projectPath, ".godot", "gdharness-restart.json");
44372
+ return join31(projectPath, ".godot", "gdharness-restart.json");
44191
44373
  }
44192
44374
  function noteRestartBegun(note) {
44193
44375
  const path = restartNotePath(note.projectPath);
@@ -44201,7 +44383,7 @@ function noteRestartBegun(note) {
44201
44383
  }
44202
44384
  function restartOwed(projectPath) {
44203
44385
  try {
44204
- const parsed = JSON.parse(readFileSync24(restartNotePath(projectPath), "utf8"));
44386
+ const parsed = JSON.parse(readFileSync25(restartNotePath(projectPath), "utf8"));
44205
44387
  if (typeof parsed !== "object" || parsed === null) {
44206
44388
  return null;
44207
44389
  }
@@ -44226,18 +44408,18 @@ var init_restart_note = __esm(() => {
44226
44408
  });
44227
44409
 
44228
44410
  // src/run-record.ts
44229
- import { createHash as createHash5 } from "node:crypto";
44230
- import { mkdirSync as mkdirSync16, openSync as openSync3, readdirSync as readdirSync13, readFileSync as readFileSync25, realpathSync as realpathSync6, rmSync as rmSync14, statSync as statSync13 } from "node:fs";
44231
- import { basename as basename9, dirname as dirname22, join as join31, resolve as resolve7 } from "node:path";
44411
+ import { createHash as createHash6 } from "node:crypto";
44412
+ import { mkdirSync as mkdirSync16, openSync as openSync3, readdirSync as readdirSync14, readFileSync as readFileSync26, realpathSync as realpathSync6, rmSync as rmSync14, statSync as statSync14 } from "node:fs";
44413
+ import { basename as basename9, dirname as dirname22, join as join32, resolve as resolve7 } from "node:path";
44232
44414
  function runsDirectory() {
44233
- return join31(runtimeDirectory(), "runs");
44415
+ return join32(runtimeDirectory(), "runs");
44234
44416
  }
44235
44417
  function realAsFarAsItExists(path) {
44236
44418
  const missing = [];
44237
44419
  let at = path;
44238
44420
  for (;; ) {
44239
44421
  try {
44240
- return join31(realpathSync6.native(at), ...missing);
44422
+ return join32(realpathSync6.native(at), ...missing);
44241
44423
  } catch {
44242
44424
  const parent = dirname22(at);
44243
44425
  if (parent === at) {
@@ -44251,13 +44433,13 @@ function realAsFarAsItExists(path) {
44251
44433
  function projectKey(projectPath) {
44252
44434
  const real = realAsFarAsItExists(resolve7(projectPath)).replace(/[\\/]+$/, "");
44253
44435
  const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
44254
- return createHash5("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
44436
+ return createHash6("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
44255
44437
  }
44256
44438
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
44257
- return join31(directory, "runs", `note-${projectKey(projectPath)}.json`);
44439
+ return join32(directory, "runs", `note-${projectKey(projectPath)}.json`);
44258
44440
  }
44259
44441
  function latestRunPath(servedBy, directory = runtimeDirectory()) {
44260
- return join31(directory, "runs", `latest-${projectKey(servedBy)}.json`);
44442
+ return join32(directory, "runs", `latest-${projectKey(servedBy)}.json`);
44261
44443
  }
44262
44444
  function writeLatestRun(servedBy, latest) {
44263
44445
  try {
@@ -44267,7 +44449,7 @@ function writeLatestRun(servedBy, latest) {
44267
44449
  }
44268
44450
  function readLatestRun(servedBy, directory = runtimeDirectory()) {
44269
44451
  try {
44270
- const fields = JSON.parse(readFileSync25(latestRunPath(servedBy, directory), "utf8"));
44452
+ const fields = JSON.parse(readFileSync26(latestRunPath(servedBy, directory), "utf8"));
44271
44453
  if (typeof fields !== "object" || fields === null) {
44272
44454
  return null;
44273
44455
  }
@@ -44282,20 +44464,20 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
44282
44464
  }
44283
44465
  function openTranscript(startedAt) {
44284
44466
  mkdirSync16(runsDirectory(), { recursive: true });
44285
- const path = join31(runsDirectory(), `run-${startedAt}.log`);
44467
+ const path = join32(runsDirectory(), `run-${startedAt}.log`);
44286
44468
  return { path, fd: openSync3(path, "a") };
44287
44469
  }
44288
44470
  function profilePath(startedAt) {
44289
- return join31(runsDirectory(), `run-${startedAt}.profile.json`);
44471
+ return join32(runsDirectory(), `run-${startedAt}.profile.json`);
44290
44472
  }
44291
44473
  function profileMarkPath(startedAt) {
44292
- return join31(runsDirectory(), `run-${startedAt}.profile-mark.json`);
44474
+ return join32(runsDirectory(), `run-${startedAt}.profile-mark.json`);
44293
44475
  }
44294
44476
  function noteIsAbout(note, playing) {
44295
44477
  return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
44296
44478
  }
44297
44479
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
44298
- return join31(directory, "runs", `editor-${projectKey(projectPath)}.json`);
44480
+ return join32(directory, "runs", `editor-${projectKey(projectPath)}.json`);
44299
44481
  }
44300
44482
  function writeEditorRunNote(note) {
44301
44483
  mkdirSync16(runsDirectory(), { recursive: true });
@@ -44306,7 +44488,7 @@ function writeEditorRunNote(note) {
44306
44488
  }
44307
44489
  function readEditorRunNote(projectPath) {
44308
44490
  for (const directory of runtimeDirectories()) {
44309
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join31(directory, "runs", "editor-run.json"));
44491
+ const note = editorNoteAt(editorNotePath(projectPath, directory));
44310
44492
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
44311
44493
  return note;
44312
44494
  }
@@ -44316,7 +44498,7 @@ function readEditorRunNote(projectPath) {
44316
44498
  function editorNoteAt(path) {
44317
44499
  let parsed;
44318
44500
  try {
44319
- parsed = JSON.parse(readFileSync25(path, "utf8"));
44501
+ parsed = JSON.parse(readFileSync26(path, "utf8"));
44320
44502
  } catch {
44321
44503
  return null;
44322
44504
  }
@@ -44340,10 +44522,6 @@ function readRunRecord(projectPath) {
44340
44522
  if (own !== null) {
44341
44523
  return own;
44342
44524
  }
44343
- const shared = recordAt(join31(directory, "runs", SHARED_NOTE));
44344
- if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
44345
- return shared;
44346
- }
44347
44525
  }
44348
44526
  return null;
44349
44527
  }
@@ -44352,13 +44530,13 @@ function everyRunRecord() {
44352
44530
  for (const directory of runtimeDirectories()) {
44353
44531
  let entries;
44354
44532
  try {
44355
- entries = readdirSync13(join31(directory, "runs"));
44533
+ entries = readdirSync14(join32(directory, "runs"));
44356
44534
  } catch {
44357
44535
  continue;
44358
44536
  }
44359
44537
  for (const entry of entries) {
44360
- if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
44361
- const record = recordAt(join31(directory, "runs", entry));
44538
+ if (entry.startsWith("note-") && entry.endsWith(".json")) {
44539
+ const record = recordAt(join32(directory, "runs", entry));
44362
44540
  if (record !== null) {
44363
44541
  found.push(record);
44364
44542
  }
@@ -44370,7 +44548,7 @@ function everyRunRecord() {
44370
44548
  function recordAt(path) {
44371
44549
  let parsed;
44372
44550
  try {
44373
- parsed = JSON.parse(readFileSync25(path, "utf8"));
44551
+ parsed = JSON.parse(readFileSync26(path, "utf8"));
44374
44552
  } catch {
44375
44553
  return null;
44376
44554
  }
@@ -44385,17 +44563,23 @@ function recordAt(path) {
44385
44563
  return null;
44386
44564
  }
44387
44565
  const args = fields["arguments"];
44566
+ const startedBy = fields["startedBy"];
44567
+ const projectPath = fields["projectPath"];
44568
+ const command = fields["command"];
44569
+ if (typeof startedBy !== "number" || typeof projectPath !== "string" || projectPath === "" || typeof command !== "string" || !Array.isArray(args)) {
44570
+ return null;
44571
+ }
44388
44572
  return {
44389
44573
  pid,
44390
44574
  transcript,
44391
44575
  startedAt,
44392
- ...typeof fields["startedBy"] === "number" ? { startedBy: fields["startedBy"] } : {},
44393
- projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
44576
+ startedBy,
44577
+ projectPath,
44394
44578
  ...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
44395
44579
  ...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
44396
44580
  ...typeof fields["profile"] === "string" && fields["profile"] !== "" ? { profile: fields["profile"] } : {},
44397
- arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
44398
- ...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
44581
+ arguments: args.filter((value) => typeof value === "string"),
44582
+ command,
44399
44583
  ...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
44400
44584
  ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
44401
44585
  ...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
@@ -44423,7 +44607,7 @@ async function runningAs(pid) {
44423
44607
  const began = await startedAt(pid, null);
44424
44608
  const when = began === null ? {} : { startedAt: began };
44425
44609
  if (process.platform === "linux") {
44426
- const raw = readFileSync25(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
44610
+ const raw = readFileSync26(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
44427
44611
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
44428
44612
  }
44429
44613
  const args = (await runHidden("ps", ["-p", String(pid), "-o", "args="], { timeout: 15000 })).stdout.trim();
@@ -44523,26 +44707,20 @@ function whyNotTheRun(record, running, needed) {
44523
44707
  if (running.kind === "none") {
44524
44708
  return `the operating system would not describe that process (${running.unread ?? "no reason given"})`;
44525
44709
  }
44526
- const after = running.startedAt === undefined ? 0 : running.startedAt - (record.startedBy ?? record.startedAt);
44710
+ const after = running.startedAt === undefined ? 0 : running.startedAt - record.startedBy;
44527
44711
  if (after > SAME_RUN_WINDOW_MS) {
44528
44712
  return `that process started ${Math.round(after / 1000)} seconds after the run had`;
44529
44713
  }
44530
- const engine = record.command === undefined ? null : basename9(record.command);
44714
+ const engine = basename9(record.command);
44531
44715
  const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
44532
- const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
44533
- if (wanted !== null && !said.includes(wanted)) {
44534
- return `that process is not ${engine ?? "the engine"}, which the run was started with`;
44716
+ const wanted = process.platform === "win32" ? engine.toLowerCase() : engine;
44717
+ if (!said.includes(wanted)) {
44718
+ return `that process is not ${engine}, which the run was started with`;
44535
44719
  }
44536
44720
  if (running.kind === "commandLine" && readCommandLine(running.text).editor) {
44537
44721
  return "that process is an editor";
44538
44722
  }
44539
- if (record.projectPath === "") {
44540
- return wanted !== null ? null : "the record names neither the project nor the engine";
44541
- }
44542
44723
  if (running.kind === "image") {
44543
- if (wanted === null) {
44544
- return "the operating system named only its executable, and the record names no engine";
44545
- }
44546
44724
  return needed === "possible" ? null : `the operating system named only its executable and not its command line (${running.unread ?? "no reason given"}), which is not enough to signal it on`;
44547
44725
  }
44548
44726
  const project = process.platform === "win32" ? record.projectPath.toLowerCase() : record.projectPath;
@@ -44566,19 +44744,14 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
44566
44744
  }
44567
44745
  function clearRunRecord(projectPath) {
44568
44746
  for (const directory of runtimeDirectories()) {
44569
- const shared = join31(directory, "runs", SHARED_NOTE);
44570
- const sharedRecord = recordAt(shared);
44571
- const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
44572
- for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
44573
- try {
44574
- rmSync14(path, { force: true });
44575
- } catch {}
44576
- }
44747
+ try {
44748
+ rmSync14(runRecordPath(projectPath, directory), { force: true });
44749
+ } catch {}
44577
44750
  }
44578
44751
  }
44579
44752
  function ageMs(path, now) {
44580
44753
  try {
44581
- return now - statSync13(path).mtimeMs;
44754
+ return now - statSync14(path).mtimeMs;
44582
44755
  } catch {
44583
44756
  return 0;
44584
44757
  }
@@ -44592,7 +44765,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
44592
44765
  ]));
44593
44766
  let entries;
44594
44767
  try {
44595
- entries = readdirSync13(directory);
44768
+ entries = readdirSync14(directory);
44596
44769
  } catch {
44597
44770
  return;
44598
44771
  }
@@ -44601,7 +44774,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
44601
44774
  if (!transcript && !entry.endsWith(".partial")) {
44602
44775
  continue;
44603
44776
  }
44604
- const path = join31(directory, entry);
44777
+ const path = join32(directory, entry);
44605
44778
  if (kept.has(path) || ageMs(path, now) < keepMs) {
44606
44779
  continue;
44607
44780
  }
@@ -44610,7 +44783,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
44610
44783
  } catch {}
44611
44784
  }
44612
44785
  }
44613
- var SHARED_NOTE = "run.json", SAME_RUN_WINDOW_MS = 1e4;
44786
+ var SAME_RUN_WINDOW_MS = 1e4;
44614
44787
  var init_run_record = __esm(() => {
44615
44788
  init_errors();
44616
44789
  init_paths();
@@ -44622,10 +44795,10 @@ var init_run_record = __esm(() => {
44622
44795
  });
44623
44796
 
44624
44797
  // src/script-profile.ts
44625
- import { readFileSync as readFileSync26 } from "node:fs";
44798
+ import { readFileSync as readFileSync27 } from "node:fs";
44626
44799
  function readProfileFile(path) {
44627
44800
  try {
44628
- const parsed = JSON.parse(readFileSync26(path, "utf8"));
44801
+ const parsed = JSON.parse(readFileSync27(path, "utf8"));
44629
44802
  return typeof parsed === "object" && typeof parsed.connected === "boolean" ? parsed : null;
44630
44803
  } catch {
44631
44804
  return null;
@@ -44682,7 +44855,7 @@ function writeProfileMark(path, mark) {
44682
44855
  }
44683
44856
  function readProfileMark(path) {
44684
44857
  try {
44685
- const parsed = JSON.parse(readFileSync26(path, "utf8"));
44858
+ const parsed = JSON.parse(readFileSync27(path, "utf8"));
44686
44859
  return typeof parsed === "object" && typeof parsed.totals === "object" ? parsed : null;
44687
44860
  } catch {
44688
44861
  return null;
@@ -44855,18 +45028,18 @@ import {
44855
45028
  closeSync as closeSync3,
44856
45029
  constants,
44857
45030
  copyFileSync,
44858
- existsSync as existsSync25,
45031
+ existsSync as existsSync26,
44859
45032
  fstatSync,
44860
45033
  mkdirSync as mkdirSync17,
44861
45034
  openSync as openSync4,
44862
- readdirSync as readdirSync14,
44863
- readFileSync as readFileSync27,
45035
+ readdirSync as readdirSync15,
45036
+ readFileSync as readFileSync28,
44864
45037
  readSync as readSync2,
44865
45038
  rmdirSync as rmdirSync2,
44866
- statSync as statSync14,
45039
+ statSync as statSync15,
44867
45040
  writeFileSync as writeFileSync18
44868
45041
  } from "node:fs";
44869
- import { basename as basename10, dirname as dirname23, extname as extname4, isAbsolute as isAbsolute5, join as join32, normalize as normalize2, relative as relative6 } from "node:path";
45042
+ import { basename as basename10, dirname as dirname23, extname as extname4, isAbsolute as isAbsolute5, join as join33, normalize as normalize2, relative as relative6 } from "node:path";
44870
45043
  import { setTimeout as delay5 } from "node:timers/promises";
44871
45044
  import { fileURLToPath as fileURLToPath5 } from "node:url";
44872
45045
  function editorRestartTimeoutMs() {
@@ -44903,15 +45076,15 @@ function writtenFileRefusal(path, rule) {
44903
45076
  return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
44904
45077
  }
44905
45078
  const directory = realPathOr(dirname23(path));
44906
- if (!existsSync25(directory)) {
45079
+ if (!existsSync26(directory)) {
44907
45080
  return `${named}'s directory ${dirname23(path)} does not exist; ${what} creates no directories.`;
44908
45081
  }
44909
- const where = join32(directory, basename10(path));
45082
+ const where = join33(directory, basename10(path));
44910
45083
  const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
44911
45084
  if (inside !== undefined) {
44912
45085
  return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
44913
45086
  }
44914
- const taken = rule.writes(where).find((file) => existsSync25(file));
45087
+ const taken = rule.writes(where).find((file) => existsSync26(file));
44915
45088
  if (taken !== undefined) {
44916
45089
  return taken === where ? `${named} ${path} already exists; ${what} is not written over a file. Name a new one.` : `${named} ${path} would write ${taken}, which already exists; ${what} is not written over a file. Name a new one.`;
44917
45090
  }
@@ -45102,7 +45275,7 @@ async function closeLikeAPlayer(announced, alsoGoing, projectPath) {
45102
45275
  }
45103
45276
  const asked = await runtimeRequest(endpoint, "close_game", {}, CLOSE_ASK_MS);
45104
45277
  if (!asked.ok) {
45105
- return asked.reason === "error" ? `its runtime did not take the close (${asked.message}), as an addon older than this server's does not` : `its runtime did not answer the close within ${CLOSE_ASK_MS / 1000} seconds, as a game held at a breakpoint or stuck in a frame does not`;
45278
+ return asked.reason === "error" ? `its runtime refused the close (${asked.message})` : `its runtime did not answer the close within ${CLOSE_ASK_MS / 1000} seconds, as a game held at a breakpoint or stuck in a frame does not`;
45106
45279
  }
45107
45280
  if (await untilGone([announced, ...alsoGoing], closeWaitMs())) {
45108
45281
  return null;
@@ -45424,6 +45597,18 @@ function outsideTheirRanges(spec, args) {
45424
45597
  function scanWaitMs() {
45425
45598
  return waitFromEnv("GDHARNESS_SCAN_WAIT_MS", 30000);
45426
45599
  }
45600
+ function importedFirstAnswer(importedFirst) {
45601
+ if (importedFirst === undefined) {
45602
+ return {};
45603
+ }
45604
+ return {
45605
+ ...importedFirst.imported.length === 0 ? {} : { importedFirst: importedFirst.imported },
45606
+ ...importedFirst.notImported.length === 0 ? {} : { notImportedFirst: importedFirst.notImported },
45607
+ ...importedFirst.uidsWritten.length === 0 ? {} : { uidsWrittenFirst: importedFirst.uidsWritten },
45608
+ importTookMs: importedFirst.tookMs,
45609
+ ...importedFirst.note === undefined ? {} : { importNote: importedFirst.note }
45610
+ };
45611
+ }
45427
45612
  function scanWaitIn(payload) {
45428
45613
  const waited = readNumber(payload, "waitedForEditorScanMs");
45429
45614
  const note = readString(payload, "scanNote");
@@ -45452,7 +45637,7 @@ function notRunNote(notRun, failFast) {
45452
45637
  function exportPresetRefusal(projectPath, preset) {
45453
45638
  let text;
45454
45639
  try {
45455
- text = readFileSync27(join32(projectPath, "export_presets.cfg"), "utf8");
45640
+ text = readFileSync28(join33(projectPath, "export_presets.cfg"), "utf8");
45456
45641
  } catch {
45457
45642
  return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. project_export add_preset writes one, as the editor's Project > Export does.`;
45458
45643
  }
@@ -45516,20 +45701,22 @@ function writtenBeside(before, after, output, within) {
45516
45701
  function filesIn(folder) {
45517
45702
  const found = new Map;
45518
45703
  try {
45519
- for (const entry of readdirSync14(folder, { withFileTypes: true })) {
45704
+ for (const entry of readdirSync15(folder, { withFileTypes: true })) {
45520
45705
  if (entry.isFile()) {
45521
- found.set(entry.name, statSync14(join32(folder, entry.name)).mtimeMs);
45706
+ found.set(entry.name, statSync15(join33(folder, entry.name)).mtimeMs);
45522
45707
  }
45523
45708
  }
45524
45709
  } catch {}
45525
45710
  return found;
45526
45711
  }
45527
45712
  function scanWaitAnswer(scanned, started = "game") {
45713
+ const imports = importedFirstAnswer(scanned.importedFirst);
45528
45714
  if (scanned.waitedMs === 0) {
45529
- return {};
45715
+ return imports;
45530
45716
  }
45531
45717
  const still = `The editor was still scanning the project after ${scanWaitMs() / 1000} seconds, so`;
45532
45718
  return {
45719
+ ...imports,
45533
45720
  waitedForEditorScanMs: scanned.waitedMs,
45534
45721
  scanNote: !scanned.stillScanning ? undefined : started === "game" ? `${still} this game started during the scan and may have read a class cache being rewritten. If ` + 'editor_output shows "Could not find type" for a class that is declared, editor_rescan ' + "waits for the scan to finish, and a start after it reads the finished cache." : `${still} the engine answering this started during the scan and may have read a class cache being ` + "rewritten, and a class cache it wrote can be written over when the scan finishes. If " + 'engine_messages shows "Could not find type" for a class that is declared, editor_rescan waits ' + "for the scan to finish, and the same call after it reads the finished cache."
45535
45722
  };
@@ -45548,7 +45735,7 @@ function unsavedInEditorNote(scripts) {
45548
45735
  return `${named} ${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.`;
45549
45736
  }
45550
45737
  function editorAddonMissing(projectPath) {
45551
- if (!existsSync25(join32(projectPath, "project.godot"))) {
45738
+ if (!existsSync26(join33(projectPath, "project.godot"))) {
45552
45739
  return null;
45553
45740
  }
45554
45741
  const report = inspectProject(projectPath);
@@ -45637,7 +45824,7 @@ function withUidsMissing(answer, missing) {
45637
45824
  last.text = answerJson({
45638
45825
  ...parsed,
45639
45826
  uidsMissing: missing,
45640
- uidsNote: `${missing.length === 1 ? "This script has" : "These scripts have"} no .uid sidecar yet: the engine writes one when it imports the project, which a test run does not, so a check that tracks the sidecars fails on ${missing.length === 1 ? "it" : "them"}. project_import refresh_uids writes them.`
45827
+ uidsNote: `${missing.length === 1 ? "This script has" : "These scripts have"} no .uid sidecar yet: the engine writes one when it imports the project, and this run had nothing to import, so a check that tracks the sidecars fails on ${missing.length === 1 ? "it" : "them"}. project_import refresh_uids writes them.`
45641
45828
  });
45642
45829
  }
45643
45830
  } catch {}
@@ -45813,7 +46000,7 @@ function profileFrameFunctions(projectPath) {
45813
46000
  let projectGodot = null;
45814
46001
  if (projectPath !== null) {
45815
46002
  try {
45816
- projectGodot = readFileSync27(join32(projectPath, "project.godot"), "utf8");
46003
+ projectGodot = readFileSync28(join33(projectPath, "project.godot"), "utf8");
45817
46004
  } catch {}
45818
46005
  }
45819
46006
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -45837,7 +46024,7 @@ function projectSourceOf(projectPath) {
45837
46024
  let source = null;
45838
46025
  if (inside.ok) {
45839
46026
  try {
45840
- source = readFileSync27(inside.absolutePath, "utf8");
46027
+ source = readFileSync28(inside.absolutePath, "utf8");
45841
46028
  } catch {}
45842
46029
  }
45843
46030
  read.set(script, source);
@@ -45992,7 +46179,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
45992
46179
  }
45993
46180
  function gdUnitFailsOnScriptErrors(projectPath) {
45994
46181
  try {
45995
- return settingKeys(readFileSync27(join32(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
46182
+ return settingKeys(readFileSync28(join33(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
45996
46183
  } catch {
45997
46184
  return true;
45998
46185
  }
@@ -46007,13 +46194,13 @@ function uncaughtNote(uncaught) {
46007
46194
  }
46008
46195
  function elsewhereIn(projectPath, asked) {
46009
46196
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
46010
- if (wanted === "" || existsSync25(join32(projectPath, wanted))) {
46197
+ if (wanted === "" || existsSync26(join33(projectPath, wanted))) {
46011
46198
  return "";
46012
46199
  }
46013
46200
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
46014
46201
  let near = [];
46015
46202
  try {
46016
- near = readdirSync14(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
46203
+ near = readdirSync15(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
46017
46204
  } catch {
46018
46205
  return "";
46019
46206
  }
@@ -46104,23 +46291,23 @@ function engineChecked(why) {
46104
46291
  }
46105
46292
  function isDirectory(path) {
46106
46293
  try {
46107
- return statSync14(path).isDirectory();
46294
+ return statSync15(path).isDirectory();
46108
46295
  } catch {
46109
46296
  return false;
46110
46297
  }
46111
46298
  }
46112
46299
  function scriptsUnder(directory, prefix) {
46113
- if (existsSync25(join32(directory, ".gdignore"))) {
46300
+ if (existsSync26(join33(directory, ".gdignore"))) {
46114
46301
  return [];
46115
46302
  }
46116
46303
  const found = [];
46117
- for (const entry of readdirSync14(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
46304
+ for (const entry of readdirSync15(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
46118
46305
  if (entry.name.startsWith(".")) {
46119
46306
  continue;
46120
46307
  }
46121
46308
  const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
46122
46309
  if (entry.isDirectory()) {
46123
- found.push(...scriptsUnder(join32(directory, entry.name), path));
46310
+ found.push(...scriptsUnder(join33(directory, entry.name), path));
46124
46311
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
46125
46312
  found.push(path);
46126
46313
  }
@@ -46128,29 +46315,18 @@ function scriptsUnder(directory, prefix) {
46128
46315
  return found;
46129
46316
  }
46130
46317
  function mainSceneOf(projectFile) {
46131
- const scene = parseProjectGodot(readFileSync27(projectFile, "utf8"))["application"]?.["run/main_scene"];
46318
+ const scene = parseProjectGodot(readFileSync28(projectFile, "utf8"))["application"]?.["run/main_scene"];
46132
46319
  return typeof scene === "string" && scene !== "" ? scene : null;
46133
46320
  }
46134
- function runtimeAddonAnnouncesEditors(projectPath) {
46135
- if (projectPath === null) {
46136
- return false;
46137
- }
46138
- try {
46139
- return readFileSync27(join32(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
46140
- } catch {
46141
- return false;
46142
- }
46143
- }
46144
46321
  function editorPlaysHeadless(projectFile) {
46145
- const runArgs = parseProjectGodot(readFileSync27(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
46322
+ const runArgs = parseProjectGodot(readFileSync28(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
46146
46323
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
46147
46324
  }
46148
- function camelCased(params) {
46325
+ function ownArguments(params) {
46149
46326
  const result = emptyRecord();
46150
46327
  const source = asParams(params);
46151
46328
  for (const key of Object.keys(source)) {
46152
- const name = key.startsWith("_") ? key : key.replace(/_([a-zA-Z0-9])/g, (_, letter) => letter.toUpperCase());
46153
- result[name] = source[key];
46329
+ result[key] = source[key];
46154
46330
  }
46155
46331
  return result;
46156
46332
  }
@@ -46161,7 +46337,7 @@ function needsItsOwnProcess(asked) {
46161
46337
  class GodotServer {
46162
46338
  stdio = null;
46163
46339
  locator = new GodotLocator;
46164
- operationsScript = join32(__dirname2, "godot", "operations", "godot_operations.gd");
46340
+ operationsScript = join33(__dirname2, "godot", "operations", "godot_operations.gd");
46165
46341
  godotBridge;
46166
46342
  tools = buildToolDefinitions();
46167
46343
  activeProcess = null;
@@ -46236,6 +46412,7 @@ class GodotServer {
46236
46412
  console.error(`[SERVER] Using Godot at: ${located.path}`);
46237
46413
  }
46238
46414
  this.stdio = serveStdio(() => this.protocol(), {
46415
+ legacy: "reject",
46239
46416
  onerror: (error) => {
46240
46417
  console.error("[MCP Error]", error);
46241
46418
  }
@@ -46422,14 +46599,14 @@ class GodotServer {
46422
46599
  }
46423
46600
  noteTheStart() {
46424
46601
  const cwd = process.cwd();
46425
- const mcpJson = join32(cwd, ".mcp.json");
46602
+ const mcpJson = join33(cwd, ".mcp.json");
46426
46603
  const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
46427
46604
  const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
46428
46605
  writeLifecycle("started", {
46429
46606
  argv: process.argv.slice(1),
46430
46607
  cwd,
46431
46608
  project: this.ownProject,
46432
- mcpJson: existsSync25(mcpJson) ? mcpJson : null
46609
+ mcpJson: existsSync26(mcpJson) ? mcpJson : null
46433
46610
  });
46434
46611
  if (predecessor !== null) {
46435
46612
  writeLifecycle("replaced", { project: this.ownProject, predecessor });
@@ -46551,7 +46728,7 @@ class GodotServer {
46551
46728
  return { name, version };
46552
46729
  }
46553
46730
  }
46554
- return protocol.getClientVersion();
46731
+ return;
46555
46732
  };
46556
46733
  protocol.setRequestHandler("tools/list", (_request, ctx) => {
46557
46734
  this.noteTheClient(clientOf(ctx.mcpReq.envelope));
@@ -46564,13 +46741,13 @@ class GodotServer {
46564
46741
  if (!spec) {
46565
46742
  throw new ProtocolError(ProtocolErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}. Tools: ${TOOL_SPECS.map((tool) => tool.name).join(", ")}`);
46566
46743
  }
46567
- const args = camelCased(request.params.arguments);
46744
+ const args = ownArguments(request.params.arguments);
46568
46745
  const checked = this.validateArguments(spec, args);
46569
46746
  if (!checked.ok) {
46570
46747
  return checked.response;
46571
46748
  }
46572
46749
  const named = args["projectPath"];
46573
- if (typeof named === "string" && existsSync25(join32(named, "project.godot"))) {
46750
+ if (typeof named === "string" && existsSync26(join33(named, "project.godot"))) {
46574
46751
  this.lastProjectPath = named;
46575
46752
  }
46576
46753
  this.updates.refresh();
@@ -46606,7 +46783,7 @@ class GodotServer {
46606
46783
  let notSaved;
46607
46784
  try {
46608
46785
  savedAt = answer.content.map((item, index) => {
46609
- const file = join32(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
46786
+ const file = join33(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
46610
46787
  writeFileSync18(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
46611
46788
  return file;
46612
46789
  });
@@ -46740,11 +46917,6 @@ class GodotServer {
46740
46917
  const installed = installedAddonVersion(this.ownProject);
46741
46918
  return installed === null || installed === SERVER_VERSION ? null : installed;
46742
46919
  }
46743
- editorCodeOnDisk() {
46744
- const status = this.godotBridge.getStatus();
46745
- const project = (status.connected ? status.projectPath : undefined) ?? this.ownProject;
46746
- return project === null || project === "" ? undefined : installedEditorDigest(project);
46747
- }
46748
46920
  runtimeStaleNote(endpoint) {
46749
46921
  const project = endpoint.project.path;
46750
46922
  return runtimeMismatch(endpoint.pid, endpoint.addon, SERVER_VERSION, shippedRuntimeDigest(), project === "" ? undefined : installedRuntimeDigest(project), project);
@@ -46779,9 +46951,13 @@ class GodotServer {
46779
46951
  });
46780
46952
  const takes = wanted.length === 0 ? "It takes no arguments." : `It takes: ${shaped.join(", ")}.`;
46781
46953
  const where = named === null ? spec.name : `${spec.name} ${named}`;
46954
+ const spelled = unknown.flatMap((key) => {
46955
+ const camel = key.replace(/_([a-zA-Z0-9])/g, (_, letter) => letter.toUpperCase());
46956
+ return camel !== key && known.has(camel) ? [`${key} is spelled ${camel}`] : [];
46957
+ });
46782
46958
  return {
46783
46959
  ok: false,
46784
- response: this.createErrorResponse(`${where} does not take ${unknown.join(", ")}. ${takes} This is gdharness ${SERVER_VERSION}, which is what decides whether an argument exists.`)
46960
+ response: this.createErrorResponse(`${where} does not take ${unknown.join(", ")}.${spelled.length === 0 ? "" : ` ${spelled.join(", ")}.`} ${takes} This is gdharness ${SERVER_VERSION}, which is what decides whether an argument exists.`)
46785
46961
  };
46786
46962
  }
46787
46963
  const wrong = [
@@ -46898,7 +47074,7 @@ class GodotServer {
46898
47074
  const contained = this.containProjectFiles({ ...arguments_, ...extra });
46899
47075
  return contained.ok ? await this.handleViaBridge(command, contained.value, op === "" ? tool : `${tool} ${op}`) : contained.response;
46900
47076
  };
46901
- const editorOrEngine = async (command, effect, extra = {}) => await this.editorOrEngine(command, { ...arguments_, ...extra }, `${tool} ${op}`, effect);
47077
+ const editorOrEngine = async (command, effect) => await this.editorOrEngine(command, arguments_, `${tool} ${op}`, effect);
46902
47078
  switch (tool) {
46903
47079
  case "project_info":
46904
47080
  return await this.handleProjectInfo(args);
@@ -46927,13 +47103,13 @@ class GodotServer {
46927
47103
  case "set":
46928
47104
  return await editorOrEngine("set_node_properties", "writes");
46929
47105
  case "duplicate":
46930
- return await editorOrEngine("duplicate_node", "writes", { parentPath: args["parentNodePath"] });
47106
+ return await editorOrEngine("duplicate_node", "writes");
46931
47107
  case "reparent":
46932
47108
  return await editorOrEngine("reparent_node", "writes");
46933
47109
  case "delete":
46934
47110
  return await editorOrEngine("delete_node", "writes");
46935
47111
  default:
46936
- return await editorOrEngine("set_tilemap_cells", "writes", { tilemapNodePath: args["nodePath"] });
47112
+ return await editorOrEngine("set_tilemap_cells", "writes");
46937
47113
  }
46938
47114
  case "scene_signal":
46939
47115
  switch (op) {
@@ -46997,13 +47173,13 @@ class GodotServer {
46997
47173
  case "modify":
46998
47174
  return await editorOrEngine("modify_resource", "writes");
46999
47175
  case "create_shader":
47000
- return await editorOrEngine("create_shader", "writes", { shaderPath: args["resourcePath"] });
47176
+ return await editorOrEngine("create_shader", "writes");
47001
47177
  case "create_tileset":
47002
- return await editorOrEngine("create_tileset", "writes", { tilesetPath: args["resourcePath"] });
47178
+ return await editorOrEngine("create_tileset", "writes");
47003
47179
  case "set_theme_color":
47004
- return await editorOrEngine("set_theme_color", "writes", { themePath: args["resourcePath"] });
47180
+ return await editorOrEngine("set_theme_color", "writes");
47005
47181
  default:
47006
- return await editorOrEngine("set_theme_font_size", "writes", { themePath: args["resourcePath"] });
47182
+ return await editorOrEngine("set_theme_font_size", "writes");
47007
47183
  }
47008
47184
  case "editor_launch":
47009
47185
  if (op === "close") {
@@ -47224,8 +47400,8 @@ class GodotServer {
47224
47400
  if (path === undefined) {
47225
47401
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
47226
47402
  }
47227
- const file = join32(path, "project.godot");
47228
- if (!existsSync25(file)) {
47403
+ const file = join33(path, "project.godot");
47404
+ if (!existsSync26(file)) {
47229
47405
  return {
47230
47406
  ok: false,
47231
47407
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -47396,6 +47572,42 @@ class GodotServer {
47396
47572
  }
47397
47573
  return { ok: true, value: stale };
47398
47574
  }
47575
+ async importWhatChanged(projectPath) {
47576
+ if (staleImports(projectPath).length === 0) {
47577
+ return;
47578
+ }
47579
+ const unminted = scriptsWithoutUid(projectPath);
47580
+ const began = Date.now();
47581
+ const done = await this.reimport(projectPath, undefined, false, scanWaitMs());
47582
+ const tookMs = Date.now() - began;
47583
+ const stillUnminted = scriptsWithoutUid(projectPath);
47584
+ const uidsWritten = unminted.filter((script) => !stillUnminted.includes(script)).map((script) => `res://${script}`);
47585
+ if (!done.ok) {
47586
+ const why = done.response.content.map((block) => block.text).join(" ");
47587
+ return {
47588
+ imported: [],
47589
+ notImported: [],
47590
+ uidsWritten,
47591
+ tookMs,
47592
+ note: `Files changed since they were imported could not be imported before this run, so it reads their previous import: ${why}`
47593
+ };
47594
+ }
47595
+ const imported = (readArray(done.payload, "reimported") ?? []).map(String);
47596
+ const notImported = readArray(done.payload, "notReimported") ?? [];
47597
+ const note = done.payload["stillImporting"] === true ? `The editor was still importing files changed since they were imported when the wait ran out after ${scanWaitMs() / 1000} seconds, so this run may read their previous import. project_import status says where each stands.` : notImported.length > 0 ? "The files under notImportedFirst are still on their previous import, so this run shows what they held before. project_import status says why." : undefined;
47598
+ return { imported, notImported, uidsWritten, tookMs, ...note === undefined ? {} : { note } };
47599
+ }
47600
+ async afterTheImport(projectPath, waited, importedFirst) {
47601
+ if (importedFirst === undefined) {
47602
+ return waited;
47603
+ }
47604
+ const settled = await this.untilTheEditorsScanIsWritten(projectPath);
47605
+ return {
47606
+ waitedMs: waited.waitedMs + settled.waitedMs,
47607
+ stillScanning: settled.stillScanning,
47608
+ importedFirst
47609
+ };
47610
+ }
47399
47611
  async untilTheEditorsScanIsWritten(projectPath) {
47400
47612
  const open = this.godotBridge.getStatus().projectPath;
47401
47613
  if (!this.godotBridge.isConnected() || open === undefined || !isSameDirectory(open, projectPath)) {
@@ -47606,9 +47818,8 @@ class GodotServer {
47606
47818
  return {};
47607
47819
  } catch (error) {
47608
47820
  const message = errorMessage(error);
47609
- const why = message.includes("Unknown tool") ? "runs an addon from before writes were handed to it" : `could not take the change (${message})`;
47610
47821
  return {
47611
- editorNote: `The open editor ${why}, so it still holds the old values, answers them when asked, and writes them back over the file the next time it saves. editor_launch restart loads the file and the addon this server ships.`
47822
+ editorNote: error instanceof AddonOfAnotherRelease ? `The open editor was not asked to take the change, so it still holds the old values, answers them when asked, and writes them back over the file the next time it saves. ${message}` : `The open editor could not take the change (${message}), so it still holds the old values, answers them when asked, and writes them back over the file the next time it saves. editor_launch restart loads the file again.`
47612
47823
  };
47613
47824
  }
47614
47825
  }
@@ -47821,8 +48032,7 @@ class GodotServer {
47821
48032
  const message = errorMessage(error);
47822
48033
  return {
47823
48034
  ok: false,
47824
- response: message.includes("Unknown tool") ? this.createErrorResponse("The editor is running an addon from before reimport_files, so it cannot be asked to reimport.", [
47825
- "editor_launch restart loads the addon this server ships",
48035
+ response: error instanceof AddonOfAnotherRelease ? this.createErrorResponse(message, [
47826
48036
  "Or close the editor, and project_import reimport runs the engine import instead"
47827
48037
  ]) : this.createErrorResponse(`The editor answered reimport_files with an error: ${message}`)
47828
48038
  };
@@ -47859,7 +48069,7 @@ class GodotServer {
47859
48069
  }
47860
48070
  const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
47861
48071
  this.logDebug(`Running ${operation} in ${projectPath}: ${JSON.stringify(params)}`);
47862
- const outcome = await runOperation({ godotPath: engine.value, script: this.operationsScript, debug: GODOT_DEBUG_MODE_DEFAULT }, operation, params, projectPath);
48072
+ const outcome = await runOperation({ godotPath: engine.value, script: this.operationsScript, debug: DEBUG_MODE }, operation, params, projectPath);
47863
48073
  return withScanWait(outcome, scanned);
47864
48074
  }
47865
48075
  answer(outcome) {
@@ -47891,7 +48101,7 @@ class GodotServer {
47891
48101
  if (!engine.ok) {
47892
48102
  return engine.response;
47893
48103
  }
47894
- const application = parseProjectGodot(readFileSync27(project.value.file, "utf8"))["application"] ?? {};
48104
+ const application = parseProjectGodot(readFileSync28(project.value.file, "utf8"))["application"] ?? {};
47895
48105
  const name = application["config/name"];
47896
48106
  const mainScene = application["run/main_scene"];
47897
48107
  const info = {
@@ -47989,7 +48199,7 @@ class GodotServer {
47989
48199
  const exportArgs = [
47990
48200
  "--headless",
47991
48201
  "--log-file",
47992
- join32(exportLogs, "engine.log"),
48202
+ join33(exportLogs, "engine.log"),
47993
48203
  "--path",
47994
48204
  project.value.path,
47995
48205
  debug ? "--export-debug" : "--export-release",
@@ -47997,7 +48207,7 @@ class GodotServer {
47997
48207
  output.absolutePath
47998
48208
  ];
47999
48209
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
48000
- const writtenAt = () => existsSync25(output.absolutePath) ? statSync14(output.absolutePath).mtimeMs : null;
48210
+ const writtenAt = () => existsSync26(output.absolutePath) ? statSync15(output.absolutePath).mtimeMs : null;
48001
48211
  const caches = editorCacheDirectory(engine.value);
48002
48212
  const exportOnce = async () => {
48003
48213
  const before = writtenAt();
@@ -48080,10 +48290,10 @@ class GodotServer {
48080
48290
  if (movie !== null) {
48081
48291
  return this.createErrorResponse(movie);
48082
48292
  }
48083
- const missing = paths.value.filter((path) => !existsSync25(join32(project.value.path, path.slice("res://".length))));
48293
+ const missing = paths.value.filter((path) => !existsSync26(join33(project.value.path, path.slice("res://".length))));
48084
48294
  const asked = paths.value.filter((path) => !missing.includes(path));
48085
48295
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
48086
- const noRunner = !existsSync25(join32(project.value.path, runner));
48296
+ const noRunner = !existsSync26(join33(project.value.path, runner));
48087
48297
  if (asked.length === 0) {
48088
48298
  const verdict = `nothing at ${missing.join(" and ")}`;
48089
48299
  const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
@@ -48120,7 +48330,15 @@ class GodotServer {
48120
48330
  if (!classes.ok) {
48121
48331
  return this.answer(classes);
48122
48332
  }
48123
- const scanned = scanWaitIn(classes.payload);
48333
+ const rebuildWaited = scanWaitIn(classes.payload);
48334
+ const importWaited = scanWaitAnswer(await this.afterTheImport(project.value.path, { waitedMs: 0, stillScanning: false }, await this.importWhatChanged(project.value.path)));
48335
+ const waitedMs = (rebuildWaited.waitedForEditorScanMs ?? 0) + (importWaited.waitedForEditorScanMs ?? 0);
48336
+ const scanNote = importWaited.scanNote ?? rebuildWaited.scanNote;
48337
+ const scanned = {
48338
+ ...importWaited,
48339
+ ...waitedMs > 0 ? { waitedForEditorScanMs: waitedMs } : {},
48340
+ ...scanNote === undefined ? {} : { scanNote }
48341
+ };
48124
48342
  const ours = `gdharness-reports/${randomUUID2()}`;
48125
48343
  const reports = `res://.godot/${ours}`;
48126
48344
  const ignored = readStringArray(args, "ignore") ?? [];
@@ -48191,14 +48409,14 @@ class GodotServer {
48191
48409
  await run.settled;
48192
48410
  const tookMs = Date.now() - began;
48193
48411
  const notCompiled = stopped.for ?? run.log.all.map((entry) => autoloadNotCompiled(entry, autoloaded)).find((found) => found !== null) ?? null;
48194
- const reportsDir = join32(project.value.path, ".godot", ...ours.split("/"));
48412
+ const reportsDir = join33(project.value.path, ".godot", ...ours.split("/"));
48195
48413
  let report = null;
48196
48414
  let reportProblem = null;
48197
48415
  try {
48198
- const written = existsSync25(reportsDir) && notCompiled === null ? readdirSync14(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
48416
+ const written = existsSync26(reportsDir) && notCompiled === null ? readdirSync15(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
48199
48417
  const newest = written.at(-1);
48200
48418
  if (newest !== undefined) {
48201
- report = parseJUnit(readFileSync27(join32(reportsDir, newest, "results.xml"), "utf8"));
48419
+ report = parseJUnit(readFileSync28(join33(reportsDir, newest, "results.xml"), "utf8"));
48202
48420
  }
48203
48421
  } catch (error) {
48204
48422
  reportProblem = errorMessage(error);
@@ -48398,7 +48616,7 @@ class GodotServer {
48398
48616
  const checked = project.ok ? await this.engineDiagnostics(project.value.path, [readString(args, "scriptPath") ?? ""], one.noLanguageServer) : { refusal: project.response };
48399
48617
  return "refusal" in checked ? checked.refusal : this.jsonTextResponse({ ...checked.answers[0], ...engineChecked(one.noLanguageServer.reason) });
48400
48618
  }
48401
- return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
48619
+ return "refusal" in one ? one.refusal : this.jsonTextResponse(one.answer);
48402
48620
  }
48403
48621
  const progress = callProgress();
48404
48622
  const cancelled = callSignal();
@@ -48448,7 +48666,7 @@ class GodotServer {
48448
48666
  reported.push(one.answer);
48449
48667
  }
48450
48668
  }
48451
- return this.jsonTextResponse(this.markedIfStale({
48669
+ return this.jsonTextResponse({
48452
48670
  clean: errors === 0 && unread === 0,
48453
48671
  scriptsChecked: scripts.value.length,
48454
48672
  errors,
@@ -48457,7 +48675,7 @@ class GodotServer {
48457
48675
  scripts: reported,
48458
48676
  nothingReported,
48459
48677
  ...byEngine === null ? {} : engineChecked(byEngine)
48460
- }));
48678
+ });
48461
48679
  }
48462
48680
  async compileScripts(projectPath, paths) {
48463
48681
  const payload = { checked: [], failed: [], scripts: [] };
@@ -48514,13 +48732,6 @@ class GodotServer {
48514
48732
  })
48515
48733
  };
48516
48734
  }
48517
- markedIfStale(answer) {
48518
- const status = this.godotBridge.getStatus();
48519
- if (!hasSaidWhoItIs(status)) {
48520
- return answer;
48521
- }
48522
- return markIfStale(answer, status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
48523
- }
48524
48735
  async diagnosticsOf(args) {
48525
48736
  const waited = await this.waitForTheEditorOpenedHere(args, []);
48526
48737
  if (waited !== null) {
@@ -48566,11 +48777,11 @@ class GodotServer {
48566
48777
  const cached = cachedClasses(projectPath);
48567
48778
  const sourceOf = (resourcePath) => {
48568
48779
  const contained = resolveWithinProject(projectPath, resourcePath);
48569
- if (!contained.ok || !existsSync25(contained.absolutePath)) {
48780
+ if (!contained.ok || !existsSync26(contained.absolutePath)) {
48570
48781
  return null;
48571
48782
  }
48572
48783
  try {
48573
- return readFileSync27(contained.absolutePath, "utf8");
48784
+ return readFileSync28(contained.absolutePath, "utf8");
48574
48785
  } catch {
48575
48786
  return null;
48576
48787
  }
@@ -48660,11 +48871,11 @@ class GodotServer {
48660
48871
  }
48661
48872
  sourceOfClass(projectPath, resourcePath) {
48662
48873
  const contained = resolveWithinProject(projectPath, resourcePath);
48663
- if (!contained.ok || !existsSync25(contained.absolutePath)) {
48874
+ if (!contained.ok || !existsSync26(contained.absolutePath)) {
48664
48875
  return null;
48665
48876
  }
48666
48877
  try {
48667
- return readFileSync27(contained.absolutePath, "utf8");
48878
+ return readFileSync28(contained.absolutePath, "utf8");
48668
48879
  } catch {
48669
48880
  return null;
48670
48881
  }
@@ -48787,12 +48998,11 @@ class GodotServer {
48787
48998
  return client;
48788
48999
  }
48789
49000
  breakpointsAtRisk() {
48790
- const status = this.godotBridge.getStatus();
48791
- if (!hasSaidWhoItIs(status) || status.syncsBreakpoints === true) {
49001
+ if (this.godotBridge.getStatus().syncsBreakpoints !== false) {
48792
49002
  return {};
48793
49003
  }
48794
49004
  return {
48795
- breakpointsAtRisk: status.syncsBreakpoints === false ? "This editor clears every breakpoint in its script editor whenever a debug session opens on its adapter, which this server does for its first breakpoint or stack read: network/debug_adapter/sync_breakpoints is off. The installed addon turns it on as it loads, so the editor is running an older one; editor_launch restart puts the installed addon in, or turn the setting on in Editor Settings > Network > Debug Adapter." : "Whether this editor keeps its breakpoints when a debug session opens cannot be told: the addon it is running predates the report. Godot clears them unless network/debug_adapter/sync_breakpoints is on, which the installed addon turns on as it loads; editor_launch restart puts the installed addon in."
49005
+ breakpointsAtRisk: "This editor clears every breakpoint in its script editor whenever a debug session opens on its adapter, which this server does for its first breakpoint or stack read: network/debug_adapter/sync_breakpoints is off. The addon turns it on as it loads, so it was turned off since; turn it on in Editor Settings > Network > Debug Adapter, or editor_launch restart turns it on again."
48796
49006
  };
48797
49007
  }
48798
49008
  breakpointsForAGame(projectPath) {
@@ -48822,7 +49032,7 @@ class GodotServer {
48822
49032
  ...Array.isArray(stack) ? {
48823
49033
  stack: stack.map((frame) => ({
48824
49034
  ...frame,
48825
- ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join32(projectPath, frame["script"].slice("res://".length)) } : {}
49035
+ ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join33(projectPath, frame["script"].slice("res://".length)) } : {}
48826
49036
  }))
48827
49037
  } : {}
48828
49038
  });
@@ -48886,13 +49096,13 @@ class GodotServer {
48886
49096
  const arriving = status.connected ? [] : arrivingFound;
48887
49097
  const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
48888
49098
  const greeted = hasSaidWhoItIs(status);
48889
- const stale = greeted && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
49099
+ const staleNote = this.godotBridge.anotherRelease();
48890
49100
  const unfinished = this.restartLeftUnfinished(status.connected);
48891
49101
  const cannotConnect = status.connected || this.ownProject === null ? null : editorAddonMissing(this.ownProject);
48892
49102
  return {
48893
49103
  ...status,
48894
49104
  serverVersion: SERVER_VERSION,
48895
- addonIsStale: greeted ? stale : undefined,
49105
+ addonIsStale: greeted ? staleNote !== undefined : undefined,
48896
49106
  greeting: status.connected && !greeted ? "The editor has connected and has not yet said who it is: its project, pid, addon version and ports arrive with its greeting a moment after the socket opens, later while it imports. Ask again for them." : undefined,
48897
49107
  bridgeAvailable: this.bridgeStartupError === null && status.port !== null,
48898
49108
  mayYetConnect: status.connected ? undefined : cannotConnect !== null ? false : arriving.length > 0 ? true : unfinished !== null ? unfinished.underway : this.closedHere !== null ? false : anEditorIsStillComing(status.listeningSince, launchedIsUp),
@@ -48926,8 +49136,8 @@ class GodotServer {
48926
49136
  port: this.godotBridge.configuredPort,
48927
49137
  note: `The gdharness server this one replaced, pid ${this.handingOverFrom}, still holds port ${this.godotBridge.configuredPort} and is standing down; this server takes the port when it lets go, and takes another after ${HANDOVER_MS / 1000} seconds if it does not.`
48928
49138
  },
48929
- staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()) : undefined,
48930
- addonNote: greeted ? sameCodeNote(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
49139
+ staleNote,
49140
+ addonNote: greeted && staleNote === undefined ? sameCodeNote(status.addonVersion ?? "", SERVER_VERSION) : undefined,
48931
49141
  startedByAnEditorNote: status.startedByAnEditor === true ? "Another editor started this one: Godot's own restart, or the project manager opening a project. None of the arguments a server gave the editor before it reached this one, so it serves the ports its editor settings name (and none at all while another editor holds those, when the script and debug tools reach that editor instead), its console is not captured, and it restarts itself. Close it and use editor_launch open to have an editor this server started." : undefined,
48932
49142
  ...this.breakpointsAtRisk(),
48933
49143
  retryingBridge: this.bridgeRetry === null ? undefined : true,
@@ -49012,7 +49222,7 @@ class GodotServer {
49012
49222
  project: endpoint.project,
49013
49223
  ...this.ownProject === null ? {} : { ofThisProject: isSameDirectory(endpoint.project.path, this.ownProject) },
49014
49224
  editorPid: endpoint.editorPid,
49015
- addonVersion: endpoint.addon === undefined || endpoint.addon.version === "" ? null : endpoint.addon.version,
49225
+ addonVersion: endpoint.addon.version === "" ? null : endpoint.addon.version,
49016
49226
  ...unknown ? {} : { addonIsStale: staleNote !== undefined },
49017
49227
  ...staleNote === undefined ? {} : { staleNote },
49018
49228
  reachable: reply.ok,
@@ -49101,7 +49311,7 @@ class GodotServer {
49101
49311
  return null;
49102
49312
  }
49103
49313
  try {
49104
- return settingKeys(readFileSync27(join32(projectPath, "project.godot"), "utf8"));
49314
+ return settingKeys(readFileSync28(join33(projectPath, "project.godot"), "utf8"));
49105
49315
  } catch {
49106
49316
  return null;
49107
49317
  }
@@ -49125,7 +49335,7 @@ class GodotServer {
49125
49335
  ]);
49126
49336
  }
49127
49337
  const settingsBefore = this.settingKeysOf(before.projectPath);
49128
- const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, args["hidden"] === true) : await this.handleViaBridge("restart_editor", {});
49338
+ const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, args["hidden"] === true, resolveSilent(args["silent"])) : await this.handleViaBridge("restart_editor", {});
49129
49339
  if (asked.isError === true) {
49130
49340
  return asked;
49131
49341
  }
@@ -49147,10 +49357,11 @@ class GodotServer {
49147
49357
  editorPid: now.editorPid,
49148
49358
  addonVersion: now.addonVersion,
49149
49359
  serverVersion: SERVER_VERSION,
49150
- addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
49151
- staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()),
49360
+ addonIsStale: this.godotBridge.anotherRelease() !== undefined,
49361
+ staleNote: this.godotBridge.anotherRelease(),
49152
49362
  ...this.whatTheEditorDropped(settingsBefore, after),
49153
49363
  ...servers,
49364
+ ...mine && this.launchedEditor !== null ? { arguments: this.launchedEditor.arguments } : {},
49154
49365
  tookMs: Date.now() - began
49155
49366
  });
49156
49367
  }
@@ -49239,7 +49450,7 @@ class GodotServer {
49239
49450
  whatTheEditorDropped(before, after) {
49240
49451
  return settingsDroppedReport(before, after);
49241
49452
  }
49242
- async startItAgain(projectPath, editorPid, hidden) {
49453
+ async startItAgain(projectPath, editorPid, hidden, silent) {
49243
49454
  const engine = await this.engine();
49244
49455
  if (!engine.ok) {
49245
49456
  return engine.response;
@@ -49268,7 +49479,7 @@ class GodotServer {
49268
49479
  `editor_launch open starts one on ${projectPath} once it has gone`
49269
49480
  ]);
49270
49481
  }
49271
- const opened = await this.openAnEditor(engine.value, projectPath, ports, hidden);
49482
+ const opened = await this.openAnEditor(engine.value, projectPath, ports, hidden, silent);
49272
49483
  restartSettled(projectPath);
49273
49484
  if (opened.error !== null) {
49274
49485
  return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
@@ -49318,7 +49529,7 @@ class GodotServer {
49318
49529
  const ports = await this.portsForAnEditor();
49319
49530
  const before = this.settingKeysOf(project.value.path);
49320
49531
  const unfinished = restartOwed(project.value.path);
49321
- const opened = await this.openAnEditor(engine.value, project.value.path, ports, args["hidden"] === true);
49532
+ const opened = await this.openAnEditor(engine.value, project.value.path, ports, args["hidden"] === true, resolveSilent(args["silent"]));
49322
49533
  if (opened.error !== null) {
49323
49534
  return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
49324
49535
  }
@@ -49330,6 +49541,7 @@ class GodotServer {
49330
49541
  projectPath: project.value.path,
49331
49542
  lspPort: ports.lsp,
49332
49543
  dapPort: ports.dap,
49544
+ arguments: opened.arguments,
49333
49545
  finishesRestart: unfinished === null ? undefined : `This is the launch half of a restart begun at ${unfinished.quitAt} by server pid ${unfinished.byPid}, which quit editor pid ${unfinished.editorPid ?? "unknown"} and was ended before it could start one.`,
49334
49546
  settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
49335
49547
  });
@@ -49364,12 +49576,13 @@ class GodotServer {
49364
49576
  }
49365
49577
  return null;
49366
49578
  }
49367
- async openAnEditor(engine, projectPath, ports, hidden) {
49579
+ async openAnEditor(engine, projectPath, ports, hidden, silent) {
49368
49580
  clearEditorLog(projectPath);
49369
49581
  freeLibraryCopies(projectPath);
49582
+ const args = editorArguments(projectPath, ports, editorLogPath(projectPath), hidden, silent);
49370
49583
  const launched = await launchOutsideTheTree({
49371
49584
  command: engine,
49372
- args: editorArguments(projectPath, ports, editorLogPath(projectPath), hidden),
49585
+ args,
49373
49586
  env: {
49374
49587
  ...process.env,
49375
49588
  GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
@@ -49380,16 +49593,17 @@ class GodotServer {
49380
49593
  ...hidden ? { desktop: HIDDEN_DESKTOP } : { noActivate: true }
49381
49594
  });
49382
49595
  if ("error" in launched) {
49383
- return { pid: null, error: launched.error };
49596
+ return { pid: null, error: launched.error, arguments: args };
49384
49597
  }
49385
49598
  writeEditorLogNote(projectPath, launched.pid);
49386
49599
  this.launchedEditor = {
49387
49600
  pid: launched.pid,
49388
49601
  startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null,
49389
- projectPath
49602
+ projectPath,
49603
+ arguments: args
49390
49604
  };
49391
49605
  this.closedHere = null;
49392
- return { pid: launched.pid, error: null };
49606
+ return { pid: launched.pid, error: null, arguments: args };
49393
49607
  }
49394
49608
  async portsForAnEditor() {
49395
49609
  const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
@@ -49454,11 +49668,13 @@ class GodotServer {
49454
49668
  if (!engine.ok) {
49455
49669
  return engine.response;
49456
49670
  }
49457
- const scanned = await this.untilTheEditorsScanIsWritten(project.value.path);
49671
+ const waited = await this.untilTheEditorsScanIsWritten(project.value.path);
49458
49672
  const refreshed = await this.refreshStaleClasses(project.value.path);
49459
49673
  if (!refreshed.ok) {
49460
49674
  return refreshed.response;
49461
49675
  }
49676
+ const imported = exported === null ? await this.importWhatChanged(project.value.path) : undefined;
49677
+ const scanned = await this.afterTheImport(project.value.path, waited, imported);
49462
49678
  const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
49463
49679
  if (op === "check") {
49464
49680
  return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, scanned, asked.value, flags.value);
@@ -49487,9 +49703,10 @@ class GodotServer {
49487
49703
  });
49488
49704
  const runtimeWaitMs = this.announceBudget(args, project.value.path, sceneArgument);
49489
49705
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
49490
- const silent = resolveSilent(args["silent"], process.env) && !headless;
49706
+ const silent = resolveSilent(args["silent"]) && !headless;
49707
+ const silentOnlyHere = silent && !editorPlaysHeadless(project.value.file);
49491
49708
  const profiled = readBoolean(args, "profile") === true;
49492
- if (exported === null && this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && flags.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
49709
+ if (exported === null && this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && flags.value.length === 0 && !needsItsOwnProcess(asked.value) && !silentOnlyHere) {
49493
49710
  return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended, profiled);
49494
49711
  }
49495
49712
  const cmdArgs = runArguments({
@@ -49510,7 +49727,7 @@ class GodotServer {
49510
49727
  started.announcedBefore = alreadyPlaying;
49511
49728
  started.bootScene = runtimeWaitMs.scene;
49512
49729
  this.activeProcess = started;
49513
- const spawnedInstead = exported !== null ? " An exported build is started without a debugger, since a release build quits when given one, so breakpoints and debug_state do not reach it." : this.godotBridge.isConnected() && editorWouldPlay && (given.value.length > 0 || flags.value.length > 0) ? ` The editor cannot be handed arguments for a game it plays, so this one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? ` The editor plays a game with its own environment, so a run given one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}, and silent: false plays it through the editor.${OWN_DEBUGGER}` : "";
49730
+ const spawnedInstead = exported !== null ? " An exported build is started without a debugger, since a release build quits when given one, so breakpoints and debug_state do not reach it." : this.godotBridge.isConnected() && editorWouldPlay && (given.value.length > 0 || flags.value.length > 0) ? ` The editor cannot be handed arguments for a game it plays, so this one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? ` The editor plays a game with its own environment, so a run given one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && silentOnlyHere ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here, silent as every run is unless asked otherwise, and silent: false plays it through the editor, with sound.${OWN_DEBUGGER}` : "";
49514
49731
  const endedForThis = endedToStartThis(ended);
49515
49732
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
49516
49733
  const handed = this.dapHoldingBreakpointsOf(project.value.path).breakpointsHeld();
@@ -49539,7 +49756,7 @@ class GodotServer {
49539
49756
  }
49540
49757
  async runtimeUp(projectPath, before, budget, withArgs) {
49541
49758
  const budgetMs = budget.ms;
49542
- if (!existsSync25(join32(projectPath, RUNTIME_AUTOLOAD.path))) {
49759
+ if (!existsSync26(join33(projectPath, RUNTIME_AUTOLOAD.path))) {
49543
49760
  return runtimeVerdict(null, {
49544
49761
  addon: false,
49545
49762
  budgetMs,
@@ -49558,7 +49775,7 @@ class GodotServer {
49558
49775
  const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
49559
49776
  const fresh = await announcedSince(projectPath, before, {
49560
49777
  budgetMs: Math.max(runBegan + budgetMs - waitBegan, 0),
49561
- accept: (one) => this.isTheGameOf(this.activeProcess, one, projectPath),
49778
+ accept: (one) => this.isTheGameOf(this.activeProcess, one),
49562
49779
  giveUp: () => {
49563
49780
  if (this.dapClient?.isStopped() === true) {
49564
49781
  return true;
@@ -49625,7 +49842,7 @@ class GodotServer {
49625
49842
  }
49626
49843
  let announcedAt;
49627
49844
  try {
49628
- announcedAt = statSync14(endpoint.file).mtimeMs;
49845
+ announcedAt = statSync15(endpoint.file).mtimeMs;
49629
49846
  } catch {
49630
49847
  return;
49631
49848
  }
@@ -49808,7 +50025,7 @@ class GodotServer {
49808
50025
  return;
49809
50026
  }
49810
50027
  const before = run.announcedBefore ?? new Set;
49811
- const theOne = this.announcedOfTheRunsProject(run, runtimesAnnounced().running).find((one) => one.pid === Number(named) && !before.has(one.pid) && this.isTheGameOf(run, one, run.projectPath));
50028
+ const theOne = this.announcedOfTheRunsProject(run, runtimesAnnounced().running).find((one) => one.pid === Number(named) && !before.has(one.pid) && this.isTheGameOf(run, one));
49812
50029
  if (theOne !== undefined) {
49813
50030
  this.tie(run, theOne);
49814
50031
  }
@@ -49819,7 +50036,7 @@ class GodotServer {
49819
50036
  return 0;
49820
50037
  }
49821
50038
  try {
49822
- return reportAlreadyInTranscript(readFileSync27(game.transcript), readFileSync27(report));
50039
+ return reportAlreadyInTranscript(readFileSync28(game.transcript), readFileSync28(report));
49823
50040
  } catch (error) {
49824
50041
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
49825
50042
  return 0;
@@ -49902,16 +50119,16 @@ class GodotServer {
49902
50119
  return ended;
49903
50120
  }
49904
50121
  }
49905
- const startedAs = {
50122
+ const { startedBy, command, projectPath } = running;
50123
+ const why = startedBy === undefined || command === undefined || projectPath === null ? "this server holds no engine, project and start of it to check the process against" : await whyNotStillTheRecordedRun({
49906
50124
  pid: running.pid,
49907
50125
  transcript: running.transcript ?? "",
49908
50126
  startedAt: running.startedAt,
49909
- ...running.startedBy === undefined ? {} : { startedBy: running.startedBy },
49910
- projectPath: running.projectPath ?? "",
50127
+ startedBy,
50128
+ projectPath,
49911
50129
  arguments: [],
49912
- ...running.command === undefined ? {} : { command: running.command }
49913
- };
49914
- const why = await whyNotStillTheRecordedRun(startedAs);
50130
+ command
50131
+ });
49915
50132
  if (why !== null) {
49916
50133
  clearTheRecord();
49917
50134
  running.endedHere = null;
@@ -50196,7 +50413,7 @@ class GodotServer {
50196
50413
  return run.announcedPid;
50197
50414
  }
50198
50415
  const before = run.announcedBefore ?? new Set;
50199
- const candidates = ours.filter((one) => !before.has(one.pid) && this.isTheGameOf(run, one, run.projectPath));
50416
+ const candidates = ours.filter((one) => !before.has(one.pid) && this.isTheGameOf(run, one));
50200
50417
  const theOne = candidates.length === 1 ? candidates[0] : undefined;
50201
50418
  if (theOne !== undefined) {
50202
50419
  this.tie(run, theOne);
@@ -50306,22 +50523,16 @@ class GodotServer {
50306
50523
  run.announcedPid = endpoint.pid;
50307
50524
  this.noteBootOf(run, endpoint);
50308
50525
  }
50309
- playedByOurEditor(endpoint, projectPath) {
50526
+ playedByOurEditor(endpoint) {
50310
50527
  const editor = this.godotBridge.getStatus().editorPid;
50311
- if (editor === undefined) {
50312
- return true;
50313
- }
50314
- if (endpoint.editorPid !== undefined) {
50315
- return endpoint.editorPid === editor;
50316
- }
50317
- return !runtimeAddonAnnouncesEditors(projectPath ?? this.ownProject);
50528
+ return editor === undefined || endpoint.editorPid === editor;
50318
50529
  }
50319
- isTheGameOf(run, endpoint, projectPath) {
50530
+ isTheGameOf(run, endpoint) {
50320
50531
  if (run === null) {
50321
50532
  return false;
50322
50533
  }
50323
50534
  if (run.throughEditor) {
50324
- return this.playedByOurEditor(endpoint, projectPath);
50535
+ return this.playedByOurEditor(endpoint);
50325
50536
  }
50326
50537
  const editor = this.godotBridge.getStatus().editorPid;
50327
50538
  return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
@@ -50339,12 +50550,12 @@ class GodotServer {
50339
50550
  log: new GameLog,
50340
50551
  transcript: record.transcript,
50341
50552
  readOffset: 0,
50342
- projectPath: record.projectPath === "" ? null : record.projectPath,
50553
+ projectPath: record.projectPath,
50343
50554
  startedAt: record.startedAt,
50344
- ...record.startedBy === undefined ? {} : { startedBy: record.startedBy },
50555
+ startedBy: record.startedBy,
50345
50556
  ...record.keeper === undefined ? {} : { keeper: record.keeper },
50346
50557
  ...record.profile === undefined ? {} : { profile: record.profile },
50347
- ...record.command === undefined ? {} : { command: record.command },
50558
+ command: record.command,
50348
50559
  exitCode: null,
50349
50560
  exitSignal: null,
50350
50561
  throughEditor: false,
@@ -50397,9 +50608,8 @@ class GodotServer {
50397
50608
  if (record === null) {
50398
50609
  return `No game is running.${noEditor} Start one with editor_run.`;
50399
50610
  }
50400
- const whose = record.projectPath === "" ? "a project it does not name" : record.projectPath;
50401
50611
  const ours = mine === null ? "this server was started without GDHARNESS_PROJECT and has no editor connected, so it cannot say whose that run is" : `this server serves ${mine}`;
50402
- return `No game of this server's is running.${noEditor} A run started from ${whose} is recorded on this machine, and ${ours}, so it is not this server's to answer for or to end. Start one with editor_run.`;
50612
+ return `No game of this server's is running.${noEditor} A run started from ${record.projectPath} is recorded on this machine, and ${ours}, so it is not this server's to answer for or to end. Start one with editor_run.`;
50403
50613
  }
50404
50614
  aRuntimeOfOursIsAnswering() {
50405
50615
  return this.announcedForOurProject(discoverRuntimes());
@@ -50521,7 +50731,7 @@ class GodotServer {
50521
50731
  };
50522
50732
  const going = await this.runStillGoing(run);
50523
50733
  const projectPath = run.projectPath;
50524
- const projectGone = projectPath !== null && !existsSync25(join32(projectPath, "project.godot"));
50734
+ const projectGone = projectPath !== null && !existsSync26(join33(projectPath, "project.godot"));
50525
50735
  if (projectGone && callersName !== undefined) {
50526
50736
  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"]);
50527
50737
  }
@@ -50868,7 +51078,7 @@ class GodotServer {
50868
51078
  notes.push(endedWithoutACode({
50869
51079
  throughEditor: run.throughEditor,
50870
51080
  announced: announced !== undefined || run.announcedPid !== undefined,
50871
- couldAnnounce: run.projectPath !== null && existsSync25(join32(run.projectPath, RUNTIME_AUTOLOAD.path)),
51081
+ couldAnnounce: run.projectPath !== null && existsSync26(join33(run.projectPath, RUNTIME_AUTOLOAD.path)),
50872
51082
  endedHere: typeof run.endedHere === "string",
50873
51083
  pickedUp: run.pickedUp === true
50874
51084
  }));
@@ -51185,10 +51395,9 @@ class GodotServer {
51185
51395
  idleSince = null;
51186
51396
  continue;
51187
51397
  }
51188
- const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
51189
51398
  const completed = readNumber(status, "scansCompleted");
51190
51399
  const notYetOver = completedBefore !== undefined && completed !== undefined && completed <= completedBefore;
51191
- busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending || notYetOver;
51400
+ busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || notYetOver;
51192
51401
  if (busy) {
51193
51402
  idleSince = null;
51194
51403
  } else {
@@ -51248,7 +51457,7 @@ class GodotServer {
51248
51457
  constantsBefore: listed("heldConstantsBefore")["heldConstantsBefore"] ?? [],
51249
51458
  constantsAfter: listed("constants")["constants"] ?? [],
51250
51459
  names: projectPath === "" ? [] : this.classesAScriptNames(projectPath, script),
51251
- restored: typeof answer["restored"] === "boolean" ? answer["restored"] : null,
51460
+ restored: answer["restored"] === true,
51252
51461
  restoredAs: readString(answer, "restoredAs") ?? ""
51253
51462
  })
51254
51463
  };
@@ -51352,7 +51561,7 @@ class GodotServer {
51352
51561
  }
51353
51562
  const failed = answer["failed"];
51354
51563
  if (typeof failed === "number" && failed !== 0) {
51355
- const left = answer["restored"] === true ? "" : answer["restored"] === false ? "; compiling it again from the text it was built from failed too, so the editor holds it unusable until it compiles" : "; a failed compile leaves the copy the editor holds unusable until it compiles, and this editor does not put it back";
51564
+ const left = answer["restored"] === true ? "" : "; compiling it again from the text it was built from failed too, so the editor holds it unusable until it compiles";
51356
51565
  notReloaded.push({
51357
51566
  scriptPath,
51358
51567
  problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}${left}`
@@ -51498,7 +51707,7 @@ class GodotServer {
51498
51707
  if (!scriptPath.ok) {
51499
51708
  return this.createErrorResponse(scriptPath.reason);
51500
51709
  }
51501
- if (!existsSync25(scriptPath.absolutePath)) {
51710
+ if (!existsSync26(scriptPath.absolutePath)) {
51502
51711
  return this.createErrorResponse(`Script file does not exist: ${scriptPath.absolutePath}`);
51503
51712
  }
51504
51713
  const asked = readNonEmptyString(args, "newScriptPath");
@@ -51912,7 +52121,7 @@ class GodotServer {
51912
52121
  }
51913
52122
  const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
51914
52123
  const declared = new RegExp(`^\\s*(?:@\\w+(?:\\([^)]*\\))?\\s+)*class_name\\s+${escaped}\\b`, "m");
51915
- if (declared.test(readFileSync27(scriptPath, "utf8"))) {
52124
+ if (declared.test(readFileSync28(scriptPath, "utf8"))) {
51916
52125
  return null;
51917
52126
  }
51918
52127
  const checked = await this.classesTheEditorCannotSee(args);
@@ -52011,20 +52220,18 @@ class GodotServer {
52011
52220
  ]);
52012
52221
  }
52013
52222
  try {
52014
- return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
52223
+ return this.jsonTextResponse(await this.godotBridge.invokeTool(toolName, args));
52015
52224
  } catch (error) {
52016
52225
  if (error instanceof BridgeTimeout) {
52017
52226
  return error;
52018
52227
  }
52228
+ if (error instanceof AddonOfAnotherRelease) {
52229
+ return this.createErrorResponse(error.message, ["editor_status says which addon the editor holds"]);
52230
+ }
52019
52231
  const message = errorMessage(error);
52020
52232
  if (message.includes(`Unknown tool: ${toolName}`)) {
52021
- const status = this.godotBridge.getStatus();
52022
- const remedy = addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
52023
- if (remedy === undefined) {
52024
- const log = open === undefined || open === "" ? "the editor's Output panel" : `${editorLogPath(open)} for an editor editor_launch opened, or the Output panel of one opened by hand`;
52025
- return this.createErrorResponse(`The editor's gdharness addon is this ${SERVER_VERSION} server's own and still has no ${named}, so a script of the addon failed to load in this editor and the commands it holds were never registered. The script and the error are in ${log}.`, ["editor_status says which addon the editor holds"]);
52026
- }
52027
- return this.createErrorResponse(`The editor's gdharness addon is older than this ${SERVER_VERSION} server and has no ${named}, so it cannot answer this call. ${remedy}`, ["editor_status says which addon the editor holds"]);
52233
+ const log = open === undefined || open === "" ? "the editor's Output panel" : `${editorLogPath(open)} for an editor editor_launch opened, or the Output panel of one opened by hand`;
52234
+ return this.createErrorResponse(`The editor's gdharness addon is this ${SERVER_VERSION} server's own and still has no ${named}, so a script of the addon failed to load in this editor and the commands it holds were never registered. The script and the error are in ${log}.`, ["editor_status says which addon the editor holds"]);
52028
52235
  }
52029
52236
  return this.createErrorResponse(`The editor answered ${named} with an error: ${message}`);
52030
52237
  }
@@ -52080,7 +52287,7 @@ class GodotServer {
52080
52287
  async ownRunOnItsWay(projectPath, budgetMs) {
52081
52288
  const run = await this.currentRun();
52082
52289
  const project = projectPath ?? run?.projectPath ?? null;
52083
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync25(join32(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
52290
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync26(join33(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
52084
52291
  return null;
52085
52292
  }
52086
52293
  const already = runtimesAnnounced().running;
@@ -52089,12 +52296,12 @@ class GodotServer {
52089
52296
  return !run.announcedBefore.has(one.pid);
52090
52297
  }
52091
52298
  try {
52092
- return statSync14(one.file).mtimeMs >= run.startedAt;
52299
+ return statSync15(one.file).mtimeMs >= run.startedAt;
52093
52300
  } catch {
52094
52301
  return false;
52095
52302
  }
52096
52303
  };
52097
- if (already.some((one) => announcedSinceTheStart(one) && this.isTheGameOf(run, one, project))) {
52304
+ if (already.some((one) => announcedSinceTheStart(one) && this.isTheGameOf(run, one))) {
52098
52305
  return null;
52099
52306
  }
52100
52307
  const which = run.throughEditor ? "the game the editor is playing for it" : `pid ${run.pid ?? "unknown"}`;
@@ -52106,7 +52313,7 @@ class GodotServer {
52106
52313
  }
52107
52314
  const fresh = await announcedSince(project, new Set(already.map((one) => one.pid)), {
52108
52315
  budgetMs: Math.min(budgetMs, remaining),
52109
- accept: (one) => this.isTheGameOf(this.activeProcess, one, project),
52316
+ accept: (one) => this.isTheGameOf(this.activeProcess, one),
52110
52317
  giveUp: () => this.activeProcess !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
52111
52318
  });
52112
52319
  const endpoint = await this.theRunsAmong(run, fresh);
@@ -52169,13 +52376,15 @@ class GodotServer {
52169
52376
  return this.createErrorResponse(refusal);
52170
52377
  }
52171
52378
  }
52172
- const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
52173
- const screenshotPath = screenshotDir ? join32(screenshotDir, "capture.png") : null;
52174
52379
  const staleNote = this.runtimeStaleNote(choice.endpoint);
52175
- const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
52176
- const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
52177
-
52178
- ${staleNote}`);
52380
+ if (staleNote !== undefined) {
52381
+ return this.createErrorResponse(staleNote, [
52382
+ "editor_status lists the game and the runtime addon it loaded"
52383
+ ]);
52384
+ }
52385
+ const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
52386
+ const screenshotPath = screenshotDir ? join33(screenshotDir, "capture.png") : null;
52387
+ const refused = (message) => this.createErrorResponse(message);
52179
52388
  const onGame = {
52180
52389
  pid: choice.endpoint.pid,
52181
52390
  what: `${asked.tool} ${asked.op}`,
@@ -52201,7 +52410,6 @@ ${staleNote}`);
52201
52410
  waitedMs: timeoutMs,
52202
52411
  note: `${reply.message} ${longerWaitNote(asked.tool, asked.op, timeoutMs)}`,
52203
52412
  ...answeredBy,
52204
- ...stale,
52205
52413
  ...alongside()
52206
52414
  });
52207
52415
  }
@@ -52210,7 +52418,7 @@ ${staleNote}`);
52210
52418
  }
52211
52419
  const { id: _id, ...payload } = reply.payload;
52212
52420
  if (!expectsScreenshot) {
52213
- return this.jsonTextResponse({ ...payload, ...answeredBy, ...stale, ...alongside() });
52421
+ return this.jsonTextResponse({ ...payload, ...answeredBy, ...alongside() });
52214
52422
  }
52215
52423
  const returnedPath = readString(payload, "path");
52216
52424
  if (readString(payload, "type") !== "screenshot_file" || !returnedPath || !screenshotPath) {
@@ -52234,11 +52442,9 @@ ${staleNote}`);
52234
52442
  type: "text",
52235
52443
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${ranWith.length === 0 ? "" : `
52236
52444
 
52237
- Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}${staleNote === undefined ? "" : `
52238
-
52239
- ${staleNote}`}`
52445
+ Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}`
52240
52446
  },
52241
- { type: "image", data: readFileSync27(screenshotPath).toString("base64"), mimeType: "image/png" }
52447
+ { type: "image", data: readFileSync28(screenshotPath).toString("base64"), mimeType: "image/png" }
52242
52448
  ]
52243
52449
  };
52244
52450
  } finally {
@@ -52355,7 +52561,7 @@ async function runGodotServer() {
52355
52561
  const server = new GodotServer;
52356
52562
  await server.run();
52357
52563
  }
52358
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
52564
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
52359
52565
  var init_server = __esm(() => {
52360
52566
  init_dist();
52361
52567
  init_stdio();
@@ -52384,6 +52590,7 @@ var init_server = __esm(() => {
52384
52590
  init_godot_path();
52385
52591
  init_headless();
52386
52592
  init_headless_operations();
52593
+ init_import_freshness();
52387
52594
  init_issues();
52388
52595
  init_junit();
52389
52596
  init_keeper_channel();
@@ -52476,8 +52683,8 @@ var init_server = __esm(() => {
52476
52683
  init_bridge_announce();
52477
52684
  init_errors();
52478
52685
  init_godot_path();
52479
- import { existsSync as existsSync26 } from "node:fs";
52480
- import { join as join33, resolve as resolve8 } from "node:path";
52686
+ import { existsSync as existsSync27 } from "node:fs";
52687
+ import { join as join34, resolve as resolve8 } from "node:path";
52481
52688
 
52482
52689
  // src/harnesses.ts
52483
52690
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
@@ -53472,10 +53679,10 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
53472
53679
  - \`editor_status\` says whether an editor is connected and whether its addon matches the server.
53473
53680
  \`addonIsStale\` means the editor is running different addon code from what the server ships
53474
53681
  (an older \`addonVersion\` on the same code is not stale), and \`staleNote\` says which half is behind: an editor that
53475
- needs restarting, or a server that needs reconnecting in your harness. Every answer that came
53476
- from the editor carries the same two when they differ, so a stale addon cannot answer quietly:
53477
- believe a restart over a diagnostic that arrived with them. A game's runtime addon is judged
53478
- the same way, under each entry in \`runtimes\` and on every \`runtime_*\` answer from it.
53682
+ needs restarting, a project that needs the upgrade command, or a server that needs reconnecting
53683
+ in your harness. A stale editor is asked nothing but to quit, and every other call to it is
53684
+ refused with the same note. A game's runtime addon is judged the same way, under each entry in
53685
+ \`runtimes\`, and a \`runtime_*\` call to a stale game is refused with its note.
53479
53686
  - Start the game with \`editor_run start\`, never by spawning an engine. The editor plays it, so
53480
53687
  its debugger holds it, which is what gives the \`debug_*\` tools something to talk to. Its answer
53481
53688
  says under \`runtime\` whether the game can be talked to yet; when it cannot, \`mayYetAnnounce\`
@@ -53761,7 +53968,7 @@ class UsageError extends Refusal {
53761
53968
  function projectArgument(at) {
53762
53969
  const named = args.slice(at).find((value) => !value.startsWith("--"));
53763
53970
  const projectPath = resolve8(named ?? ".");
53764
- if (!existsSync26(join33(projectPath, "project.godot"))) {
53971
+ if (!existsSync27(join34(projectPath, "project.godot"))) {
53765
53972
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
53766
53973
  }
53767
53974
  return projectPath;
@@ -53769,11 +53976,11 @@ function projectArgument(at) {
53769
53976
  async function engine(projectPath) {
53770
53977
  const located = await new GodotLocator().find();
53771
53978
  if (located.ok) {
53772
- return { godotPath: located.path, script: shippedOperationsScript(), debug: GODOT_DEBUG_MODE_DEFAULT };
53979
+ return { godotPath: located.path, script: shippedOperationsScript(), debug: DEBUG_MODE };
53773
53980
  }
53774
53981
  const recorded = located.named === null && projectPath !== undefined ? recordedEnginePath(projectPath) : null;
53775
53982
  if (recorded !== null) {
53776
- return { godotPath: recorded, script: shippedOperationsScript(), debug: GODOT_DEBUG_MODE_DEFAULT };
53983
+ return { godotPath: recorded, script: shippedOperationsScript(), debug: DEBUG_MODE };
53777
53984
  }
53778
53985
  const main = located.named === null && projectPath !== undefined ? mainCheckoutOf(projectPath) : null;
53779
53986
  const theirs = main === null ? null : recordedEnginePath(main);
@@ -53782,7 +53989,7 @@ async function engine(projectPath) {
53782
53989
  throw new UsageError(`${refusal.message} ${refusal.advice.join(". ")}.`);
53783
53990
  }
53784
53991
  console.error(`engine: ${theirs}, which the harness config of ${main} names; this project is a git worktree of it`);
53785
- return { godotPath: theirs, script: shippedOperationsScript(), debug: GODOT_DEBUG_MODE_DEFAULT };
53992
+ return { godotPath: theirs, script: shippedOperationsScript(), debug: DEBUG_MODE };
53786
53993
  }
53787
53994
  function said2(outcome, what) {
53788
53995
  if (!outcome.ok) {
@@ -53806,7 +54013,7 @@ async function registerRuntime(godot, projectPath) {
53806
54013
  return;
53807
54014
  }
53808
54015
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
53809
- if (!existsSync26(join33(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
54016
+ if (!existsSync27(join34(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
53810
54017
  console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
53811
54018
  }
53812
54019
  }
@@ -53938,7 +54145,7 @@ async function setup() {
53938
54145
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
53939
54146
  }
53940
54147
  if (!args.includes("--no-skill")) {
53941
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync26);
54148
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync27);
53942
54149
  for (const written of writeSkill(directories, getLocalVersion())) {
53943
54150
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
53944
54151
  }
@@ -54052,14 +54259,14 @@ async function upgrade() {
54052
54259
  moved.push(took.from);
54053
54260
  }
54054
54261
  }
54055
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync26), version)) {
54262
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync27), version)) {
54056
54263
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
54057
54264
  }
54058
54265
  console.log(`
54059
54266
  On ${version}. What is left for you:`);
54060
54267
  console.log(digestBefore !== null && digestBefore === installedEditorDigest(projectPath) ? ` 1. Nothing for the editor: this upgrade left the editor addons the same code as before,
54061
54268
  ` + ` so it gives an open editor nothing to restart for. editor_status says addonIsStale
54062
- ` + " if the editor is older than that." : ` 1. The open editor is still running the addons it loaded at startup. Restart it with the
54269
+ ` + " if the editor is running other code." : ` 1. The open editor is still running the addons it loaded at startup. Restart it with the
54063
54270
  ` + " editor_launch restart tool, which closes and reopens the window.");
54064
54271
  const serving = await serverServing(projectPath);
54065
54272
  console.log(harnessNote({ moved, written, byHand }, [launch.command, ...launch.args].join(" "), serving === null ? null : { server: serving, isThisVersion: serving.version === version }));