gdharness 1.1.21 → 1.1.22

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.
Files changed (47) hide show
  1. package/build/cli.js +1036 -525
  2. package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
  3. package/build/godot/addons/gdharness_editor/reading.gd +17 -0
  4. package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
  5. package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
  6. package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
  7. package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
  8. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
  9. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
  10. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
  11. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
  13. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
  14. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
  15. package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
  16. package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
  17. package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +8 -4
  19. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
  20. package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
  22. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +28 -8
  23. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +7 -299
  24. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
  25. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
  26. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
  27. package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
  28. package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
  29. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +20 -9
  30. package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
  31. package/build/godot/operations/audio_buses.gd +47 -11
  32. package/build/godot/operations/class_cache.gd +18 -2
  33. package/build/godot/operations/dependencies.gd +128 -17
  34. package/build/godot/operations/file_walk.gd +20 -0
  35. package/build/godot/operations/gdscript_analysis.gd +59 -20
  36. package/build/godot/operations/gdscript_authoring.gd +127 -60
  37. package/build/godot/operations/godot_operations.gd +24 -5
  38. package/build/godot/operations/import_pipeline.gd +179 -19
  39. package/build/godot/operations/logger.gd +2 -2
  40. package/build/godot/operations/plugins.gd +88 -70
  41. package/build/godot/operations/project_config.gd +41 -6
  42. package/build/godot/operations/project_diagnostics.gd +25 -10
  43. package/build/godot/operations/reading.gd +17 -0
  44. package/build/godot/operations/resource_files.gd +60 -17
  45. package/build/godot/operations/serialisation.gd +116 -7
  46. package/build/index.js +949 -435
  47. package/package.json +1 -1
package/build/index.js CHANGED
@@ -10201,27 +10201,27 @@ function defectReport(where, error, context = {}) {
10201
10201
  }
10202
10202
 
10203
10203
  // src/server.ts
10204
- import { execFile as execFile6, spawn as spawn3 } from "node:child_process";
10204
+ import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
10205
10205
  import { randomUUID as randomUUID2 } from "node:crypto";
10206
10206
  import {
10207
10207
  appendFileSync,
10208
10208
  closeSync as closeSync2,
10209
10209
  constants,
10210
10210
  copyFileSync,
10211
- existsSync as existsSync12,
10211
+ existsSync as existsSync14,
10212
10212
  fstatSync,
10213
10213
  mkdirSync as mkdirSync10,
10214
10214
  openSync as openSync3,
10215
10215
  readdirSync as readdirSync8,
10216
- readFileSync as readFileSync17,
10216
+ readFileSync as readFileSync18,
10217
10217
  readSync as readSync2,
10218
10218
  rmdirSync as rmdirSync2,
10219
10219
  statSync as statSync8
10220
10220
  } from "node:fs";
10221
- import { basename as basename4, dirname as dirname11, extname as extname2, isAbsolute as isAbsolute3, join as join18, normalize as normalize2, relative as relative3 } from "node:path";
10221
+ import { basename as basename4, dirname as dirname12, extname as extname2, isAbsolute as isAbsolute3, join as join19, normalize as normalize2, relative as relative3 } from "node:path";
10222
10222
  import { setTimeout as delay3 } from "node:timers/promises";
10223
10223
  import { fileURLToPath as fileURLToPath4 } from "node:url";
10224
- import { promisify as promisify6 } from "node:util";
10224
+ import { promisify as promisify5 } from "node:util";
10225
10225
 
10226
10226
  // node_modules/zod/v3/helpers/util.js
10227
10227
  var util;
@@ -24914,6 +24914,12 @@ function resolveWithinProject(projectPath, candidatePath) {
24914
24914
  return { ok: false, reason: "Path contains a null byte." };
24915
24915
  }
24916
24916
  const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
24917
+ if (withoutScheme.startsWith("uid://")) {
24918
+ return {
24919
+ ok: false,
24920
+ reason: `Path '${trimmed}' is a UID, and a file is judged here by where it is on disk, which a UID does not say. Give the file's res:// path.`
24921
+ };
24922
+ }
24917
24923
  const scheme = OTHER_SCHEME.exec(withoutScheme);
24918
24924
  if (scheme) {
24919
24925
  return {
@@ -25620,13 +25626,13 @@ async function announcedSince(projectPath, before, waiting = {}) {
25620
25626
  const directories = waiting.directories ?? runtimeDirectories();
25621
25627
  const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
25622
25628
  for (;; ) {
25623
- const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && isSameDirectory(endpoint.project.path, projectPath) && (waiting.accept?.(endpoint) ?? true));
25624
- if (fresh !== undefined) {
25629
+ const fresh = discoverRuntimes(directories).filter((endpoint) => !before.has(endpoint.pid) && isSameDirectory(endpoint.project.path, projectPath) && (waiting.accept?.(endpoint) ?? true));
25630
+ if (fresh.length > 0) {
25625
25631
  return fresh;
25626
25632
  }
25627
25633
  const left = until - Date.now();
25628
25634
  if (left <= 0 || waiting.giveUp?.() === true) {
25629
- return null;
25635
+ return [];
25630
25636
  }
25631
25637
  await delay(Math.min(LOOK_EVERY_MS, left));
25632
25638
  }
@@ -26033,6 +26039,10 @@ function callSignal() {
26033
26039
  import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
26034
26040
  import { join as join6 } from "node:path";
26035
26041
  var DECLARATION = /^(?:@[a-z_]+(?:\([^)\n]*\))?\s+)*class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m;
26042
+ var TOOL = /^[ \t]*(?:@[a-z_]+(?:\([^)\n]*\))?[ \t]+)*@tool\b/m;
26043
+ function isToolScript(source) {
26044
+ return TOOL.test(codeOf(source));
26045
+ }
26036
26046
  function declaredClasses(projectPath) {
26037
26047
  const declared = new Map;
26038
26048
  eachScript(projectPath, (script, source) => {
@@ -26072,7 +26082,7 @@ function scriptsNaming(projectPath, classes) {
26072
26082
  }
26073
26083
  const code = codeOf(source);
26074
26084
  if (classesNamedIn(code, named).length > 0) {
26075
- naming.push({ script, tool: /^\s*@tool\b/m.test(code) });
26085
+ naming.push({ script, tool: TOOL.test(code) });
26076
26086
  }
26077
26087
  });
26078
26088
  return naming.sort((a, b) => a.script.localeCompare(b.script));
@@ -26279,6 +26289,19 @@ function classRemedy(entries) {
26279
26289
  const oneAtATime = declaring.length === 1 ? "" : " Take them one at a time: a rescan that brings in a class also reloads every script naming it and lists them under dependentsReloaded, so a script listed there needs no call of its own.";
26280
26290
  return `For ${entries.map(written).join(", ")}, run ${reload} first.${oneAtATime} In the one project ` + "that has reproduced a stale enum member, three times, the reload cleared it at once each time, " + "and the one plain editor_rescan tried left the diagnostic standing.";
26281
26291
  }
26292
+ function notReloadedNote(broughtIn, notReloaded) {
26293
+ const tried = notReloaded.filter((one) => one.tool !== true).map((one) => one.scriptPath);
26294
+ const tools = notReloaded.filter((one) => one.tool === true).map((one) => one.scriptPath);
26295
+ const all = [...tried, ...tools];
26296
+ const remedies = [];
26297
+ if (tried.length > 0) {
26298
+ remedies.push(`For ${tried.join(", ")}: editor_rescan with reloadScript on ${tried.length === 1 ? "it" : "each"}, or editor_launch restart.`);
26299
+ }
26300
+ if (tools.length > 0) {
26301
+ remedies.push(`For ${tools.join(", ")}, ${tools.length === 1 ? "a @tool script" : "@tool scripts"}, which ${tools.length === 1 ? "runs" : "run"} inside the editor and ${tools.length === 1 ? "is" : "are"} not reloaded from inside a call: editor_launch restart.`);
26302
+ }
26303
+ return `The scan brought in ${broughtIn.join(", ")}, and ${all.join(", ")} ${all.length === 1 ? "names one of them and" : "name them and"} could not be reloaded, so the editor still holds what it compiled while it could not see the class and reports that to the language server. ${remedies.join(" ")}`;
26304
+ }
26282
26305
  function failedReloadNote(failed) {
26283
26306
  const missing = (before, after) => before.filter((name) => !after.includes(name));
26284
26307
  const lost = [
@@ -26287,7 +26310,17 @@ function failedReloadNote(failed) {
26287
26310
  ];
26288
26311
  const unchanged = lost.length === 0 && missing(failed.heldAfter, failed.heldBefore).length === 0 && missing(failed.constantsAfter, failed.constantsBefore).length === 0;
26289
26312
  const answered = failed.codeName === "" ? `error ${failed.code}` : `error ${failed.code}, ${failed.codeName}`;
26290
- const copy = unchanged ? "Nothing was reloaded: the copy the editor holds is as it was, with the members under heldBeforeReload and heldConstantsBeforeReload." : `The copy the editor holds changed even so${lost.length === 0 ? "" : `, and has lost ${lost.join(", ")}`}: anything diagnosed against it now is answered from what is left, and reloading it once it compiles rebuilds it.`;
26313
+ 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.";
26314
+ let copy;
26315
+ if (failed.restored === null) {
26316
+ 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.`;
26317
+ } else if (!failed.restored) {
26318
+ 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.`;
26319
+ } else if (unchanged) {
26320
+ 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 under heldBeforeReload and heldConstantsBeforeReload.";
26321
+ } else {
26322
+ copy = `The copy the editor holds was compiled again from the text it was built from and changed even so${lost.length === 0 ? "" : `, and has lost ${lost.join(", ")}`}: anything diagnosed against it now is answered from what is left, and reloading it once it compiles rebuilds it.`;
26323
+ }
26291
26324
  const order = failed.names.length === 0 ? "" : ` If they deny a member of ${failed.names.join(" or ")} that its file declares, the editor's copy of that class is behind: reload that class first with reloadScript and then this script, the order that cleared it in the one project that has met this.`;
26292
26325
  return `${failed.script} did not compile: Godot answered ${answered}. ${copy} ` + `script_diagnostics on it gives the errors.${order}`;
26293
26326
  }
@@ -26526,7 +26559,14 @@ function bindable(port, host) {
26526
26559
  var DEFAULT_DAP_PORT = 6006;
26527
26560
  var DAP_REQUEST_TIMEOUT_MS = 1e4;
26528
26561
  var SCOPES_TIMEOUT_MS = 20000;
26562
+ var STEP_LANDS_WITHIN_MS = 1e4;
26529
26563
  var VARIABLES_POLL_TIMEOUT_MS = 2000;
26564
+ function fileKey(path) {
26565
+ return process.platform === "win32" ? path.replaceAll("\\", "/").toLowerCase() : path;
26566
+ }
26567
+
26568
+ class ReadRefusal extends Refusal {
26569
+ }
26530
26570
  function said(body, field) {
26531
26571
  const value = body?.[field];
26532
26572
  return typeof value === "string" ? value : "";
@@ -26543,17 +26583,25 @@ class GodotDAPClient {
26543
26583
  outputBuffer = [];
26544
26584
  onOutput = null;
26545
26585
  maxOutputLines = 1000;
26586
+ outputDropped = 0;
26587
+ outputTaken = 0;
26546
26588
  initialized = false;
26547
26589
  attached = false;
26548
26590
  lastThreadId = 1;
26549
26591
  halt = null;
26550
26592
  holdKnown = false;
26593
+ stops = 0;
26551
26594
  breakpoints = new Map;
26595
+ spelled = new Map;
26552
26596
  inEditor = new Map;
26553
26597
  onBreakpointsChanged = null;
26554
- constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
26598
+ scopesPatienceMs;
26599
+ stepPatienceMs;
26600
+ constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1", patience = {}) {
26555
26601
  this.port = port;
26556
26602
  this.host = host;
26603
+ this.scopesPatienceMs = patience.scopesMs ?? SCOPES_TIMEOUT_MS;
26604
+ this.stepPatienceMs = patience.stepMs ?? STEP_LANDS_WITHIN_MS;
26557
26605
  this.pendingRequests = new Map;
26558
26606
  }
26559
26607
  async connect() {
@@ -26714,6 +26762,7 @@ class GodotDAPClient {
26714
26762
  }
26715
26763
  }
26716
26764
  if (this.outputBuffer.length > this.maxOutputLines) {
26765
+ this.outputDropped += this.outputBuffer.length - this.maxOutputLines;
26717
26766
  this.outputBuffer = this.outputBuffer.slice(this.outputBuffer.length - this.maxOutputLines);
26718
26767
  }
26719
26768
  return;
@@ -26730,6 +26779,7 @@ class GodotDAPClient {
26730
26779
  text: said(body, "text")
26731
26780
  };
26732
26781
  this.holdKnown = true;
26782
+ this.stops += 1;
26733
26783
  return;
26734
26784
  }
26735
26785
  if (eventName === "continued") {
@@ -26746,20 +26796,24 @@ class GodotDAPClient {
26746
26796
  if (typeof path !== "string" || typeof line !== "number") {
26747
26797
  return;
26748
26798
  }
26799
+ const key = fileKey(path);
26800
+ if (!this.spelled.has(key)) {
26801
+ this.spelled.set(key, path);
26802
+ }
26749
26803
  if (said(body, "reason") === "removed") {
26750
- this.inEditor.get(path)?.delete(line);
26751
- const held = this.breakpoints.get(path);
26804
+ this.inEditor.get(key)?.delete(line);
26805
+ const held = this.breakpoints.get(key);
26752
26806
  if (held?.delete(line) === true) {
26753
26807
  if (held.size === 0) {
26754
- this.breakpoints.delete(path);
26808
+ this.breakpoints.delete(key);
26755
26809
  }
26756
26810
  this.onBreakpointsChanged?.(this.breakpointsHeld());
26757
26811
  }
26758
26812
  return;
26759
26813
  }
26760
- const known = this.inEditor.get(path) ?? new Set;
26814
+ const known = this.inEditor.get(key) ?? new Set;
26761
26815
  known.add(line);
26762
- this.inEditor.set(path, known);
26816
+ this.inEditor.set(key, known);
26763
26817
  return;
26764
26818
  }
26765
26819
  if (eventName === "terminated" || eventName === "exited") {
@@ -26842,24 +26896,44 @@ class GodotDAPClient {
26842
26896
  getOutput(clear = false) {
26843
26897
  const lines = [...this.outputBuffer];
26844
26898
  if (clear) {
26899
+ this.outputTaken += lines.length;
26845
26900
  this.outputBuffer = [];
26846
26901
  }
26847
26902
  return lines;
26848
26903
  }
26904
+ outputSoFar() {
26905
+ const output = this.getOutput(false);
26906
+ const notShown = this.outputDropped + this.outputTaken;
26907
+ if (notShown === 0) {
26908
+ return { lines: output.length, output };
26909
+ }
26910
+ const where = [
26911
+ this.outputDropped > 0 ? `${this.outputDropped} fell out of this buffer, which keeps the last ${this.maxOutputLines}` : "",
26912
+ this.outputTaken > 0 ? `${this.outputTaken} were moved into the run's log by an earlier read` : ""
26913
+ ].filter((part) => part !== "");
26914
+ return {
26915
+ lines: output.length,
26916
+ output,
26917
+ notShown,
26918
+ note: `${notShown} earlier ${notShown === 1 ? "line is" : "lines are"} not in this answer: ${where.join(", and ")}. editor_output reads what the server kept of the run's console.`
26919
+ };
26920
+ }
26849
26921
  async setBreakpoint(filePath, line) {
26850
- const wanted = new Set(this.breakpoints.get(filePath) ?? []);
26922
+ const wanted = new Set(this.breakpoints.get(fileKey(filePath)) ?? []);
26851
26923
  wanted.add(line);
26852
26924
  return await this.sendBreakpoints(filePath, wanted);
26853
26925
  }
26854
26926
  async removeBreakpoint(filePath, line) {
26855
- const wanted = new Set(this.breakpoints.get(filePath) ?? []);
26927
+ const wanted = new Set(this.breakpoints.get(fileKey(filePath)) ?? []);
26856
26928
  wanted.delete(line);
26857
26929
  return await this.sendBreakpoints(filePath, wanted, line);
26858
26930
  }
26859
26931
  async sendBreakpoints(filePath, mine, clearing) {
26860
26932
  await this.ensureConnected();
26861
26933
  await this.initialize();
26862
- const lines = new Set([...mine, ...this.inEditor.get(filePath) ?? []]);
26934
+ const key = fileKey(filePath);
26935
+ this.spelled.set(key, filePath);
26936
+ const lines = new Set([...mine, ...this.inEditor.get(key) ?? []]);
26863
26937
  if (clearing !== undefined) {
26864
26938
  lines.delete(clearing);
26865
26939
  }
@@ -26869,14 +26943,14 @@ class GodotDAPClient {
26869
26943
  breakpoints: sorted.map((breakpointLine) => ({ line: breakpointLine }))
26870
26944
  });
26871
26945
  if (mine.size === 0) {
26872
- this.breakpoints.delete(filePath);
26946
+ this.breakpoints.delete(key);
26873
26947
  } else {
26874
- this.breakpoints.set(filePath, new Set(mine));
26948
+ this.breakpoints.set(key, new Set(mine));
26875
26949
  }
26876
26950
  if (lines.size === 0) {
26877
- this.inEditor.delete(filePath);
26951
+ this.inEditor.delete(key);
26878
26952
  } else {
26879
- this.inEditor.set(filePath, lines);
26953
+ this.inEditor.set(key, lines);
26880
26954
  }
26881
26955
  return answer;
26882
26956
  }
@@ -26885,11 +26959,11 @@ class GodotDAPClient {
26885
26959
  }
26886
26960
  breakpointsInEditor() {
26887
26961
  const theirs = [];
26888
- for (const [scriptPath, lines] of this.inEditor) {
26889
- const mine = this.breakpoints.get(scriptPath);
26962
+ for (const [key, lines] of this.inEditor) {
26963
+ const mine = this.breakpoints.get(key);
26890
26964
  const rest = Array.from(lines).filter((line) => !mine?.has(line)).sort((a, b) => a - b);
26891
26965
  if (rest.length > 0) {
26892
- theirs.push({ scriptPath, lines: rest });
26966
+ theirs.push({ scriptPath: this.spelled.get(key) ?? key, lines: rest });
26893
26967
  }
26894
26968
  }
26895
26969
  return theirs;
@@ -26897,12 +26971,13 @@ class GodotDAPClient {
26897
26971
  async reapplyBreakpoints() {
26898
26972
  const applied = [];
26899
26973
  const refused = [];
26900
- for (const [filePath, lines] of Array.from(this.breakpoints)) {
26974
+ for (const [key, lines] of Array.from(this.breakpoints)) {
26975
+ const filePath = this.spelled.get(key) ?? key;
26901
26976
  try {
26902
26977
  await this.sendBreakpoints(filePath, lines);
26903
26978
  applied.push({ scriptPath: filePath, lines: Array.from(lines).sort((a, b) => a - b) });
26904
26979
  } catch (error) {
26905
- this.breakpoints.delete(filePath);
26980
+ this.breakpoints.delete(key);
26906
26981
  refused.push({
26907
26982
  scriptPath: filePath,
26908
26983
  reason: error instanceof Error ? error.message : String(error)
@@ -26915,19 +26990,21 @@ class GodotDAPClient {
26915
26990
  return { applied, refused };
26916
26991
  }
26917
26992
  breakpointsHeld() {
26918
- return Array.from(this.breakpoints, ([scriptPath, lines]) => ({
26919
- scriptPath,
26993
+ return Array.from(this.breakpoints, ([key, lines]) => ({
26994
+ scriptPath: this.spelled.get(key) ?? key,
26920
26995
  lines: Array.from(lines).sort((a, b) => a - b)
26921
26996
  }));
26922
26997
  }
26923
26998
  holdBreakpoints(held) {
26924
26999
  for (const { scriptPath, lines } of held) {
26925
- const fileBreakpoints = this.breakpoints.get(scriptPath) ?? new Set;
27000
+ const key = fileKey(scriptPath);
27001
+ const fileBreakpoints = this.breakpoints.get(key) ?? new Set;
26926
27002
  for (const line of lines) {
26927
27003
  fileBreakpoints.add(line);
26928
27004
  }
26929
27005
  if (fileBreakpoints.size > 0) {
26930
- this.breakpoints.set(scriptPath, fileBreakpoints);
27006
+ this.breakpoints.set(key, fileBreakpoints);
27007
+ this.spelled.set(key, scriptPath);
26931
27008
  }
26932
27009
  }
26933
27010
  }
@@ -26938,15 +27015,16 @@ class GodotDAPClient {
26938
27015
  this.halt = null;
26939
27016
  this.holdKnown = true;
26940
27017
  }
26941
- async stepOver(threadId) {
27018
+ async step(command) {
26942
27019
  await this.attach();
26943
- const resolvedThreadId = await this.resolveThreadId(threadId);
26944
- await this.sendRequest("next", { threadId: resolvedThreadId });
26945
- }
26946
- async stepInto(threadId) {
26947
- await this.attach();
26948
- const resolvedThreadId = await this.resolveThreadId(threadId);
26949
- await this.sendRequest("stepIn", { threadId: resolvedThreadId });
27020
+ const resolvedThreadId = await this.resolveThreadId();
27021
+ const before = this.stops;
27022
+ await this.sendRequest(command, { threadId: resolvedThreadId });
27023
+ const deadline = Date.now() + this.stepPatienceMs;
27024
+ while (this.stops === before && this.connected && Date.now() < deadline) {
27025
+ await delay2(50);
27026
+ }
27027
+ return this.stops !== before;
26950
27028
  }
26951
27029
  async getStackTrace(threadId) {
26952
27030
  await this.attach();
@@ -26972,16 +27050,16 @@ class GodotDAPClient {
26972
27050
  } catch {}
26973
27051
  await delay2(200);
26974
27052
  }
26975
- return [];
27053
+ return null;
26976
27054
  }
26977
27055
  async getScopes(frameId) {
26978
27056
  await this.attach();
26979
27057
  const frames = await this.getStackTrace();
26980
27058
  const frame = frameId === undefined ? frames[0] : frames.find((each) => each["id"] === frameId);
26981
27059
  if (frame === undefined) {
26982
- return [];
27060
+ throw new ReadRefusal(frames.length === 0 ? "The adapter gave no stack, so there is no frame to read variables from" : `The stack the adapter gives now has no frame ${String(frameId)}; its frames are ${frames.map((each) => String(each["id"])).join(", ")}. Read the stack again for the frames the game is at now`);
26983
27061
  }
26984
- const deadline = Date.now() + SCOPES_TIMEOUT_MS;
27062
+ const deadline = Date.now() + this.scopesPatienceMs;
26985
27063
  let scopes = [];
26986
27064
  while (scopes.length === 0 && Date.now() < deadline) {
26987
27065
  try {
@@ -26993,6 +27071,9 @@ class GodotDAPClient {
26993
27071
  } catch {}
26994
27072
  await delay2(200);
26995
27073
  }
27074
+ if (scopes.length === 0) {
27075
+ throw new ReadRefusal(`The scopes of frame ${String(frame["id"])} did not arrive from the game within ${this.scopesPatienceMs / 1000}s. They come from the game through the editor, so a busy machine can take longer: ask again`);
27076
+ }
26996
27077
  const named = [];
26997
27078
  for (const scope of scopes) {
26998
27079
  const reference = scope["variablesReference"];
@@ -27056,9 +27137,8 @@ async function handleDAPTool(client, toolName, args) {
27056
27137
  try {
27057
27138
  switch (toolName) {
27058
27139
  case "dap_get_output": {
27059
- const output = client.getOutput(false);
27060
27140
  return {
27061
- content: [{ type: "text", text: JSON.stringify({ lines: output.length, output }, null, 2) }]
27141
+ content: [{ type: "text", text: JSON.stringify(client.outputSoFar(), null, 2) }]
27062
27142
  };
27063
27143
  }
27064
27144
  case "dap_set_breakpoint": {
@@ -27085,19 +27165,13 @@ async function handleDAPTool(client, toolName, args) {
27085
27165
  }
27086
27166
  case "dap_step_over":
27087
27167
  case "dap_step_into": {
27088
- if (toolName === "dap_step_into") {
27089
- await client.stepInto();
27090
- } else {
27091
- await client.stepOver();
27092
- }
27093
- return {
27094
- content: [
27095
- {
27096
- type: "text",
27097
- text: JSON.stringify({ stepped: true, stack: await client.getStackTrace() }, null, 2)
27098
- }
27099
- ]
27168
+ const heldAgain = await client.step(toolName === "dap_step_into" ? "stepIn" : "next");
27169
+ const answer = heldAgain ? { stepped: true, heldAgain, stack: await client.getStackTrace() } : {
27170
+ stepped: true,
27171
+ heldAgain,
27172
+ note: `The game had not stopped again ${client.stepPatienceMs / 1000}s after the step: the step is still running, it went on without stopping anywhere, or the game was not held when it was asked. debug_state stack answers once it stops.`
27100
27173
  };
27174
+ return { content: [{ type: "text", text: JSON.stringify(answer, null, 2) }] };
27101
27175
  }
27102
27176
  case "dap_get_stack_trace": {
27103
27177
  const stack = await client.getStackTrace();
@@ -27108,8 +27182,14 @@ async function handleDAPTool(client, toolName, args) {
27108
27182
  case "dap_get_variables": {
27109
27183
  const frameId = typeof safeArgs.frameId === "number" ? safeArgs.frameId : undefined;
27110
27184
  const scopes = await client.getScopes(frameId);
27185
+ const missing = scopes.filter((scope) => scope.variables === null).map((scope) => scope.name);
27186
+ const answer = missing.length === 0 ? { scopes } : {
27187
+ scopes,
27188
+ valuesMissing: missing,
27189
+ note: `The values of ${missing.join(", ")} did not arrive from the game within ${client.scopesPatienceMs / 1000}s, so they are unknown rather than empty. Ask again.`
27190
+ };
27111
27191
  return {
27112
- content: [{ type: "text", text: JSON.stringify({ scopes }, null, 2) }]
27192
+ content: [{ type: "text", text: JSON.stringify(answer, null, 2) }]
27113
27193
  };
27114
27194
  }
27115
27195
  default:
@@ -27117,10 +27197,12 @@ async function handleDAPTool(client, toolName, args) {
27117
27197
  }
27118
27198
  } catch (error) {
27119
27199
  const message = error instanceof Error ? error.message : String(error);
27120
- const cure = toolName === "dap_set_breakpoint" || toolName === "dap_remove_breakpoint" ? "Breakpoints need the editor open, not a running game: editor_status says whether one is connected, and the path is the script as the project spells it." : "The debug tools answer for a game the editor is playing: start one with editor_run.";
27200
+ const cure = error instanceof ReadRefusal ? "" : toolName === "dap_set_breakpoint" || toolName === "dap_remove_breakpoint" ? "Breakpoints need the editor open, not a running game: editor_status says whether one is connected, and the path is the script as the project spells it." : "The debug tools answer for a game the editor is playing: start one with editor_run.";
27121
27201
  return {
27122
27202
  isError: true,
27123
- content: [{ type: "text", text: `DAP tool '${toolName}' failed: ${message}. ${cure}` }]
27203
+ content: [
27204
+ { type: "text", text: `DAP tool '${toolName}' failed: ${message}.${cure === "" ? "" : ` ${cure}`}` }
27205
+ ]
27124
27206
  };
27125
27207
  }
27126
27208
  }
@@ -27482,6 +27564,47 @@ function linesBefore(entries, at, collapsed, wanted) {
27482
27564
  return found.reverse();
27483
27565
  }
27484
27566
 
27567
+ // src/engine-run.ts
27568
+ import { spawn as spawn2 } from "node:child_process";
27569
+ function runEngine(executable, args, options = {}) {
27570
+ return new Promise((resolve) => {
27571
+ const log = new GameLog;
27572
+ let failure = null;
27573
+ const signal = callSignal();
27574
+ const child = spawn2(executable, [...args], {
27575
+ stdio: ["ignore", "pipe", "pipe"],
27576
+ windowsHide: true,
27577
+ ...signal === undefined ? {} : { signal }
27578
+ });
27579
+ child.stdout.on("data", (chunk) => {
27580
+ log.append("stdout", chunk);
27581
+ });
27582
+ child.stderr.on("data", (chunk) => {
27583
+ log.append("stderr", chunk);
27584
+ });
27585
+ const { timeout } = options;
27586
+ const timer = timeout === undefined ? undefined : setTimeout(() => {
27587
+ failure ??= `it ran past its ${timeout.said}`;
27588
+ child.kill();
27589
+ }, timeout.ms);
27590
+ child.on("error", (error) => {
27591
+ failure ??= error.name === "AbortError" ? "the call was cancelled" : `it could not be started: ${error.message}`;
27592
+ });
27593
+ child.on("close", (code, closedBy) => {
27594
+ clearTimeout(timer);
27595
+ log.finish();
27596
+ const started = child.pid !== undefined;
27597
+ resolve({ log, exitCode: started ? code : null, exitSignal: closedBy, failure });
27598
+ });
27599
+ });
27600
+ }
27601
+ function howItEnded(run) {
27602
+ if (run.failure !== null) {
27603
+ return run.exitSignal === null ? run.failure : `ended by ${run.exitSignal}, because ${run.failure}`;
27604
+ }
27605
+ return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}`;
27606
+ }
27607
+
27485
27608
  // src/godot-bridge.ts
27486
27609
  import { randomUUID } from "node:crypto";
27487
27610
  import { EventEmitter } from "node:events";
@@ -28148,10 +28271,8 @@ class GodotLocator {
28148
28271
  }
28149
28272
 
28150
28273
  // src/headless.ts
28151
- import { execFile as execFile3 } from "node:child_process";
28152
- import { writeFileSync as writeFileSync8 } from "node:fs";
28153
- import { join as join13 } from "node:path";
28154
- import { promisify as promisify3 } from "node:util";
28274
+ import { existsSync as existsSync10, readFileSync as readFileSync12, realpathSync as realpathSync2, writeFileSync as writeFileSync8 } from "node:fs";
28275
+ import { dirname as dirname7, join as join13 } from "node:path";
28155
28276
 
28156
28277
  // src/reimport.ts
28157
28278
  import { createHash as createHash2 } from "node:crypto";
@@ -28277,7 +28398,6 @@ function lastTryOnExit() {
28277
28398
  }
28278
28399
 
28279
28400
  // src/headless.ts
28280
- var run3 = promisify3(execFile3);
28281
28401
  function snakeCased(params) {
28282
28402
  const result = emptyRecord();
28283
28403
  for (const [key, value] of Object.entries(params)) {
@@ -28286,34 +28406,58 @@ function snakeCased(params) {
28286
28406
  }
28287
28407
  return result;
28288
28408
  }
28289
- function lastJsonObject(stdout) {
28290
- const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("{"));
28291
- for (const line of lines.reverse()) {
28292
- try {
28293
- const parsed = JSON.parse(line);
28294
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
28295
- return parsed;
28296
- }
28297
- } catch {}
28298
- }
28299
- return null;
28409
+ function engineExtras(outcome) {
28410
+ return {
28411
+ ...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
28412
+ ...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
28413
+ ...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
28414
+ };
28300
28415
  }
28301
- function problems(stderr) {
28302
- const log = new GameLog;
28303
- log.append("stderr", stderr);
28304
- log.finish();
28305
- return log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
28416
+ var OWN_ERROR = "[ERROR] ";
28417
+ var MESSAGES_KEPT = 200;
28418
+ var ERRORS_NAMED = 3;
28419
+ function problemsIn(log) {
28420
+ return log.everything().flatMap((entry) => {
28421
+ if (entry.severity !== "info") {
28422
+ return [entry];
28423
+ }
28424
+ if (entry.source === "stderr" && entry.text.startsWith(OWN_ERROR)) {
28425
+ return [{ ...entry, severity: "error", text: entry.text.slice(OWN_ERROR.length) }];
28426
+ }
28427
+ return [];
28428
+ });
28429
+ }
28430
+ function reported(problems) {
28431
+ const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
28432
+ return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
28306
28433
  }
28307
- function reason(stdout, stderr) {
28308
- const own = stderr.split(/\r?\n/).filter((line) => line.startsWith("[ERROR] ")).map((line) => line.slice("[ERROR] ".length));
28434
+ function reason(log, problems) {
28435
+ const own = log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
28309
28436
  if (own.length > 0) {
28310
- return own.join("; ");
28311
- }
28312
- const engine = problems(stderr).filter((entry) => entry.severity === "error").map((entry) => entry.text);
28313
- if (engine.length > 0) {
28314
- return engine.join("; ");
28437
+ return own.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; ");
28438
+ }
28439
+ const errors = problems.filter((entry) => entry.severity === "error").map((entry) => entry.text);
28440
+ if (errors.length > 0) {
28441
+ const rest = errors.length - ERRORS_NAMED;
28442
+ return rest > 0 ? `${errors.slice(0, ERRORS_NAMED).join("; ")}; and ${rest} more error${rest === 1 ? "" : "s"}` : errors.join("; ");
28443
+ }
28444
+ const printed = log.everything().filter((entry) => entry.source === "stdout");
28445
+ return printed.at(-1)?.text ?? "no output at all";
28446
+ }
28447
+ function scriptFault(problems, script) {
28448
+ const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
28449
+ const directories = [...new Set([spelled(dirname7(script)), spelled(resolvedOr(dirname7(script)))])];
28450
+ return problems.find((entry) => {
28451
+ const at = entry.detail.find((line) => line.startsWith("at: "))?.replaceAll("\\", "/").toLowerCase();
28452
+ return entry.severity === "error" && at !== undefined && directories.some((directory) => at.includes(directory));
28453
+ });
28454
+ }
28455
+ function resolvedOr(path) {
28456
+ try {
28457
+ return realpathSync2.native(path);
28458
+ } catch {
28459
+ return path;
28315
28460
  }
28316
- return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
28317
28461
  }
28318
28462
  function librariesNotCopied(messages) {
28319
28463
  const found = new Set;
@@ -28353,30 +28497,33 @@ async function runImport(godotPath, projectPath, options = {}) {
28353
28497
  }
28354
28498
  async function importOnce(godotPath, projectPath) {
28355
28499
  const logDir = scratchDirectory("import");
28500
+ let ran;
28356
28501
  try {
28357
- const { stderr } = await run3(godotPath, ["--headless", "--log-file", join13(logDir, "engine.log"), "--path", projectPath, "--import"], { signal: callSignal() });
28358
- return { ok: true, messages: problems(stderr) };
28359
- } catch (error) {
28360
- if (error instanceof Error && "stdout" in error && "stderr" in error) {
28361
- const failed = error;
28362
- return {
28363
- ok: false,
28364
- message: `the import pass failed (exit ${failed.code ?? "unknown"}): ${reason(failed.stdout, failed.stderr)}`,
28365
- messages: problems(failed.stderr)
28366
- };
28367
- }
28368
- return {
28369
- ok: false,
28370
- message: `the import pass could not be run: ${error instanceof Error ? error.message : String(error)}`,
28371
- messages: []
28372
- };
28502
+ ran = await runEngine(godotPath, [
28503
+ "--headless",
28504
+ "--log-file",
28505
+ join13(logDir, "engine.log"),
28506
+ "--path",
28507
+ projectPath,
28508
+ "--import"
28509
+ ]);
28373
28510
  } finally {
28374
28511
  discard(logDir);
28375
28512
  }
28513
+ const problems = problemsIn(ran.log);
28514
+ if (ran.exitCode === 0 && ran.failure === null) {
28515
+ return { ok: true, ...reported(problems) };
28516
+ }
28517
+ return {
28518
+ ok: false,
28519
+ message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
28520
+ ...reported(problems)
28521
+ };
28376
28522
  }
28377
28523
  async function runOperation(engine, operation, params, projectPath) {
28378
28524
  const paramsDir = scratchDirectory("params");
28379
28525
  const paramsFile = join13(paramsDir, `${operation}.json`);
28526
+ const answerFile = join13(paramsDir, "answer.json");
28380
28527
  writeFileSync8(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
28381
28528
  const args = [
28382
28529
  "--headless",
@@ -28388,38 +28535,63 @@ async function runOperation(engine, operation, params, projectPath) {
28388
28535
  engine.script,
28389
28536
  operation,
28390
28537
  `@file:${paramsFile}`,
28538
+ answerFile,
28391
28539
  ...engine.debug ? ["--debug-godot"] : []
28392
28540
  ];
28393
- let stdout;
28394
- let stderr;
28541
+ let ran;
28542
+ let answer;
28395
28543
  try {
28396
- ({ stdout, stderr } = await run3(engine.godotPath, args, { signal: callSignal() }));
28397
- } catch (error) {
28398
- if (error instanceof Error && "stdout" in error && "stderr" in error) {
28399
- const failed = error;
28400
- return {
28401
- ok: false,
28402
- message: `${operation} failed (exit ${failed.code ?? "unknown"}): ${reason(failed.stdout, failed.stderr)}`,
28403
- messages: problems(failed.stderr)
28404
- };
28405
- }
28544
+ ran = await runEngine(engine.godotPath, args);
28545
+ answer = answerIn(answerFile);
28546
+ } finally {
28547
+ discard(paramsDir);
28548
+ }
28549
+ const problems = problemsIn(ran.log);
28550
+ if (answer.kind === "unreadable") {
28406
28551
  return {
28407
28552
  ok: false,
28408
- message: `${operation} could not be run: ${error instanceof Error ? error.message : String(error)}`,
28409
- messages: []
28553
+ message: `${operation} wrote an answer that could not be read (${answer.why})`,
28554
+ ...reported(problems)
28410
28555
  };
28411
- } finally {
28412
- discard(paramsDir);
28413
28556
  }
28414
- const payload = lastJsonObject(stdout);
28415
- if (payload === null) {
28557
+ if (answer.kind === "none") {
28558
+ const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
28416
28559
  return {
28417
28560
  ok: false,
28418
- message: `${operation} produced no result: ${reason(stdout, stderr)}`,
28419
- messages: problems(stderr)
28561
+ message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
28562
+ ...reported(problems)
28563
+ };
28564
+ }
28565
+ const fault = scriptFault(problems, engine.script);
28566
+ if (fault !== undefined) {
28567
+ return {
28568
+ ok: false,
28569
+ message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
28570
+ ...reported(problems)
28571
+ };
28572
+ }
28573
+ const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under engine_messages.`;
28574
+ return {
28575
+ ok: true,
28576
+ payload: answer.payload,
28577
+ ...reported(problems),
28578
+ ...afterAnswer === undefined ? {} : { afterAnswer }
28579
+ };
28580
+ }
28581
+ function answerIn(file) {
28582
+ if (!existsSync10(file)) {
28583
+ return { kind: "none" };
28584
+ }
28585
+ const text = readFileSync12(file, "utf8");
28586
+ try {
28587
+ const parsed = JSON.parse(text);
28588
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { kind: "answer", payload: parsed } : { kind: "unreadable", why: `it is JSON but not an object: ${text.slice(0, 200)}` };
28589
+ } catch (error) {
28590
+ return {
28591
+ kind: "unreadable",
28592
+ why: `${error instanceof Error ? error.message : String(error)}: ${text.slice(0, 200)}`
28420
28593
  };
28421
28594
  }
28422
- return { ok: true, payload, messages: problems(stderr) };
28423
28595
  }
28424
28596
 
28425
28597
  // src/headless-operations.ts
@@ -28620,13 +28792,17 @@ function parseJUnit(xml) {
28620
28792
  const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
28621
28793
  const suites = suiteElements.map((suite) => {
28622
28794
  const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
28795
+ const failures = cases.filter((entry) => entry.status === "failed").length;
28796
+ const errors = cases.filter((entry) => entry.status === "error").length;
28623
28797
  return {
28624
28798
  name: suite.attributes["name"] ?? "",
28625
28799
  path: pathOf(suite),
28626
28800
  tests: cases.length,
28627
28801
  discovered: count(suite.attributes, "tests"),
28628
- failures: cases.filter((entry) => entry.status === "failed").length,
28629
- errors: cases.filter((entry) => entry.status === "error").length,
28802
+ failures,
28803
+ errors,
28804
+ hookFailures: Math.max(0, count(suite.attributes, "failures") - failures),
28805
+ hookErrors: Math.max(0, count(suite.attributes, "errors") - errors),
28630
28806
  skipped: cases.filter((entry) => entry.status === "skipped").length,
28631
28807
  time: count(suite.attributes, "time"),
28632
28808
  cases
@@ -28637,13 +28813,22 @@ function parseJUnit(xml) {
28637
28813
  tests: all.length,
28638
28814
  failures: all.filter((entry) => entry.status === "failed").length,
28639
28815
  errors: all.filter((entry) => entry.status === "error").length,
28816
+ hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
28817
+ hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
28640
28818
  skipped: all.filter((entry) => entry.status === "skipped").length,
28641
28819
  time: suites.reduce((sum, suite) => sum + suite.time, 0),
28642
28820
  suites
28643
28821
  };
28644
28822
  }
28823
+ var HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
28824
+ function hookFailuresPrinted(printed) {
28825
+ return printed.flatMap((line) => {
28826
+ const found = HOOK_FAILURE.exec(line);
28827
+ return found === null ? [] : [{ hook: found[2] ?? "", path: found[3] ?? "", line: Number(found[4]), message: found[1] ?? "" }];
28828
+ });
28829
+ }
28645
28830
  var NOTHING_RAN = /no test cases found/i;
28646
- var NO_SUCH_PATH = /given directory or file does not exists:\s*(\S+)/i;
28831
+ var NO_SUCH_PATH = /given directory or file does not exists:\s*(.+?)\s*$/i;
28647
28832
  function whyNoReport(printed, asked) {
28648
28833
  const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
28649
28834
  if (missing !== undefined) {
@@ -28730,13 +28915,17 @@ function withActualsPrinted(failed, printed) {
28730
28915
  at: match[5] ?? "",
28731
28916
  ...readDiff(match[1] ?? "")
28732
28917
  }));
28918
+ const used = new Set;
28733
28919
  return failed.map((entry) => {
28734
28920
  if (entry.detail === null) {
28735
28921
  return entry;
28736
28922
  }
28737
28923
  let detail = entry.detail;
28738
28924
  let recovered = false;
28739
- for (const diff of diffs) {
28925
+ for (const [index, diff] of diffs.entries()) {
28926
+ if (used.has(index)) {
28927
+ continue;
28928
+ }
28740
28929
  const wrong = ` but was
28741
28930
  '${diff.merged.replaceAll("\r", "")}'${diff.suffix}
28742
28931
  at ${diff.at}`;
@@ -28745,6 +28934,7 @@ function withActualsPrinted(failed, printed) {
28745
28934
  '${diff.actual}'${diff.suffix}
28746
28935
  at ${diff.at}`);
28747
28936
  recovered = true;
28937
+ used.add(index);
28748
28938
  }
28749
28939
  }
28750
28940
  const quoted = QUOTED_EQUALITY.exec(detail);
@@ -28768,7 +28958,7 @@ function masked(expected, actual) {
28768
28958
  return `${expected}${actual}`.includes(MASKED_BRACKET) && allSpelled(expected) && allSpelled(actual);
28769
28959
  }
28770
28960
  var PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
28771
- var RUNNING_SUITE = /Run Test Suite:\s*(\S+)/;
28961
+ var RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
28772
28962
  var ORPHANS = /(\d+)\s+orphans/;
28773
28963
  var OVERALL = "Overall Summary";
28774
28964
  function orphansPrinted(printed) {
@@ -28803,13 +28993,24 @@ function orphansPrinted(printed) {
28803
28993
  }
28804
28994
 
28805
28995
  // src/lsp_client.ts
28806
- import { realpathSync as realpathSync2 } from "node:fs";
28996
+ import { existsSync as existsSync11, realpathSync as realpathSync3 } from "node:fs";
28807
28997
  import { readFile, realpath } from "node:fs/promises";
28808
28998
  import { createConnection as createConnection3 } from "node:net";
28809
- import { dirname as dirname7, resolve as resolve3 } from "node:path";
28999
+ import { dirname as dirname8, join as join14, resolve as resolve3 } from "node:path";
28810
29000
  import { fileURLToPath, pathToFileURL } from "node:url";
28811
29001
  var DEFAULT_LSP_PORT = 6005;
28812
29002
  var DIAGNOSTICS_TIMEOUT_MS = 5000;
29003
+
29004
+ class ArgumentRefusal extends Refusal {
29005
+ }
29006
+
29007
+ class AnotherProjectsServer extends Refusal {
29008
+ serves;
29009
+ constructor(port, serves, asked) {
29010
+ super(`The language server on port ${port} belongs to the editor of ${serves}, not ${asked}.`);
29011
+ this.serves = serves;
29012
+ }
29013
+ }
28813
29014
  function diagnosticsKey(uri) {
28814
29015
  let decoded;
28815
29016
  try {
@@ -28821,7 +29022,7 @@ function diagnosticsKey(uri) {
28821
29022
  return decoded;
28822
29023
  }
28823
29024
  try {
28824
- return realpathSync2.native(fileURLToPath(decoded)).toLowerCase();
29025
+ return realpathSync3.native(fileURLToPath(decoded)).toLowerCase();
28825
29026
  } catch {
28826
29027
  return decoded.toLowerCase();
28827
29028
  }
@@ -28838,8 +29039,10 @@ class GodotLSPClient {
28838
29039
  connectPromise = null;
28839
29040
  initialized = false;
28840
29041
  rootPath = null;
29042
+ servedWorkspace = null;
28841
29043
  diagnosticsWaiters = new Map;
28842
29044
  documentVersions = new Map;
29045
+ documentTurns = new Map;
28843
29046
  constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1") {
28844
29047
  this.port = port;
28845
29048
  this.host = host;
@@ -28936,7 +29139,7 @@ class GodotLSPClient {
28936
29139
  if (this.initialized) {
28937
29140
  return;
28938
29141
  }
28939
- const rootPath = this.rootPath ?? dirname7(resolve3(filePath));
29142
+ const rootPath = this.rootPath ?? dirname8(resolve3(filePath));
28940
29143
  await this.initialize(rootPath);
28941
29144
  }
28942
29145
  async sendRequest(method, params) {
@@ -29011,6 +29214,10 @@ class GodotLSPClient {
29011
29214
  }
29012
29215
  }
29013
29216
  }
29217
+ if (message["method"] === "gdscript_client/changeWorkspace") {
29218
+ const path = message["params"]?.["path"];
29219
+ this.servedWorkspace = typeof path === "string" ? path : null;
29220
+ }
29014
29221
  if (message["method"] === "textDocument/publishDiagnostics") {
29015
29222
  const params = message["params"];
29016
29223
  const paramsObject = params && typeof params === "object" ? params : null;
@@ -29113,10 +29320,14 @@ class GodotLSPClient {
29113
29320
  this.sendNotification("textDocument/didClose", { textDocument: { uri } });
29114
29321
  } catch {}
29115
29322
  }
29323
+ forgetServedWorkspace() {
29324
+ this.servedWorkspace = null;
29325
+ }
29116
29326
  async initialize(rootPath) {
29117
29327
  await this.ensureConnected();
29118
29328
  const resolvedRootPath = resolve3(rootPath);
29119
29329
  const rootUri = pathToFileURL(resolvedRootPath).href;
29330
+ this.forgetServedWorkspace();
29120
29331
  const result = await this.sendRequest("initialize", {
29121
29332
  processId: process.pid,
29122
29333
  rootPath: resolvedRootPath,
@@ -29142,60 +29353,82 @@ class GodotLSPClient {
29142
29353
  }
29143
29354
  ]
29144
29355
  });
29356
+ const served = this.servedWorkspace;
29357
+ if (served !== null && !isSameDirectory(served, resolvedRootPath)) {
29358
+ throw new AnotherProjectsServer(this.port, served, resolvedRootPath);
29359
+ }
29145
29360
  this.sendNotification("initialized", {});
29146
29361
  this.initialized = true;
29147
29362
  this.rootPath = resolvedRootPath;
29148
29363
  return result;
29149
29364
  }
29150
- async getDiagnostics(filePath, content) {
29151
- await this.ensureConnected();
29152
- await this.ensureInitializedForFile(filePath);
29365
+ async withDocument(filePath, work) {
29153
29366
  const key = diagnosticsKey(this.toFileUri(filePath));
29154
- const diagnosticsPromise = new Promise((resolveDiagnostics, rejectDiagnostics) => {
29155
- const existing = this.diagnosticsWaiters.get(key);
29156
- if (existing) {
29157
- clearTimeout(existing.timer);
29158
- }
29159
- const timer = setTimeout(() => {
29160
- this.diagnosticsWaiters.delete(key);
29161
- rejectDiagnostics(new Error(`Godot published no diagnostics for ${key} within ${DIAGNOSTICS_TIMEOUT_MS}ms. ` + "The language server may not be running, or may not have this file in its workspace."));
29162
- }, DIAGNOSTICS_TIMEOUT_MS);
29163
- this.diagnosticsWaiters.set(key, {
29164
- resolve: resolveDiagnostics,
29165
- reject: rejectDiagnostics,
29166
- timer
29167
- });
29168
- });
29169
- let opened = null;
29367
+ const before = this.documentTurns.get(key) ?? Promise.resolve();
29368
+ let done = () => {};
29369
+ const mine = new Promise((finished) => {
29370
+ done = finished;
29371
+ });
29372
+ const turn = before.then(() => mine);
29373
+ this.documentTurns.set(key, turn);
29374
+ await before;
29170
29375
  try {
29171
- opened = this.syncDocument(filePath, content);
29172
- return await diagnosticsPromise;
29173
- } catch (error) {
29174
- const waiter = this.diagnosticsWaiters.get(key);
29175
- if (waiter) {
29176
- clearTimeout(waiter.timer);
29177
- this.diagnosticsWaiters.delete(key);
29178
- }
29179
- throw error;
29376
+ return await work();
29180
29377
  } finally {
29181
- if (opened !== null) {
29182
- this.closeDocument(opened);
29378
+ done();
29379
+ if (this.documentTurns.get(key) === turn) {
29380
+ this.documentTurns.delete(key);
29183
29381
  }
29184
29382
  }
29185
29383
  }
29186
- async getCompletions(filePath, content, line, character) {
29384
+ async getDiagnostics(filePath, content) {
29187
29385
  await this.ensureConnected();
29188
29386
  await this.ensureInitializedForFile(filePath);
29189
- const uri = this.syncDocument(filePath, content);
29190
- let result;
29191
- try {
29192
- result = await this.sendRequest("textDocument/completion", {
29193
- textDocument: { uri },
29194
- position: { line, character }
29387
+ return this.withDocument(filePath, async () => {
29388
+ const key = diagnosticsKey(this.toFileUri(filePath));
29389
+ const diagnosticsPromise = new Promise((resolveDiagnostics, rejectDiagnostics) => {
29390
+ const timer = setTimeout(() => {
29391
+ this.diagnosticsWaiters.delete(key);
29392
+ rejectDiagnostics(new Error(`Godot published no diagnostics for ${key} within ${DIAGNOSTICS_TIMEOUT_MS}ms. ` + "The language server may not be running, or may not have this file in its workspace."));
29393
+ }, DIAGNOSTICS_TIMEOUT_MS);
29394
+ this.diagnosticsWaiters.set(key, {
29395
+ resolve: resolveDiagnostics,
29396
+ reject: rejectDiagnostics,
29397
+ timer
29398
+ });
29195
29399
  });
29196
- } finally {
29197
- this.closeDocument(uri);
29198
- }
29400
+ let opened = null;
29401
+ try {
29402
+ opened = this.syncDocument(filePath, content);
29403
+ return await diagnosticsPromise;
29404
+ } catch (error) {
29405
+ const waiter = this.diagnosticsWaiters.get(key);
29406
+ if (waiter) {
29407
+ clearTimeout(waiter.timer);
29408
+ this.diagnosticsWaiters.delete(key);
29409
+ }
29410
+ throw error;
29411
+ } finally {
29412
+ if (opened !== null) {
29413
+ this.closeDocument(opened);
29414
+ }
29415
+ }
29416
+ });
29417
+ }
29418
+ async getCompletions(filePath, content, line, character) {
29419
+ await this.ensureConnected();
29420
+ await this.ensureInitializedForFile(filePath);
29421
+ const result = await this.withDocument(filePath, async () => {
29422
+ const uri = this.syncDocument(filePath, content);
29423
+ try {
29424
+ return await this.sendRequest("textDocument/completion", {
29425
+ textDocument: { uri },
29426
+ position: { line, character }
29427
+ });
29428
+ } finally {
29429
+ this.closeDocument(uri);
29430
+ }
29431
+ });
29199
29432
  if (Array.isArray(result)) {
29200
29433
  return result;
29201
29434
  }
@@ -29210,28 +29443,31 @@ class GodotLSPClient {
29210
29443
  async getHover(filePath, content, line, character) {
29211
29444
  await this.ensureConnected();
29212
29445
  await this.ensureInitializedForFile(filePath);
29213
- const uri = this.syncDocument(filePath, content);
29214
- try {
29215
- return await this.sendRequest("textDocument/hover", {
29216
- textDocument: { uri },
29217
- position: { line, character }
29218
- });
29219
- } finally {
29220
- this.closeDocument(uri);
29221
- }
29446
+ return this.withDocument(filePath, async () => {
29447
+ const uri = this.syncDocument(filePath, content);
29448
+ try {
29449
+ return await this.sendRequest("textDocument/hover", {
29450
+ textDocument: { uri },
29451
+ position: { line, character }
29452
+ });
29453
+ } finally {
29454
+ this.closeDocument(uri);
29455
+ }
29456
+ });
29222
29457
  }
29223
29458
  async getDocumentSymbols(filePath, content) {
29224
29459
  await this.ensureConnected();
29225
29460
  await this.ensureInitializedForFile(filePath);
29226
- const uri = this.syncDocument(filePath, content);
29227
- let result;
29228
- try {
29229
- result = await this.sendRequest("textDocument/documentSymbol", {
29230
- textDocument: { uri }
29231
- });
29232
- } finally {
29233
- this.closeDocument(uri);
29234
- }
29461
+ const result = await this.withDocument(filePath, async () => {
29462
+ const uri = this.syncDocument(filePath, content);
29463
+ try {
29464
+ return await this.sendRequest("textDocument/documentSymbol", {
29465
+ textDocument: { uri }
29466
+ });
29467
+ } finally {
29468
+ this.closeDocument(uri);
29469
+ }
29470
+ });
29235
29471
  return Array.isArray(result) ? result : [];
29236
29472
  }
29237
29473
  isConnected() {
@@ -29263,36 +29499,39 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
29263
29499
  try {
29264
29500
  projectPath = await realpath(requestedProjectPath);
29265
29501
  } catch {
29266
- throw new Refusal(`Project path does not exist: ${requestedProjectPath}`);
29502
+ throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
29503
+ }
29504
+ if (!existsSync11(join14(projectPath, "project.godot"))) {
29505
+ throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
29267
29506
  }
29268
29507
  const contained = resolveWithinProject(projectPath, scriptPathValue);
29269
29508
  if (!contained.ok) {
29270
- throw new Refusal(contained.reason);
29509
+ throw new ArgumentRefusal(contained.reason);
29271
29510
  }
29272
29511
  let scriptPath;
29273
29512
  try {
29274
29513
  scriptPath = await realpath(contained.absolutePath);
29275
29514
  } catch {
29276
- throw new Refusal(`Script file does not exist: ${contained.absolutePath}`);
29515
+ throw new ArgumentRefusal(`Script file does not exist: ${contained.absolutePath}`);
29277
29516
  }
29278
29517
  if (!isWithinRoot(projectPath, scriptPath)) {
29279
- throw new Refusal("scriptPath resolves outside the project root boundary.");
29518
+ throw new ArgumentRefusal("scriptPath resolves outside the project root boundary.");
29280
29519
  }
29281
29520
  return { projectPath, scriptPath };
29282
29521
  }
29283
29522
  async function handleLSPTool(client, toolName, args) {
29284
29523
  try {
29285
29524
  if (!args || typeof args !== "object") {
29286
- throw new Refusal("Tool arguments must be an object.");
29525
+ throw new ArgumentRefusal("Tool arguments must be an object.");
29287
29526
  }
29288
29527
  const parsedArgs = args;
29289
29528
  const projectPathValue = parsedArgs["projectPath"];
29290
29529
  const scriptPathValue = parsedArgs["scriptPath"];
29291
29530
  if (typeof projectPathValue !== "string" || projectPathValue.length === 0) {
29292
- throw new Refusal("Missing required argument: projectPath");
29531
+ throw new ArgumentRefusal("Missing required argument: projectPath");
29293
29532
  }
29294
29533
  if (typeof scriptPathValue !== "string" || scriptPathValue.length === 0) {
29295
- throw new Refusal("Missing required argument: scriptPath");
29534
+ throw new ArgumentRefusal("Missing required argument: scriptPath");
29296
29535
  }
29297
29536
  const { projectPath, scriptPath } = await resolveLSPPaths(projectPathValue, scriptPathValue);
29298
29537
  const content = await readFile(scriptPath, "utf8");
@@ -29306,7 +29545,7 @@ async function handleLSPTool(client, toolName, args) {
29306
29545
  const line = Number(parsedArgs["line"]);
29307
29546
  const character = Number(parsedArgs["character"]);
29308
29547
  if (!Number.isFinite(line) || !Number.isFinite(character)) {
29309
- throw new Refusal("Arguments line and character must be numbers.");
29548
+ throw new ArgumentRefusal("Arguments line and character must be numbers.");
29310
29549
  }
29311
29550
  const completions = await client.getCompletions(scriptPath, content, line, character);
29312
29551
  return asToolResponse({ completions });
@@ -29315,7 +29554,7 @@ async function handleLSPTool(client, toolName, args) {
29315
29554
  const line = Number(parsedArgs["line"]);
29316
29555
  const character = Number(parsedArgs["character"]);
29317
29556
  if (!Number.isFinite(line) || !Number.isFinite(character)) {
29318
- throw new Refusal("Arguments line and character must be numbers.");
29557
+ throw new ArgumentRefusal("Arguments line and character must be numbers.");
29319
29558
  }
29320
29559
  const hover = await client.getHover(scriptPath, content, line, character);
29321
29560
  return asToolResponse({ hover });
@@ -29329,13 +29568,15 @@ async function handleLSPTool(client, toolName, args) {
29329
29568
  }
29330
29569
  } catch (error) {
29331
29570
  return asToolResponse({
29332
- error: normalizeLSPError(error, client.port)
29571
+ error: normalizeLSPError(error, client.port),
29572
+ ...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
29573
+ ...error instanceof ArgumentRefusal ? { refusedArguments: true } : {}
29333
29574
  });
29334
29575
  }
29335
29576
  }
29336
29577
 
29337
29578
  // src/outside.ts
29338
- import { spawn as spawn2 } from "node:child_process";
29579
+ import { spawn as spawn3 } from "node:child_process";
29339
29580
  import { fileURLToPath as fileURLToPath2 } from "node:url";
29340
29581
  function changesFrom(env, base) {
29341
29582
  const changes = {};
@@ -29369,7 +29610,7 @@ function readLaunched(printed) {
29369
29610
  }
29370
29611
  async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
29371
29612
  return await new Promise((resolve) => {
29372
- const launcher = spawn2(runtime, [keeper, "launch"], {
29613
+ const launcher = spawn3(runtime, [keeper, "launch"], {
29373
29614
  stdio: ["pipe", "pipe", "pipe"],
29374
29615
  windowsHide: true
29375
29616
  });
@@ -29400,9 +29641,9 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
29400
29641
  }
29401
29642
 
29402
29643
  // src/process-time.ts
29403
- import { execFile as execFile4 } from "node:child_process";
29404
- import { promisify as promisify4 } from "node:util";
29405
- var run4 = promisify4(execFile4);
29644
+ import { execFile as execFile3 } from "node:child_process";
29645
+ import { promisify as promisify3 } from "node:util";
29646
+ var run3 = promisify3(execFile3);
29406
29647
  var ASK_TIMEOUT_MS2 = 6000;
29407
29648
  async function cpuSecondsOf(pid) {
29408
29649
  try {
@@ -29417,7 +29658,7 @@ async function secondsOnWindows(pid) {
29417
29658
  return Number.isFinite(seconds) ? seconds : undefined;
29418
29659
  }
29419
29660
  async function askPosix2(pid) {
29420
- const { stdout } = await run4("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
29661
+ const { stdout } = await run3("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
29421
29662
  return secondsFromClock(stdout.trim());
29422
29663
  }
29423
29664
  function secondsFromClock(printed) {
@@ -29437,14 +29678,14 @@ function secondsFromClock(printed) {
29437
29678
  }
29438
29679
 
29439
29680
  // src/project-scan.ts
29440
- import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync12 } from "node:fs";
29441
- import { join as join14 } from "node:path";
29681
+ import { existsSync as existsSync12, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
29682
+ import { join as join15 } from "node:path";
29442
29683
  var SKIPPED = new Set(["node_modules"]);
29443
29684
  function skipped(name) {
29444
29685
  return name.startsWith(".") || SKIPPED.has(name);
29445
29686
  }
29446
29687
  function steppedOver(directory) {
29447
- return existsSync10(join14(directory, ".gdignore"));
29688
+ return existsSync12(join15(directory, ".gdignore"));
29448
29689
  }
29449
29690
  var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
29450
29691
  function projectStructure(projectPath) {
@@ -29458,7 +29699,7 @@ function projectStructure(projectPath) {
29458
29699
  continue;
29459
29700
  }
29460
29701
  if (entry.isDirectory()) {
29461
- visit(join14(directory, entry.name));
29702
+ visit(join15(directory, entry.name));
29462
29703
  } else if (entry.isFile()) {
29463
29704
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
29464
29705
  if (extension === "tscn") {
@@ -29489,14 +29730,14 @@ function scriptsWithoutUid(projectPath) {
29489
29730
  }
29490
29731
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
29491
29732
  if (entry.isDirectory()) {
29492
- visit(join14(directory, entry.name), spelled);
29733
+ visit(join15(directory, entry.name), spelled);
29493
29734
  continue;
29494
29735
  }
29495
29736
  if (!entry.isFile()) {
29496
29737
  continue;
29497
29738
  }
29498
29739
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
29499
- if (CARRIES_A_UID.has(extension) && !existsSync10(join14(directory, `${entry.name}.uid`))) {
29740
+ if (CARRIES_A_UID.has(extension) && !existsSync12(join15(directory, `${entry.name}.uid`))) {
29500
29741
  found.push(spelled);
29501
29742
  }
29502
29743
  }
@@ -29531,7 +29772,7 @@ function searchProject(projectPath, options) {
29531
29772
  if (skipped(entry.name)) {
29532
29773
  continue;
29533
29774
  }
29534
- const entryPath = join14(directory, entry.name);
29775
+ const entryPath = join15(directory, entry.name);
29535
29776
  if (entry.isDirectory()) {
29536
29777
  visit(entryPath);
29537
29778
  continue;
@@ -29542,7 +29783,7 @@ function searchProject(projectPath, options) {
29542
29783
  }
29543
29784
  result.summary.files_searched += 1;
29544
29785
  const matches = [];
29545
- for (const [index, line] of readFileSync12(entryPath, "utf8").split(`
29786
+ for (const [index, line] of readFileSync13(entryPath, "utf8").split(`
29546
29787
  `).entries()) {
29547
29788
  if (full()) {
29548
29789
  break;
@@ -29565,7 +29806,7 @@ function searchProject(projectPath, options) {
29565
29806
  }
29566
29807
 
29567
29808
  // src/resources.ts
29568
- import { readFileSync as readFileSync13 } from "node:fs";
29809
+ import { readFileSync as readFileSync14 } from "node:fs";
29569
29810
  import { extname, resolve as resolve4 } from "node:path";
29570
29811
  var STATIC_RESOURCES = [
29571
29812
  {
@@ -29760,7 +30001,7 @@ function readResourceText(uri, getProjectPath) {
29760
30001
  const parsedUri = parseGodotUri(uri);
29761
30002
  if (parsedUri.kind === "project-info") {
29762
30003
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
29763
- const rawProject = readFileSync13(projectFilePath, "utf-8");
30004
+ const rawProject = readFileSync14(projectFilePath, "utf-8");
29764
30005
  const parsedProject = parseProjectGodot(rawProject);
29765
30006
  return {
29766
30007
  mimeType: "application/json",
@@ -29769,7 +30010,7 @@ function readResourceText(uri, getProjectPath) {
29769
30010
  }
29770
30011
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
29771
30012
  ensureAllowedExtension(parsedUri.kind, filePath);
29772
- const text = readFileSync13(filePath, "utf-8");
30013
+ const text = readFileSync14(filePath, "utf-8");
29773
30014
  return {
29774
30015
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
29775
30016
  text
@@ -29806,15 +30047,15 @@ function setupResourceHandlers(mcp, getProjectPath) {
29806
30047
  }
29807
30048
 
29808
30049
  // src/restart-note.ts
29809
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
29810
- import { dirname as dirname8, join as join15 } from "node:path";
30050
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync15, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
30051
+ import { dirname as dirname9, join as join16 } from "node:path";
29811
30052
  function restartNotePath(projectPath) {
29812
- return join15(projectPath, ".godot", "gdharness-restart.json");
30053
+ return join16(projectPath, ".godot", "gdharness-restart.json");
29813
30054
  }
29814
30055
  function noteRestartBegun(note) {
29815
30056
  const path = restartNotePath(note.projectPath);
29816
30057
  try {
29817
- mkdirSync7(dirname8(path), { recursive: true });
30058
+ mkdirSync7(dirname9(path), { recursive: true });
29818
30059
  writeFileSync9(path, `${JSON.stringify(note, null, 2)}
29819
30060
  `, "utf8");
29820
30061
  } catch (error) {
@@ -29823,7 +30064,7 @@ function noteRestartBegun(note) {
29823
30064
  }
29824
30065
  function restartOwed(projectPath) {
29825
30066
  try {
29826
- const parsed = JSON.parse(readFileSync14(restartNotePath(projectPath), "utf8"));
30067
+ const parsed = JSON.parse(readFileSync15(restartNotePath(projectPath), "utf8"));
29827
30068
  if (typeof parsed !== "object" || parsed === null) {
29828
30069
  return null;
29829
30070
  }
@@ -29845,32 +30086,32 @@ function restartSettled(projectPath) {
29845
30086
  }
29846
30087
 
29847
30088
  // src/run-record.ts
29848
- import { execFile as execFile5 } from "node:child_process";
30089
+ import { execFile as execFile4 } from "node:child_process";
29849
30090
  import { createHash as createHash3 } from "node:crypto";
29850
30091
  import {
29851
30092
  mkdirSync as mkdirSync8,
29852
30093
  openSync as openSync2,
29853
30094
  readdirSync as readdirSync6,
29854
- readFileSync as readFileSync15,
29855
- realpathSync as realpathSync3,
30095
+ readFileSync as readFileSync16,
30096
+ realpathSync as realpathSync4,
29856
30097
  rmSync as rmSync7,
29857
30098
  statSync as statSync6,
29858
30099
  writeFileSync as writeFileSync10
29859
30100
  } from "node:fs";
29860
- import { basename as basename3, dirname as dirname9, join as join16, resolve as resolve5 } from "node:path";
29861
- import { promisify as promisify5 } from "node:util";
29862
- var execFileAsync = promisify5(execFile5);
30101
+ import { basename as basename3, dirname as dirname10, join as join17, resolve as resolve5 } from "node:path";
30102
+ import { promisify as promisify4 } from "node:util";
30103
+ var execFileAsync = promisify4(execFile4);
29863
30104
  function runsDirectory() {
29864
- return join16(runtimeDirectory(), "runs");
30105
+ return join17(runtimeDirectory(), "runs");
29865
30106
  }
29866
30107
  function realAsFarAsItExists(path) {
29867
30108
  const missing = [];
29868
30109
  let at = path;
29869
30110
  for (;; ) {
29870
30111
  try {
29871
- return join16(realpathSync3.native(at), ...missing);
30112
+ return join17(realpathSync4.native(at), ...missing);
29872
30113
  } catch {
29873
- const parent = dirname9(at);
30114
+ const parent = dirname10(at);
29874
30115
  if (parent === at) {
29875
30116
  return path;
29876
30117
  }
@@ -29885,16 +30126,19 @@ function projectKey(projectPath) {
29885
30126
  return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
29886
30127
  }
29887
30128
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
29888
- return join16(directory, "runs", `note-${projectKey(projectPath)}.json`);
30129
+ return join17(directory, "runs", `note-${projectKey(projectPath)}.json`);
29889
30130
  }
29890
30131
  var SHARED_NOTE = "run.json";
29891
30132
  function openTranscript(startedAt) {
29892
30133
  mkdirSync8(runsDirectory(), { recursive: true });
29893
- const path = join16(runsDirectory(), `run-${startedAt}.log`);
30134
+ const path = join17(runsDirectory(), `run-${startedAt}.log`);
29894
30135
  return { path, fd: openSync2(path, "a") };
29895
30136
  }
30137
+ function noteIsAbout(note, playing) {
30138
+ return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
30139
+ }
29896
30140
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
29897
- return join16(directory, "runs", `editor-${projectKey(projectPath)}.json`);
30141
+ return join17(directory, "runs", `editor-${projectKey(projectPath)}.json`);
29898
30142
  }
29899
30143
  function writeEditorRunNote(note) {
29900
30144
  mkdirSync8(runsDirectory(), { recursive: true });
@@ -29902,7 +30146,7 @@ function writeEditorRunNote(note) {
29902
30146
  }
29903
30147
  function readEditorRunNote(projectPath) {
29904
30148
  for (const directory of runtimeDirectories()) {
29905
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join16(directory, "runs", "editor-run.json"));
30149
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join17(directory, "runs", "editor-run.json"));
29906
30150
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
29907
30151
  return note;
29908
30152
  }
@@ -29912,7 +30156,7 @@ function readEditorRunNote(projectPath) {
29912
30156
  function editorNoteAt(path) {
29913
30157
  let parsed;
29914
30158
  try {
29915
- parsed = JSON.parse(readFileSync15(path, "utf8"));
30159
+ parsed = JSON.parse(readFileSync16(path, "utf8"));
29916
30160
  } catch {
29917
30161
  return null;
29918
30162
  }
@@ -29926,7 +30170,9 @@ function editorNoteAt(path) {
29926
30170
  if (typeof projectPath !== "string" || typeof transcript !== "string" || typeof startedAt !== "number") {
29927
30171
  return null;
29928
30172
  }
29929
- return { projectPath, transcript, startedAt };
30173
+ const play = fields["play"];
30174
+ const which = typeof play === "object" && play !== null && typeof play["play"] === "number" && typeof play["editorPid"] === "number" ? play : undefined;
30175
+ return { projectPath, transcript, startedAt, ...which === undefined ? {} : { play: which } };
29930
30176
  }
29931
30177
  function readRunRecord(projectPath) {
29932
30178
  for (const directory of runtimeDirectories()) {
@@ -29934,7 +30180,7 @@ function readRunRecord(projectPath) {
29934
30180
  if (own !== null) {
29935
30181
  return own;
29936
30182
  }
29937
- const shared = recordAt(join16(directory, "runs", SHARED_NOTE));
30183
+ const shared = recordAt(join17(directory, "runs", SHARED_NOTE));
29938
30184
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
29939
30185
  return shared;
29940
30186
  }
@@ -29946,13 +30192,13 @@ function everyRunRecord() {
29946
30192
  for (const directory of runtimeDirectories()) {
29947
30193
  let entries;
29948
30194
  try {
29949
- entries = readdirSync6(join16(directory, "runs"));
30195
+ entries = readdirSync6(join17(directory, "runs"));
29950
30196
  } catch {
29951
30197
  continue;
29952
30198
  }
29953
30199
  for (const entry of entries) {
29954
30200
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
29955
- const record = recordAt(join16(directory, "runs", entry));
30201
+ const record = recordAt(join17(directory, "runs", entry));
29956
30202
  if (record !== null) {
29957
30203
  found.push(record);
29958
30204
  }
@@ -29964,7 +30210,7 @@ function everyRunRecord() {
29964
30210
  function recordAt(path) {
29965
30211
  let parsed;
29966
30212
  try {
29967
- parsed = JSON.parse(readFileSync15(path, "utf8"));
30213
+ parsed = JSON.parse(readFileSync16(path, "utf8"));
29968
30214
  } catch {
29969
30215
  return null;
29970
30216
  }
@@ -30010,7 +30256,7 @@ async function runningAs(pid) {
30010
30256
  const began = await startedAt(pid, null);
30011
30257
  const when = began === null ? {} : { startedAt: began };
30012
30258
  if (process.platform === "linux") {
30013
- const raw = readFileSync15(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
30259
+ const raw = readFileSync16(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
30014
30260
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
30015
30261
  }
30016
30262
  const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
@@ -30139,7 +30385,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
30139
30385
  }
30140
30386
  function clearRunRecord(projectPath) {
30141
30387
  for (const directory of runtimeDirectories()) {
30142
- const shared = join16(directory, "runs", SHARED_NOTE);
30388
+ const shared = join17(directory, "runs", SHARED_NOTE);
30143
30389
  const sharedRecord = recordAt(shared);
30144
30390
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
30145
30391
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
@@ -30169,7 +30415,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
30169
30415
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
30170
30416
  continue;
30171
30417
  }
30172
- const path = join16(directory, entry);
30418
+ const path = join17(directory, entry);
30173
30419
  if (kept.has(path) || ageMs(path, now) < keepMs) {
30174
30420
  continue;
30175
30421
  }
@@ -30183,16 +30429,16 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
30183
30429
  import { createHash as createHash4 } from "node:crypto";
30184
30430
  import {
30185
30431
  cpSync,
30186
- existsSync as existsSync11,
30432
+ existsSync as existsSync13,
30187
30433
  mkdirSync as mkdirSync9,
30188
30434
  readdirSync as readdirSync7,
30189
- readFileSync as readFileSync16,
30435
+ readFileSync as readFileSync17,
30190
30436
  rmdirSync,
30191
30437
  rmSync as rmSync8,
30192
30438
  statSync as statSync7,
30193
30439
  writeFileSync as writeFileSync11
30194
30440
  } from "node:fs";
30195
- import { dirname as dirname10, join as join17 } from "node:path";
30441
+ import { dirname as dirname11, join as join18 } from "node:path";
30196
30442
  import { fileURLToPath as fileURLToPath3 } from "node:url";
30197
30443
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
30198
30444
  var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
@@ -30209,21 +30455,21 @@ function editorAddonDigest(from = shippedAddonsDirectory()) {
30209
30455
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
30210
30456
  continue;
30211
30457
  }
30212
- const path = join17(directory, name);
30458
+ const path = join18(directory, name);
30213
30459
  const inside = `${relative}/${name}`;
30214
30460
  if (statSync7(path).isDirectory()) {
30215
30461
  walk(path, inside);
30216
30462
  } else {
30217
30463
  hash.update(`${inside}
30218
30464
  `);
30219
- hash.update(readFileSync16(path));
30465
+ hash.update(readFileSync17(path));
30220
30466
  hash.update(`
30221
30467
  `);
30222
30468
  }
30223
30469
  }
30224
30470
  };
30225
30471
  for (const name of EDITOR_PLUGINS) {
30226
- walk(join17(from, name), name);
30472
+ walk(join18(from, name), name);
30227
30473
  }
30228
30474
  return hash.digest("hex");
30229
30475
  }
@@ -30239,14 +30485,14 @@ function shippedEditorDigest() {
30239
30485
  return shippedDigest ?? undefined;
30240
30486
  }
30241
30487
  function shippedAddonsDirectory() {
30242
- return join17(dirname10(fileURLToPath3(import.meta.url)), "godot", "addons");
30488
+ return join18(dirname11(fileURLToPath3(import.meta.url)), "godot", "addons");
30243
30489
  }
30244
30490
  function installedAddonVersion(projectPath) {
30245
- const marker = join17(projectPath, "addons", ADDONS[0], VERSION_MARKER);
30246
- if (!existsSync11(marker)) {
30491
+ const marker = join18(projectPath, "addons", ADDONS[0], VERSION_MARKER);
30492
+ if (!existsSync13(marker)) {
30247
30493
  return null;
30248
30494
  }
30249
- const said = readFileSync16(marker, "utf8").trim();
30495
+ const said = readFileSync17(marker, "utf8").trim();
30250
30496
  return said === "" ? null : said;
30251
30497
  }
30252
30498
 
@@ -30299,7 +30545,7 @@ var NODE_PATH = {
30299
30545
  };
30300
30546
  var PROPERTIES = {
30301
30547
  type: "object",
30302
- description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another.',
30548
+ description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds.',
30303
30549
  additionalProperties: true
30304
30550
  };
30305
30551
  var XY = {
@@ -30454,11 +30700,18 @@ var TOOL_SPECS = [
30454
30700
  },
30455
30701
  value: {
30456
30702
  blank: true,
30457
- description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}. "" writes an empty string.'
30703
+ description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}, and infinity as {"_type": "float", "value": "inf"}, the form a read answers with. "" writes an empty string.'
30458
30704
  },
30459
30705
  name: { type: "string", description: "Autoload name." },
30460
30706
  path: { type: "string", description: "Autoload script or scene inside the project." },
30461
- enabled: { type: "boolean", description: "Autoloads: register enabled. Default true." },
30707
+ enabled: {
30708
+ type: "boolean",
30709
+ description: "Autoloads: Godot 4 loads every autoload in the list, so false is refused. Use global: false to keep the name out of the global scope, or remove_autoload to stop it loading."
30710
+ },
30711
+ global: {
30712
+ type: "boolean",
30713
+ description: `Autoloads: make the name a global variable, the * in project.godot and the editor's "Global Variable" column. Default true. The autoload loads either way.`
30714
+ },
30462
30715
  scenePath: SCENE_PATH,
30463
30716
  actionName: { type: "string", description: 'Input action name, such as "jump".' },
30464
30717
  events: INPUT_EVENTS,
@@ -30470,9 +30723,15 @@ var TOOL_SPECS = [
30470
30723
  },
30471
30724
  pluginName: { type: "string", description: "Folder name under addons/." },
30472
30725
  busName: { type: "string", description: "Audio bus name." },
30473
- parentBusIndex: { type: "number", description: "Audio buses: the bus to send to. Default 0, Master." },
30726
+ parentBusIndex: {
30727
+ type: "number",
30728
+ description: "Audio buses: the bus to send to. Default 0, Master. The new bus goes right after it, so every later bus moves one along, and the answer lists them all."
30729
+ },
30474
30730
  busIndex: { type: "number", description: "Audio bus index." },
30475
- effectIndex: { type: "number", description: "Slot on the bus for the effect." },
30731
+ effectIndex: {
30732
+ type: "number",
30733
+ description: "Slot on the bus for the effect: the effect there is replaced, and one past the last adds one. Any other slot is refused."
30734
+ },
30476
30735
  effectType: { type: "string", description: 'Effect class, such as "AudioEffectReverb".' },
30477
30736
  volumeDb: { type: "number", description: "Bus volume in decibels." }
30478
30737
  },
@@ -30491,7 +30750,7 @@ var TOOL_SPECS = [
30491
30750
  disable_plugin: { summary: "disable an addon", requires: ["pluginName"] },
30492
30751
  add_audio_bus: { summary: "add an audio bus", requires: ["busName"] },
30493
30752
  set_audio_bus_effect: {
30494
- summary: "add or configure an effect on a bus",
30753
+ summary: "put an effect in a slot on a bus",
30495
30754
  requires: ["busIndex", "effectIndex", "effectType"]
30496
30755
  },
30497
30756
  set_audio_bus_volume: { summary: "set a bus volume", requires: ["busIndex", "volumeDb"] }
@@ -30530,7 +30789,11 @@ var TOOL_SPECS = [
30530
30789
  type: "boolean",
30531
30790
  description: "forward: include the engine's own res://. resources. Default false."
30532
30791
  },
30533
- fileTypes: { type: "array", items: { type: "string" }, description: "reverse: extensions to look in." }
30792
+ fileTypes: {
30793
+ type: "array",
30794
+ items: { type: "string" },
30795
+ description: 'reverse: extensions to look in, plus "godot" for project.godot, "cfg" for plugin.cfg and export_presets.cfg, and "import" for import sidecars. Default all of tscn, tres, gd, gdshader, godot, cfg and import, since an autoload, the main scene and a plugin script are named only in the settings files.'
30796
+ }
30534
30797
  },
30535
30798
  requires: ["projectPath", "resourcePath"]
30536
30799
  },
@@ -30575,7 +30838,10 @@ var TOOL_SPECS = [
30575
30838
  summary: "reimport what status says is outdated or failed, one resource with resourcePath, or what is current as well with force. Through the open editor when one serves the project, so it reloads what it holds, waiting for it to finish; otherwise through the engine's own import pass. Answered from a second status read: reimported lists what is current now, notReimported what is not, with its status and reason, and via says which of the two did the work. stillImporting is an editor that had not finished within timeoutMs",
30576
30839
  requires: []
30577
30840
  },
30578
- uid: { summary: "the UID of one file", requires: ["resourcePath"] },
30841
+ uid: {
30842
+ summary: "the UID of one file and where it was read: a script's or shader's .uid file, an imported file's .import, or a scene's or resource's own header",
30843
+ requires: ["resourcePath"]
30844
+ },
30579
30845
  refresh_uids: {
30580
30846
  summary: "import the project so every script and shader has its .uid sidecar, and name under uidsCreated the ones this made and under stillWithoutUid the ones the engine would not import: it writes no scene and no script, so a project whose sidecars are all present is left untouched",
30581
30847
  requires: []
@@ -30636,11 +30902,11 @@ var TOOL_SPECS = [
30636
30902
  },
30637
30903
  {
30638
30904
  name: "scene_create",
30639
- description: "Creates a scene file, saves one, or saves a copy under a new path. Needs the editor connected.",
30905
+ description: "Creates a scene file where none is, saves one, or saves a copy under a new path. A save of a scene open in the editor saves the editor's copy, unsaved changes included. Needs the editor connected.",
30640
30906
  parameters: {
30641
30907
  projectPath: PROJECT_PATH,
30642
30908
  scenePath: SCENE_PATH,
30643
- rootNodeType: { type: "string", description: "create: the root node class. Default Node2D." },
30909
+ rootNodeType: { type: "string", description: "create: the root node class. Default Node." },
30644
30910
  newPath: { type: "string", description: "save_as: where the copy goes." }
30645
30911
  },
30646
30912
  requires: ["projectPath", "scenePath"],
@@ -30664,7 +30930,7 @@ var TOOL_SPECS = [
30664
30930
  },
30665
30931
  {
30666
30932
  name: "scene_node",
30667
- description: "One node in a scene file: add, read, set, duplicate, reparent or delete it, or paint TileMap cells. Any ClassDB node type can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties. A property holding a Resource takes the res:// path of one, so a texture, a material or a theme is a set like any other. Needs the editor connected.",
30933
+ description: "One node in a scene file: add, read, set, duplicate, reparent or delete it, or paint TileMap cells. Any ClassDB node type can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties. A property holding a Resource takes the res:// path of one, so a texture, a material or a theme is a set like any other. A write is refused while the editor holds unsaved changes to the scene, and for a node that belongs to a scene this one instances or inherits. Needs the editor connected.",
30668
30934
  parameters: {
30669
30935
  projectPath: PROJECT_PATH,
30670
30936
  scenePath: SCENE_PATH,
@@ -30682,7 +30948,10 @@ var TOOL_SPECS = [
30682
30948
  type: "boolean",
30683
30949
  description: "get: include properties still at their default. Default false."
30684
30950
  },
30685
- layer: { type: "number", description: "set_tilemap_cells: the TileMap layer. Default 0." },
30951
+ layer: {
30952
+ type: "number",
30953
+ description: "set_tilemap_cells: the layer of a TileMap. Default 0. A TileMapLayer is one layer."
30954
+ },
30686
30955
  cells: TILEMAP_CELLS
30687
30956
  },
30688
30957
  requires: ["projectPath", "scenePath"],
@@ -30693,7 +30962,10 @@ var TOOL_SPECS = [
30693
30962
  duplicate: { summary: "copy a node and its children", requires: ["nodePath", "newName"] },
30694
30963
  reparent: { summary: "move a node under another parent", requires: ["nodePath", "newParentPath"] },
30695
30964
  delete: { summary: "remove a node and its children", requires: ["nodePath"] },
30696
- set_tilemap_cells: { summary: "place tiles in a TileMap", requires: ["nodePath", "cells"] }
30965
+ set_tilemap_cells: {
30966
+ summary: "place tiles in a TileMapLayer or TileMap",
30967
+ requires: ["nodePath", "cells"]
30968
+ }
30697
30969
  }
30698
30970
  },
30699
30971
  {
@@ -30781,13 +31053,16 @@ var TOOL_SPECS = [
30781
31053
  },
30782
31054
  {
30783
31055
  name: "script_edit",
30784
- description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. create loads what it wrote under the project's own warning settings and answers with parses; the engine's reasons for a refusal come back under engine_messages.",
31056
+ description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read, and a script declaring a class_name is put in the project's class list, with registered saying whether it is there. modify makes every addition or none, and answers with the line each one is on in the file written.",
30785
31057
  parameters: {
30786
31058
  projectPath: PROJECT_PATH,
30787
31059
  scriptPath: SCRIPT_PATH,
30788
- className: { type: "string", description: "create: a class_name for the script." },
30789
- extends: { type: "string", description: "create: the base class. Default Node." },
30790
- content: { type: "string", description: "create: the whole file, instead of a template." },
31060
+ className: { type: "string", description: "create: a class_name for the script. Not with content." },
31061
+ extends: { type: "string", description: "create: the base class. Default Node. Not with content." },
31062
+ content: {
31063
+ type: "string",
31064
+ description: "create: the whole file, written as given, instead of a template, className and extends."
31065
+ },
30791
31066
  template: {
30792
31067
  type: "string",
30793
31068
  enum: ["singleton", "state_machine", "component", "resource"],
@@ -30985,7 +31260,7 @@ var TOOL_SPECS = [
30985
31260
  },
30986
31261
  {
30987
31262
  name: "editor_output",
30988
- description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
31263
+ description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
30989
31264
  parameters: {
30990
31265
  severity: {
30991
31266
  type: "string",
@@ -31049,7 +31324,7 @@ var TOOL_SPECS = [
31049
31324
  },
31050
31325
  reloadScript: {
31051
31326
  type: "string",
31052
- description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching, and what it held before under heldBeforeReload. Its constants come back the same way, under heldConstantsBeforeReload and reloadedConstants, with each enum value as Kind.SHORT: an enum member is held as a constant and never shows among the methods. A script that does not compile is not reloaded: reloadProblem gives Godot's error, says whether the copy the editor holds survived, and names the classes the file uses, whose held copies being behind is what failed one before."
31327
+ description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching, and what it held before under heldBeforeReload. Its constants come back the same way, under heldConstantsBeforeReload and reloadedConstants, with each enum value as Kind.SHORT: an enum member is held as a constant and never shows among the methods. A script that does not compile is not reloaded: reloadProblem gives Godot's error, says whether the copy the editor holds survived, and names the classes the file uses, whose held copies being behind is what failed one before. A @tool script is not reloaded either, since it runs inside the editor and reloading one from inside a call can take the editor down: reloadProblem says so, and editor_launch restart recompiles it. A path spelled in another case than the file is reloaded under the spelling on disk, which is the one the editor holds its copy under, and that spelling comes back under reloadedAs."
31053
31328
  }
31054
31329
  },
31055
31330
  requires: ["projectPath"]
@@ -31195,7 +31470,7 @@ var TOOL_SPECS = [
31195
31470
  },
31196
31471
  {
31197
31472
  name: "runtime_invoke",
31198
- description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
31473
+ description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. Errors and warnings the engine reported while a called method ran come back under errors, the first 20, with errors_not_shown counting the rest. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
31199
31474
  parameters: {
31200
31475
  projectPath: RUNNING_PROJECT_PATH,
31201
31476
  pid: RUNNING_PID,
@@ -31207,12 +31482,12 @@ var TOOL_SPECS = [
31207
31482
  property: {
31208
31483
  type: "string",
31209
31484
  ops: ["set"],
31210
- description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, and a write that leaves the property as it was, because the engine would not take it, is refused and says what the property still holds. A write of the value the property already holds, at its own precision, as a float given in 64 bits to a property keeping 32, is not refused: it answers unchanged: true.'
31485
+ description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. A map keyed by numbers is stepped into by the number, "_game:units:3". The answer reads back off the same holder, and a write that leaves the property as it was is refused and says what it still holds: the engine not taking it and a setter keeping the value look the same, so the refusal names neither. A write of the value the property already holds, at its own precision, as a float given in 64 bits to a property keeping 32, is not refused: it answers unchanged: true.'
31211
31486
  },
31212
31487
  value: {
31213
31488
  ops: ["set"],
31214
31489
  blank: true,
31215
- description: `set: the value, fitted to the property's type. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key.`
31490
+ description: `set: the value, fitted to the property's type. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
31216
31491
  },
31217
31492
  method: {
31218
31493
  type: "string",
@@ -31222,7 +31497,7 @@ var TOOL_SPECS = [
31222
31497
  args: {
31223
31498
  type: "array",
31224
31499
  ops: ["call"],
31225
- description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths.`
31500
+ description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths.`
31226
31501
  },
31227
31502
  timeoutMs: {
31228
31503
  type: "integer",
@@ -31300,7 +31575,7 @@ var TOOL_SPECS = [
31300
31575
  says: {
31301
31576
  type: "string",
31302
31577
  ops: ["click"],
31303
- description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it, is not on screen. The matches are ranked the way a person picks what to press: a button before text that cannot be pressed, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one."
31578
+ description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one."
31304
31579
  },
31305
31580
  action: {
31306
31581
  type: "string",
@@ -31333,7 +31608,7 @@ var TOOL_SPECS = [
31333
31608
  replace: {
31334
31609
  type: "boolean",
31335
31610
  ops: ["text"],
31336
- description: 'text: true writes over what the field already says, which is what filling one in means. Default false types at the caret, so a field reading 2.1 typed "0.3" at reads 2.10.3. The answer says what the field holds afterwards either way.'
31611
+ description: 'text: true writes over what the field already says, which is what filling one in means. Default false types at the caret, so a field reading 2.1 typed "0.3" at reads 2.10.3. The answer says what the field holds afterwards either way. A read-only field is refused.'
31337
31612
  },
31338
31613
  index: {
31339
31614
  type: "number",
@@ -31377,11 +31652,11 @@ var TOOL_SPECS = [
31377
31652
  requires: [],
31378
31653
  operations: {
31379
31654
  click: {
31380
- summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the view moved. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press",
31655
+ summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room",
31381
31656
  requires: []
31382
31657
  },
31383
31658
  choose: {
31384
- summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the item takes the focus and Enter presses it, which is the engine's own path and needs no window. Answers with what was chosen, read before the press, what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick), and what the button in front of it shows now when it is still there",
31659
+ summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the menu is opened, the item is looked for in it as it stands then, since a game can fill a menu as it opens, and the item takes the focus and ui_accept presses it, which is the engine's own path, needs no window, and does not depend on what ui_accept is bound to. A disabled or hidden button in front of the menu is refused, and so is a press the menu did not take, which is heard rather than assumed. Answers with what was chosen, read before the press, what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick), and what the button in front of it shows now when it is still there",
31385
31660
  requires: ["nodePath"]
31386
31661
  },
31387
31662
  action: { summary: "press an action, or hold it", requires: ["action"] },
@@ -31390,7 +31665,7 @@ var TOOL_SPECS = [
31390
31665
  requires: ["keycode"]
31391
31666
  },
31392
31667
  text: {
31393
- summary: "type a string into the field being edited, a character at a time, and say what it landed in and what it holds afterwards, a secret field as its mask",
31668
+ summary: "type a string into the field being edited, a character at a time, and say what it landed in and what it holds afterwards, a secret field as its mask. The field is the focus of the dialog that has the focus when one does, which is where the keys go",
31394
31669
  requires: ["text"]
31395
31670
  },
31396
31671
  mouse_click: {
@@ -31478,7 +31753,7 @@ var TOOL_SPECS = [
31478
31753
  },
31479
31754
  {
31480
31755
  name: "debug_control",
31481
- description: "Continues or steps the debugged game through the editor's debug adapter, answering with the stack where it ended up. There is no pause and no step_out: Godot's adapter answers a pause by reporting the game stopped and leaving it running, and implements no stepOut at all, so hold the game where you want it with a breakpoint and step over or into from there.",
31756
+ description: "Continues or steps the debugged game through the editor's debug adapter. A step waits for the game to stop again and answers with the stack where it ended up; one that has not stopped the game again within ten seconds answers heldAgain false and no stack. There is no pause and no step_out: Godot's adapter answers a pause by reporting the game stopped and leaving it running, and implements no stepOut at all, so hold the game where you want it with a breakpoint and step over or into from there.",
31482
31757
  parameters: {},
31483
31758
  requires: [],
31484
31759
  operations: {
@@ -31489,12 +31764,12 @@ var TOOL_SPECS = [
31489
31764
  },
31490
31765
  {
31491
31766
  name: "debug_state",
31492
- description: "Where the debugged game is stopped: the stack trace, what is in scope at a frame with the values, or the debug adapter's console output so far. A stop is reported by the editor's adapter only to the sessions connected when it happened, so a server that connected afterwards, which is every server after a reconnect, is shown a thread and no frames about a game still sitting at its breakpoint. That server asks the runtime instead: a held game accepts a connection and never answers, and the refusal then says held rather than running, and that the stack cannot be read from here. debug_control continue lets it go, and the next stop is one this session is told about, with its stack.",
31767
+ description: "Where the debugged game is stopped: the stack trace, what is in scope at a frame with the values, or the debug adapter's console output so far, its last thousand lines, with any earlier ones counted under notShown. A stop is reported by the editor's adapter only to the sessions connected when it happened, so a server that connected afterwards, which is every server after a reconnect, is shown a thread and no frames about a game still sitting at its breakpoint. That server asks the runtime instead: a held game accepts a connection and never answers, and the refusal then says held rather than running, and that the stack cannot be read from here. debug_control continue lets it go, and the next stop is one this session is told about, with its stack.",
31493
31768
  parameters: {
31494
31769
  frameId: {
31495
31770
  type: "number",
31496
31771
  ops: ["variables"],
31497
- description: "variables: which frame, from a stack answer. Default the innermost."
31772
+ description: "variables: which frame, from a stack answer. Default the innermost. A frame the stack no longer holds is refused, and a scope whose values did not arrive in time answers variables null and is named under valuesMissing."
31498
31773
  }
31499
31774
  },
31500
31775
  requires: [],
@@ -31555,8 +31830,8 @@ function buildToolDefinitions() {
31555
31830
  // src/server.ts
31556
31831
  var UPDATE_NOTICE_EVERY = 500;
31557
31832
  var FEEDBACK_NOTICE_EVERY = 250;
31558
- var run5 = promisify6(execFile6);
31559
- var __dirname2 = dirname11(fileURLToPath4(import.meta.url));
31833
+ var run4 = promisify5(execFile5);
31834
+ var __dirname2 = dirname12(fileURLToPath4(import.meta.url));
31560
31835
  var EDITOR_RESTART_TIMEOUT_MS = 90000;
31561
31836
  var HOLD_PING_MS = 2000;
31562
31837
  var SLOWEST_FRAME_RATE = 20;
@@ -31570,16 +31845,16 @@ function captureDestinationRefusal(outputPath, projects) {
31570
31845
  if (extname2(outputPath).toLowerCase() !== ".png") {
31571
31846
  return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
31572
31847
  }
31573
- const directory = realPathOr(dirname11(outputPath));
31574
- if (!existsSync12(directory)) {
31575
- return `outputPath's directory ${dirname11(outputPath)} does not exist; a capture creates no directories.`;
31848
+ const directory = realPathOr(dirname12(outputPath));
31849
+ if (!existsSync14(directory)) {
31850
+ return `outputPath's directory ${dirname12(outputPath)} does not exist; a capture creates no directories.`;
31576
31851
  }
31577
- const where = join18(directory, basename4(outputPath));
31852
+ const where = join19(directory, basename4(outputPath));
31578
31853
  const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
31579
31854
  if (inside !== undefined) {
31580
31855
  return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
31581
31856
  }
31582
- if (existsSync12(where)) {
31857
+ if (existsSync14(where)) {
31583
31858
  return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
31584
31859
  }
31585
31860
  return null;
@@ -31610,16 +31885,18 @@ function describeHalt(halt) {
31610
31885
  function exited(run) {
31611
31886
  return run.exitCode !== null || run.exitSignal !== null;
31612
31887
  }
31613
- function endingOf(failed) {
31614
- return {
31615
- exitCode: typeof failed.code === "number" ? failed.code : null,
31616
- exitSignal: typeof failed.signal === "string" ? failed.signal : null,
31617
- failure: typeof failed.code === "string" ? failed.code : null
31618
- };
31619
- }
31620
31888
  function howItExited(run) {
31621
31889
  return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}, with no exit code`;
31622
31890
  }
31891
+ function errorsUnread(run) {
31892
+ return run.throughEditor && typeof run.errorReport !== "string";
31893
+ }
31894
+ function cleanVerdict(run) {
31895
+ if (run.consoleLost === true || errorsUnread(run)) {
31896
+ return;
31897
+ }
31898
+ return run.log.count("error") === 0;
31899
+ }
31623
31900
  function stillRunning(run) {
31624
31901
  if (run === null || exited(run) || run.endedUnwatched === true) {
31625
31902
  return false;
@@ -31636,6 +31913,11 @@ function stillRunning(run) {
31636
31913
  return true;
31637
31914
  }
31638
31915
  var PLAY_STARTS_WITHIN_MS = 30000;
31916
+ function playOf(answer) {
31917
+ const play = readNumber(answer, "play");
31918
+ const editorPid = readNumber(answer, "editorPid");
31919
+ return play === undefined || editorPid === undefined ? null : { play, editorPid };
31920
+ }
31639
31921
  function runIsUp(run, editorSays, now = Date.now()) {
31640
31922
  if (run?.throughEditor !== true) {
31641
31923
  return stillRunning(run);
@@ -31685,11 +31967,19 @@ function stopVerdict(stop) {
31685
31967
  return {
31686
31968
  stopped: true,
31687
31969
  endedPid: stop.endedPid,
31688
- clean: stop.errors === 0,
31970
+ clean: stop.clean,
31689
31971
  note: "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running.",
31690
31972
  withChildren: false
31691
31973
  };
31692
31974
  }
31975
+ if (stop.ending === "unreached") {
31976
+ return {
31977
+ stopped: false,
31978
+ notSignalled: stop.endedPid,
31979
+ note: "Nothing was ended: the editor could not be asked to stop the scene it is playing, so the game may still be running. editor_status says whether the editor is connected and playing.",
31980
+ withChildren: false
31981
+ };
31982
+ }
31693
31983
  if (stop.ending === "refused") {
31694
31984
  return {
31695
31985
  stopped: false,
@@ -31701,13 +31991,13 @@ function stopVerdict(stop) {
31701
31991
  return {
31702
31992
  stopped: true,
31703
31993
  endedPid: stop.endedPid,
31704
- clean: stop.errors === 0,
31994
+ clean: stop.clean,
31705
31995
  note: `${stop.throughEditor ? stop.endedPid === null ? "The editor was asked to stop the scene it is playing. Its game had not announced a runtime, so no process was named under endedPid, and its exit code stays with the editor." : "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."}${stop.exitSignal === null ? "" : ` It was ended by ${stop.exitSignal} and so has no exit code.`}${stop.ending === "gone" ? "" : ` It had not exited ${STOP_WAIT_MS / 1000} seconds after being told to, so it may still be going: editor_status says whether it is.`}`,
31706
31996
  withChildren: true
31707
31997
  };
31708
31998
  }
31709
31999
  function endedPreviousRun(ended) {
31710
- if (ended === null || ended.ending === "refused") {
32000
+ if (ended === null || ended.ending === "refused" || ended.ending === "unreached") {
31711
32001
  return;
31712
32002
  }
31713
32003
  return ended.pid ?? true;
@@ -31790,6 +32080,9 @@ function endedToStartThis(ended) {
31790
32080
  if (ended.ending === "refused") {
31791
32081
  return ` ${which} was not ended: that pid no longer answers as the process the run was started as, so nothing was signalled. If it is still the game, it is running beside this one and you have to end it yourself; if it is not, it belongs to something else.`;
31792
32082
  }
32083
+ if (ended.ending === "unreached") {
32084
+ return ` ${which} was not ended here: the editor could not be asked to stop it. The editor stops a play before starting the next, so it is not running beside this one if this start reached the editor.`;
32085
+ }
31793
32086
  const after = ended.ending === "lingering" ? ` It had not exited ${STOP_WAIT_MS / 1000} seconds after being told to, so it may still be running beside this one: editor_status says whether it is.` : "";
31794
32087
  return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.${after}`;
31795
32088
  }
@@ -31919,6 +32212,7 @@ var HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
31919
32212
  var HANDOVER_POLL_MS = 250;
31920
32213
  var CACHE_WRITE_MS = 2000;
31921
32214
  var SCAN_WAIT_MS = 30000;
32215
+ var STREAMS_GRACE_MS = 2000;
31922
32216
  function scanWaitIn(payload) {
31923
32217
  const waited = readNumber(payload, "waitedForEditorScanMs");
31924
32218
  const note = readString(payload, "scanNote");
@@ -31931,19 +32225,58 @@ function withScanWait(outcome, scanned) {
31931
32225
  if (scanned.waitedMs === 0) {
31932
32226
  return outcome;
31933
32227
  }
31934
- const said = scanWaitAnswer(scanned);
32228
+ const said = scanWaitAnswer(scanned, "engine");
31935
32229
  if (outcome.ok) {
31936
32230
  return { ...outcome, payload: { ...outcome.payload, ...said } };
31937
32231
  }
31938
32232
  return said.scanNote === undefined ? outcome : { ...outcome, message: `${outcome.message} ${said.scanNote}` };
31939
32233
  }
31940
- function scanWaitAnswer(scanned) {
32234
+ function notRunNote(notRun, failFast) {
32235
+ if (notRun <= 0) {
32236
+ return;
32237
+ }
32238
+ const cases = `${notRun} case${notRun === 1 ? "" : "s"}`;
32239
+ return failFast ? `This run stopped at the first failure in each suite it failed in, so ${cases} never ran and count as neither passed nor failed. Leave failFast out to run every case.` : `${cases} counted in ${notRun === 1 ? "its suite" : "their suites"} never ran and count as neither passed nor failed. failFast was not set, so it was not that: the suites below with notRun are the ones to read.`;
32240
+ }
32241
+ function exportAnswer(ending, asked, before, after) {
32242
+ const { log } = ending;
32243
+ const written = after !== null && after !== before;
32244
+ const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
32245
+ const verdict = {
32246
+ exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
32247
+ ...asked,
32248
+ exitCode: ending.exitCode,
32249
+ exitSignal: ending.exitSignal ?? undefined,
32250
+ failure: ending.failure ?? undefined,
32251
+ errors: log.count("error"),
32252
+ warnings: log.count("warning"),
32253
+ entries: forAnswer(problems.entries),
32254
+ ...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
32255
+ };
32256
+ if (verdict.exported) {
32257
+ return { content: [{ type: "text", text: JSON.stringify(verdict, null, 2) }] };
32258
+ }
32259
+ const stale = !written && after !== null ? ` The ${asked.outputPath} there is from before this export, which did not write it.` : "";
32260
+ const why = ending.exitSignal === null ? stale : ` The engine was ${howItEnded(ending)}, so it has no exit code.${stale}`;
32261
+ return {
32262
+ content: [
32263
+ {
32264
+ type: "text",
32265
+ text: `Export with preset '${asked.preset}' did not produce ${asked.outputPath}.${why}`
32266
+ },
32267
+ { type: "text", text: JSON.stringify(verdict, null, 2) }
32268
+ ],
32269
+ isError: true
32270
+ };
32271
+ }
32272
+ function scanWaitAnswer(scanned, started = "game") {
31941
32273
  if (scanned.waitedMs === 0) {
31942
32274
  return {};
31943
32275
  }
32276
+ const still = `The editor was still scanning the project after ${SCAN_WAIT_MS / 1000} seconds, so`;
31944
32277
  return {
31945
32278
  waitedForEditorScanMs: scanned.waitedMs,
31946
- scanNote: scanned.stillScanning ? `The editor was still scanning the project after ${SCAN_WAIT_MS / 1000} seconds, so 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." : undefined
32279
+ 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."
31947
32280
  };
31948
32281
  }
31949
32282
  var PLAYING_STATUS_MS = 1000;
@@ -32023,16 +32356,17 @@ var PROJECT_FILE_ARGUMENTS = [
32023
32356
  "path",
32024
32357
  "script"
32025
32358
  ];
32026
- function escapingPropertyValue(properties) {
32027
- if (typeof properties !== "object" || properties === null) {
32359
+ function escapingPropertyValue(value) {
32360
+ if (typeof value === "string") {
32361
+ return /^(res|uid):\/\//.test(value) && value.split(/[/\\]/).includes("..") ? JSON.stringify(value) : undefined;
32362
+ }
32363
+ if (typeof value !== "object" || value === null) {
32028
32364
  return;
32029
32365
  }
32030
- for (const value of Object.values(properties)) {
32031
- if (typeof value !== "string" || !value.includes("/")) {
32032
- continue;
32033
- }
32034
- if (value.split(/[/\\]/).includes("..")) {
32035
- return JSON.stringify(value);
32366
+ for (const item of Object.values(value)) {
32367
+ const found = escapingPropertyValue(item);
32368
+ if (found !== undefined) {
32369
+ return found;
32036
32370
  }
32037
32371
  }
32038
32372
  return;
@@ -32070,7 +32404,7 @@ function aboveTheRunner(entry) {
32070
32404
  }
32071
32405
  function elsewhereIn(projectPath, asked) {
32072
32406
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
32073
- if (wanted === "" || existsSync12(join18(projectPath, wanted))) {
32407
+ if (wanted === "" || existsSync14(join19(projectPath, wanted))) {
32074
32408
  return "";
32075
32409
  }
32076
32410
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -32084,7 +32418,7 @@ function elsewhereIn(projectPath, asked) {
32084
32418
  return near.length === 0 ? "" : ` The project does have ${there}.`;
32085
32419
  }
32086
32420
  function hasMainScene(projectFile) {
32087
- const scene = parseProjectGodot(readFileSync17(projectFile, "utf8"))["application"]?.["run/main_scene"];
32421
+ const scene = parseProjectGodot(readFileSync18(projectFile, "utf8"))["application"]?.["run/main_scene"];
32088
32422
  return typeof scene === "string" && scene !== "";
32089
32423
  }
32090
32424
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -32092,13 +32426,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
32092
32426
  return false;
32093
32427
  }
32094
32428
  try {
32095
- return readFileSync17(join18(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
32429
+ return readFileSync18(join19(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
32096
32430
  } catch {
32097
32431
  return false;
32098
32432
  }
32099
32433
  }
32100
32434
  function editorPlaysHeadless(projectFile) {
32101
- const runArgs = parseProjectGodot(readFileSync17(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
32435
+ const runArgs = parseProjectGodot(readFileSync18(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
32102
32436
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
32103
32437
  }
32104
32438
  function camelCased(params) {
@@ -32117,7 +32451,7 @@ function needsItsOwnProcess(asked) {
32117
32451
  class GodotServer {
32118
32452
  mcp;
32119
32453
  locator = new GodotLocator;
32120
- operationsScript = join18(__dirname2, "godot", "operations", "godot_operations.gd");
32454
+ operationsScript = join19(__dirname2, "godot", "operations", "godot_operations.gd");
32121
32455
  godotBridge;
32122
32456
  tools = buildToolDefinitions();
32123
32457
  activeProcess = null;
@@ -32617,6 +32951,9 @@ class GodotServer {
32617
32951
  const contained = this.containProjectFiles(asks);
32618
32952
  return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
32619
32953
  }
32954
+ if (headless === "create_script") {
32955
+ return await this.handleCreateScript(answerable);
32956
+ }
32620
32957
  if (headless !== "refresh_class_cache") {
32621
32958
  return await this.headless(headless, answerable);
32622
32959
  }
@@ -32787,7 +33124,7 @@ class GodotServer {
32787
33124
  return await this.handleRuntimeCommand("click", {
32788
33125
  ...whichGame(args),
32789
33126
  path: readNonEmptyString(args, "nodePath") ?? "",
32790
- button: readString(args, "button") ?? "left",
33127
+ button: args["button"] ?? "left",
32791
33128
  double: readBoolean(args, "doubleClick") ?? false,
32792
33129
  ...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
32793
33130
  ...args["index"] === undefined ? {} : { index: args["index"] }
@@ -32859,8 +33196,8 @@ class GodotServer {
32859
33196
  if (path === undefined) {
32860
33197
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
32861
33198
  }
32862
- const file = join18(path, "project.godot");
32863
- if (!existsSync12(file)) {
33199
+ const file = join19(path, "project.godot");
33200
+ if (!existsSync14(file)) {
32864
33201
  return {
32865
33202
  ok: false,
32866
33203
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -33070,13 +33407,30 @@ class GodotServer {
33070
33407
  }
33071
33408
  contained[key] = `res://${location.relativePath}`;
33072
33409
  }
33073
- const escaping = escapingPropertyValue(args["properties"]);
33410
+ const escaping = escapingPropertyValue([args["properties"], args["track"]]);
33074
33411
  if (escaping !== undefined) {
33075
33412
  return {
33076
33413
  ok: false,
33077
33414
  response: this.createErrorResponse(`The property value ${escaping} resolves outside the project directory.`, PATH_SOLUTIONS)
33078
33415
  };
33079
33416
  }
33417
+ const sources = args["sources"];
33418
+ if (Array.isArray(sources)) {
33419
+ const resolved = [];
33420
+ for (const source of sources) {
33421
+ const texture = typeof source === "object" && source !== null ? source["texture"] : undefined;
33422
+ if (typeof texture !== "string") {
33423
+ resolved.push(source);
33424
+ continue;
33425
+ }
33426
+ const location = resolveWithinProject(projectPath, texture);
33427
+ if (!location.ok) {
33428
+ return { ok: false, response: this.createErrorResponse(location.reason, PATH_SOLUTIONS) };
33429
+ }
33430
+ resolved.push({ ...source, texture: `res://${location.relativePath}` });
33431
+ }
33432
+ contained["sources"] = resolved;
33433
+ }
33080
33434
  const pluginName = readNonEmptyString(args, "pluginName");
33081
33435
  if (pluginName !== undefined) {
33082
33436
  const plugin = resolveWithinProject(projectPath, `addons/${pluginName}/plugin.cfg`);
@@ -33124,6 +33478,7 @@ class GodotServer {
33124
33478
  return this.answer(withScanWait({
33125
33479
  ok: true,
33126
33480
  messages: imported.messages,
33481
+ ...imported.messagesOmitted === undefined ? {} : { messagesOmitted: imported.messagesOmitted },
33127
33482
  payload: {
33128
33483
  uidsCreated: given,
33129
33484
  stillWithoutUid: after,
@@ -33174,16 +33529,25 @@ class GodotServer {
33174
33529
  if (changed.length === 0) {
33175
33530
  return written;
33176
33531
  }
33177
- const taken = await this.editorTakes("adopt_project_settings", { settings: changed });
33532
+ const adopting = {};
33533
+ const taken = await this.editorTakes("adopt_project_settings", { settings: changed }, adopting);
33534
+ if (taken["editorNote"] !== undefined) {
33535
+ return this.jsonTextResponse({ ...answer, ...taken });
33536
+ }
33537
+ const adopted = Array.isArray(adopting["adopted"]) ? adopting["adopted"].map(String) : [];
33538
+ const passedOver = changed.filter((name) => !adopted.includes(name));
33178
33539
  return this.jsonTextResponse({
33179
33540
  ...answer,
33180
- ...taken,
33181
- ...taken["editorNote"] === undefined ? { editorAdopted: changed } : {}
33541
+ editorAdopted: adopted,
33542
+ ...passedOver.length > 0 ? { editorPassedOver: passedOver } : {}
33182
33543
  });
33183
33544
  }
33184
- async editorTakes(command, args) {
33545
+ async editorTakes(command, args, answered = {}) {
33185
33546
  try {
33186
- await this.godotBridge.invokeTool(command, args);
33547
+ const result = await this.godotBridge.invokeTool(command, args);
33548
+ if (typeof result === "object" && result !== null) {
33549
+ Object.assign(answered, result);
33550
+ }
33187
33551
  return {};
33188
33552
  } catch (error) {
33189
33553
  const message = errorMessage(error);
@@ -33221,7 +33585,19 @@ class GodotServer {
33221
33585
  }
33222
33586
  const applied = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
33223
33587
  if (!applied.ok) {
33224
- return applied.response;
33588
+ const listed = written.payload["updated_options"];
33589
+ const updated = Array.isArray(listed) ? listed.map(String) : [];
33590
+ const which = updated.length === 0 ? "The options were" : `${updated.join(", ")} ${updated.length === 1 ? "was" : "were"}`;
33591
+ return {
33592
+ ...applied.response,
33593
+ content: [
33594
+ {
33595
+ type: "text",
33596
+ text: `${which} written to the import file, and the reimport that applies them failed, so the resource is still imported the old way until the editor's next scan or project_import reimport imports it. Why the reimport failed:`
33597
+ },
33598
+ ...applied.response.content
33599
+ ]
33600
+ };
33225
33601
  }
33226
33602
  return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
33227
33603
  }
@@ -33239,7 +33615,14 @@ class GodotServer {
33239
33615
  response: this.createErrorResponse(`${missingSource[0]} is not on disk, so there is nothing to import.`)
33240
33616
  };
33241
33617
  }
33242
- const targets = listed.filter((one) => one.status !== "missing_source" && (force || one.status !== "up_to_date")).map((one) => one.path);
33618
+ const never = listed.find((one) => one.status === "not_imported");
33619
+ if (resourcePath !== undefined && never !== undefined) {
33620
+ return {
33621
+ ok: false,
33622
+ response: this.createErrorResponse(`${never.path} is not something the engine imports: ${never.reason ?? "it has no importer"}.`)
33623
+ };
33624
+ }
33625
+ const targets = listed.filter((one) => one.status !== "missing_source" && one.status !== "not_imported" && (force || one.status !== "up_to_date")).map((one) => one.path);
33243
33626
  if (targets.length === 0) {
33244
33627
  return {
33245
33628
  ok: true,
@@ -33278,9 +33661,7 @@ class GodotServer {
33278
33661
  if (!imported.ok) {
33279
33662
  return { ok: false, response: this.answer(withScanWait(imported, scanned)) };
33280
33663
  }
33281
- if (imported.messages.length > 0) {
33282
- extra["engine_messages"] = imported.messages;
33283
- }
33664
+ Object.assign(extra, engineExtras(imported));
33284
33665
  if (imported.extensionNote !== undefined) {
33285
33666
  extra["extensionNote"] = imported.extensionNote;
33286
33667
  }
@@ -33363,17 +33744,15 @@ class GodotServer {
33363
33744
  return withScanWait(outcome, scanned);
33364
33745
  }
33365
33746
  answer(outcome) {
33747
+ const extras = engineExtras(outcome);
33366
33748
  if (!outcome.ok) {
33367
33749
  const response = this.createErrorResponse(outcome.message);
33368
- if (outcome.messages.length > 0) {
33369
- response.content.push({
33370
- type: "text",
33371
- text: JSON.stringify({ engine_messages: outcome.messages }, null, 2)
33372
- });
33750
+ if (Object.keys(extras).length > 0) {
33751
+ response.content.push({ type: "text", text: JSON.stringify(extras, null, 2) });
33373
33752
  }
33374
33753
  return response;
33375
33754
  }
33376
- return this.jsonTextResponse(outcome.messages.length > 0 ? { ...outcome.payload, engine_messages: outcome.messages } : outcome.payload);
33755
+ return this.jsonTextResponse({ ...outcome.payload, ...extras });
33377
33756
  }
33378
33757
  async handleProjectInfo(args) {
33379
33758
  const include = readStringArray(args, "include") ?? [];
@@ -33389,7 +33768,7 @@ class GodotServer {
33389
33768
  if (!engine.ok) {
33390
33769
  return engine.response;
33391
33770
  }
33392
- const application = parseProjectGodot(readFileSync17(project.value.file, "utf8"))["application"] ?? {};
33771
+ const application = parseProjectGodot(readFileSync18(project.value.file, "utf8"))["application"] ?? {};
33393
33772
  const name = application["config/name"];
33394
33773
  const mainScene = application["run/main_scene"];
33395
33774
  const info = {
@@ -33406,7 +33785,7 @@ class GodotServer {
33406
33785
  continue;
33407
33786
  }
33408
33787
  const outcome = await this.operation(spec.operation, spec.params(args, detailed), project.value.path);
33409
- info[section] = outcome.ok ? outcome.messages.length > 0 ? { ...outcome.payload, engine_messages: outcome.messages } : outcome.payload : { error: outcome.message, engine_messages: outcome.messages };
33788
+ info[section] = outcome.ok ? { ...outcome.payload, ...engineExtras(outcome) } : { error: outcome.message, ...engineExtras(outcome) };
33410
33789
  }
33411
33790
  return this.jsonTextResponse(info);
33412
33791
  }
@@ -33450,18 +33829,18 @@ class GodotServer {
33450
33829
  if (!engine.ok) {
33451
33830
  return engine.response;
33452
33831
  }
33453
- const folder = dirname11(output.absolutePath);
33832
+ const folder = dirname12(output.absolutePath);
33454
33833
  try {
33455
33834
  mkdirSync10(folder, { recursive: true });
33456
33835
  } catch (error) {
33457
- return this.createErrorResponse(`Cannot create ${dirname11(output.relativePath)} to export into: ${errorMessage(error)}`);
33836
+ return this.createErrorResponse(`Cannot create ${dirname12(output.relativePath)} to export into: ${errorMessage(error)}`);
33458
33837
  }
33459
33838
  const debug = readBoolean(args, "debug") ?? false;
33460
33839
  const exportLogs = scratchDirectory("export");
33461
33840
  const exportArgs = [
33462
33841
  "--headless",
33463
33842
  "--log-file",
33464
- join18(exportLogs, "engine.log"),
33843
+ join19(exportLogs, "engine.log"),
33465
33844
  "--path",
33466
33845
  project.value.path,
33467
33846
  debug ? "--export-debug" : "--export-release",
@@ -33469,55 +33848,18 @@ class GodotServer {
33469
33848
  output.absolutePath
33470
33849
  ];
33471
33850
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
33472
- const log = new GameLog;
33473
- let ending = { exitCode: 0, exitSignal: null, failure: null };
33851
+ const writtenAt = () => existsSync14(output.absolutePath) ? statSync8(output.absolutePath).mtimeMs : null;
33852
+ const before = writtenAt();
33853
+ let ending;
33474
33854
  try {
33475
- const { stdout, stderr } = await run5(engine.value, exportArgs, {
33476
- timeout: 300000,
33477
- maxBuffer: 64 * 1024 * 1024,
33478
- signal: callSignal()
33479
- });
33480
- log.append("stdout", stdout);
33481
- log.append("stderr", stderr);
33482
- } catch (error) {
33483
- if (!(error instanceof Error && ("stdout" in error) && ("stderr" in error))) {
33484
- return this.createErrorResponse(`Export could not be run: ${errorMessage(error)}`);
33485
- }
33486
- const failed = error;
33487
- log.append("stdout", failed.stdout);
33488
- log.append("stderr", failed.stderr);
33489
- ending = endingOf(failed);
33855
+ ending = await runEngine(engine.value, exportArgs, { timeout: { ms: 300000, said: "five minutes" } });
33490
33856
  } finally {
33491
33857
  discard(exportLogs);
33492
33858
  }
33493
- log.finish();
33494
- const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
33495
- const verdict = {
33496
- exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync12(output.absolutePath),
33497
- preset,
33498
- outputPath: output.relativePath,
33499
- debug,
33500
- exitCode: ending.exitCode,
33501
- exitSignal: ending.exitSignal ?? undefined,
33502
- failure: ending.failure ?? undefined,
33503
- errors: log.count("error"),
33504
- warnings: log.count("warning"),
33505
- entries: forAnswer(problems.entries)
33506
- };
33507
- if (!verdict.exported) {
33508
- const why = ending.exitSignal === null ? ending.failure === null ? "" : ` The engine was ended by Node (${ending.failure}), so it has no exit code.` : ` The engine was ended by ${ending.exitSignal}, so it has no exit code: the export ran past its five minutes or the call was cancelled.`;
33509
- return {
33510
- content: [
33511
- {
33512
- type: "text",
33513
- text: `Export with preset '${preset}' did not produce ${output.relativePath}.${why}`
33514
- },
33515
- { type: "text", text: JSON.stringify(verdict, null, 2) }
33516
- ],
33517
- isError: true
33518
- };
33859
+ if (ending.exitSignal === null && ending.exitCode === null) {
33860
+ return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
33519
33861
  }
33520
- return this.jsonTextResponse(verdict);
33862
+ return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt());
33521
33863
  }
33522
33864
  async handleRunTests(args) {
33523
33865
  const project = this.project(args);
@@ -33529,7 +33871,7 @@ class GodotServer {
33529
33871
  return contained.response;
33530
33872
  }
33531
33873
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
33532
- if (!existsSync12(join18(project.value.path, runner))) {
33874
+ if (!existsSync14(join19(project.value.path, runner))) {
33533
33875
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
33534
33876
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
33535
33877
  ]);
@@ -33595,21 +33937,22 @@ class GodotServer {
33595
33937
  if (timedOut && run.process.pid !== undefined) {
33596
33938
  await untilGone([run.process.pid]);
33597
33939
  }
33598
- const reportsDir = join18(project.value.path, ".godot", ...ours.split("/"));
33940
+ await run.settled;
33941
+ const reportsDir = join19(project.value.path, ".godot", ...ours.split("/"));
33599
33942
  let report = null;
33600
33943
  let reportProblem = null;
33601
33944
  try {
33602
- const written = existsSync12(reportsDir) ? readdirSync8(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
33945
+ const written = existsSync14(reportsDir) ? readdirSync8(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
33603
33946
  const newest = written.at(-1);
33604
33947
  if (newest !== undefined) {
33605
- report = parseJUnit(readFileSync17(join18(reportsDir, newest, "results.xml"), "utf8"));
33948
+ report = parseJUnit(readFileSync18(join19(reportsDir, newest, "results.xml"), "utf8"));
33606
33949
  }
33607
33950
  } catch (error) {
33608
33951
  reportProblem = errorMessage(error);
33609
33952
  } finally {
33610
33953
  discard(reportsDir, userData);
33611
33954
  try {
33612
- rmdirSync2(dirname11(reportsDir));
33955
+ rmdirSync2(dirname12(reportsDir));
33613
33956
  } catch {}
33614
33957
  }
33615
33958
  const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
@@ -33622,7 +33965,11 @@ class GodotServer {
33622
33965
  105: "script errors"
33623
33966
  };
33624
33967
  const exitCode = run.exitCode;
33625
- const printed = run.log.select({ severity: "info", sinceLastCall: false, limit: 200 }).entries;
33968
+ const printed = run.log.select({
33969
+ severity: "info",
33970
+ sinceLastCall: false,
33971
+ limit: Number.POSITIVE_INFINITY
33972
+ }).entries;
33626
33973
  const said = printed.map((entry) => entry.text);
33627
33974
  const nothingRan = timedOut ? null : whyNoReport(said, asked);
33628
33975
  const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
@@ -33638,7 +33985,7 @@ class GodotServer {
33638
33985
  ...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
33639
33986
  };
33640
33987
  if (report === null) {
33641
- const note = nothingRan !== null ? `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).`;
33988
+ const note = nothingRan !== null ? `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${verdict}.` : `The test run wrote no report (${verdict}).`;
33642
33989
  return {
33643
33990
  content: [
33644
33991
  { type: "text", text: note },
@@ -33675,10 +34022,21 @@ class GodotServer {
33675
34022
  orphans: suite.orphans,
33676
34023
  message: `${suite.orphans} node${suite.orphans === 1 ? "" : "s"} were still in the tree when this suite finished. A node built in a test and neither freed nor added to the tree is the usual cause; free it, or add it with auto_free().`
33677
34024
  }));
33678
- const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
34025
+ const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.hookFailures > 0 || suite.hookErrors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
34026
+ const hooksPrinted = hookFailuresPrinted(said);
34027
+ const hookFailures = report.suites.filter((suite) => suite.hookFailures + suite.hookErrors > 0).flatMap((suite) => {
34028
+ const printedHere = hooksPrinted.filter((hook) => hook.path === suite.path);
34029
+ return printedHere.length > 0 ? printedHere.map((hook) => ({ suite: suite.name, ...hook })) : [
34030
+ {
34031
+ suite: suite.name,
34032
+ path: suite.path,
34033
+ message: "gdUnit4 counted a failure in this suite that none of its cases carry, which is what a failed before() or after() leaves, and printed no line saying which."
34034
+ }
34035
+ ];
34036
+ });
33679
34037
  const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
33680
34038
  return this.jsonTextResponse({
33681
- passed: !timedOut && exitCode === 0 && report.failures === 0 && report.errors === 0,
34039
+ passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0,
33682
34040
  verdict,
33683
34041
  ...timedOut ? { timedOut, hung, silentForMs } : {},
33684
34042
  exitCode,
@@ -33689,13 +34047,14 @@ class GodotServer {
33689
34047
  skipped: report.skipped,
33690
34048
  time: report.time,
33691
34049
  failed,
34050
+ hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
33692
34051
  warnings: warnings.length > 0 ? warnings : undefined,
33693
34052
  orphans: orphans.total > 0 ? orphans.total : undefined,
33694
34053
  notRun: notRun > 0 ? notRun : undefined,
33695
34054
  ...printedLines,
33696
34055
  savesNote,
33697
34056
  ...scanned,
33698
- note: verdict.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRun > 0 ? `This run stopped at the first failure in each suite it failed in, so ${notRun} case${notRun === 1 ? "" : "s"} never ran and count as neither passed nor failed. Leave failFast out to run every case.` : undefined,
34057
+ note: verdict.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true),
33699
34058
  suites: unclean.map((suite) => ({
33700
34059
  name: suite.name,
33701
34060
  path: suite.path,
@@ -33703,6 +34062,7 @@ class GodotServer {
33703
34062
  notRun: suite.discovered > suite.tests ? suite.discovered - suite.tests : undefined,
33704
34063
  failures: suite.failures,
33705
34064
  errors: suite.errors,
34065
+ hookFailures: suite.hookFailures + suite.hookErrors > 0 ? suite.hookFailures + suite.hookErrors : undefined,
33706
34066
  skipped: suite.skipped,
33707
34067
  orphans: orphansIn(suite.path) > 0 ? orphansIn(suite.path) : undefined,
33708
34068
  time: suite.time
@@ -33716,7 +34076,7 @@ class GodotServer {
33716
34076
  }
33717
34077
  async godotVersion(godotPath) {
33718
34078
  try {
33719
- const { stdout } = await run5(godotPath, ["--version"], { timeout: 1e4 });
34079
+ const { stdout } = await run4(godotPath, ["--version"], { timeout: 1e4 });
33720
34080
  this.lastGodotVersion = stdout.trim();
33721
34081
  return this.lastGodotVersion;
33722
34082
  } catch (error) {
@@ -33728,8 +34088,18 @@ class GodotServer {
33728
34088
  const answer = await this.handleLSP("lsp_get_diagnostics", args);
33729
34089
  const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
33730
34090
  if (payload["error"] !== undefined) {
33731
- const reason = payload["error"];
33732
- return this.createErrorResponse(`Diagnostics unavailable: ${typeof reason === "string" ? reason : JSON.stringify(reason)}`, [
34091
+ const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
34092
+ if (payload["refusedArguments"] === true) {
34093
+ return this.createErrorResponse(reason);
34094
+ }
34095
+ if (typeof payload["serves"] === "string") {
34096
+ return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
34097
+ "editor_status names the editor on the bridge and the port it says it serves",
34098
+ "editor_launch opens this project's own editor, on a language server port of its own",
34099
+ "GDHARNESS_LSP_PORT points this server at the right one"
34100
+ ]);
34101
+ }
34102
+ return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
33733
34103
  `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
33734
34104
  "GDHARNESS_LSP_PORT points this server at another one"
33735
34105
  ]);
@@ -33760,11 +34130,11 @@ class GodotServer {
33760
34130
  const cached = cachedClasses(projectPath);
33761
34131
  const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
33762
34132
  const contained = resolveWithinProject(projectPath, resourcePath);
33763
- if (!contained.ok || !existsSync12(contained.absolutePath)) {
34133
+ if (!contained.ok || !existsSync14(contained.absolutePath)) {
33764
34134
  return null;
33765
34135
  }
33766
34136
  try {
33767
- return readFileSync17(contained.absolutePath, "utf8");
34137
+ return readFileSync18(contained.absolutePath, "utf8");
33768
34138
  } catch {
33769
34139
  return null;
33770
34140
  }
@@ -33820,11 +34190,11 @@ class GodotServer {
33820
34190
  }
33821
34191
  sourceOfClass(projectPath, resourcePath) {
33822
34192
  const contained = resolveWithinProject(projectPath, resourcePath);
33823
- if (!contained.ok || !existsSync12(contained.absolutePath)) {
34193
+ if (!contained.ok || !existsSync14(contained.absolutePath)) {
33824
34194
  return null;
33825
34195
  }
33826
34196
  try {
33827
- return readFileSync17(contained.absolutePath, "utf8");
34197
+ return readFileSync18(contained.absolutePath, "utf8");
33828
34198
  } catch {
33829
34199
  return null;
33830
34200
  }
@@ -34012,7 +34382,8 @@ class GodotServer {
34012
34382
  return {
34013
34383
  playing: readBoolean(asParams(answer), "playing") ?? false,
34014
34384
  scene: readString(asParams(answer), "scenePath") ?? "",
34015
- debugPort: readNumber(asParams(answer), "debugPort")
34385
+ debugPort: readNumber(asParams(answer), "debugPort"),
34386
+ play: playOf(asParams(answer))
34016
34387
  };
34017
34388
  } catch {
34018
34389
  return null;
@@ -34108,7 +34479,7 @@ class GodotServer {
34108
34479
  return null;
34109
34480
  }
34110
34481
  try {
34111
- return settingKeys(readFileSync17(join18(projectPath, "project.godot"), "utf8"));
34482
+ return settingKeys(readFileSync18(join19(projectPath, "project.godot"), "utf8"));
34112
34483
  } catch {
34113
34484
  return null;
34114
34485
  }
@@ -34271,6 +34642,13 @@ class GodotServer {
34271
34642
  if (!project.ok) {
34272
34643
  return project.response;
34273
34644
  }
34645
+ const open = this.godotBridge.isConnected() ? this.godotBridge.getStatus().projectPath : undefined;
34646
+ if (op !== "check" && open !== undefined && open !== "" && !isSameDirectory(open, project.value.path)) {
34647
+ return this.createErrorResponse(`This start names ${project.value.path}, and the editor on this bridge has ${open} open.`, [
34648
+ "editor_status names the project this server's editor is showing",
34649
+ "One server serves one editor: start a second server for the other project"
34650
+ ]);
34651
+ }
34274
34652
  const scene = readNonEmptyString(args, "scene");
34275
34653
  const sceneToRun = scene !== undefined ? resolveWithinProject(project.value.path, scene) : null;
34276
34654
  if (sceneToRun && !sceneToRun.ok) {
@@ -34365,7 +34743,7 @@ class GodotServer {
34365
34743
  }
34366
34744
  async runtimeUp(projectPath, before, budget, withArgs) {
34367
34745
  const budgetMs = budget.ms;
34368
- if (!existsSync12(join18(projectPath, RUNTIME_AUTOLOAD.path))) {
34746
+ if (!existsSync14(join19(projectPath, RUNTIME_AUTOLOAD.path))) {
34369
34747
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
34370
34748
  }
34371
34749
  let asking = null;
@@ -34374,7 +34752,7 @@ class GodotServer {
34374
34752
  let editorAsks = 0;
34375
34753
  const waitBegan = Date.now();
34376
34754
  const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
34377
- const endpoint = await announcedSince(projectPath, before, {
34755
+ const fresh = await announcedSince(projectPath, before, {
34378
34756
  budgetMs,
34379
34757
  accept: (one) => this.isTheGameOf(this.activeProcess, one, projectPath),
34380
34758
  giveUp: () => {
@@ -34398,11 +34776,19 @@ class GodotServer {
34398
34776
  }
34399
34777
  });
34400
34778
  const asked = editorAsks === 0 ? "editor not asked" : `editor asked ${editorAsks === 1 ? "once" : `${editorAsks} times`}, last said ${lastSaid()}`;
34401
- this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, ${asked}`);
34779
+ this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${fresh.length === 0 ? "nothing announced" : `pid ${fresh.map((one) => one.pid).join(", ")} announced`}, ${asked}`);
34402
34780
  const going = await this.currentRun();
34781
+ const endpoint = await this.theRunsAmong(going, fresh);
34403
34782
  if (endpoint !== null && going !== null && going.announcedPid === undefined) {
34404
34783
  this.tie(going, endpoint);
34405
34784
  }
34785
+ if (endpoint === null && fresh.length > 1) {
34786
+ return {
34787
+ listening: true,
34788
+ pids: fresh.map((one) => one.pid),
34789
+ note: `${fresh.length} games of this project announced at once and none could be told from the process tree as this run's own game rather than one it started, so none is taken as its runtime. editor_status lists them under runtimes.`
34790
+ };
34791
+ }
34406
34792
  return runtimeVerdict(endpoint, {
34407
34793
  addon: true,
34408
34794
  budgetMs,
@@ -34486,7 +34872,13 @@ class GodotServer {
34486
34872
  };
34487
34873
  this.activeProcess = played;
34488
34874
  this.watchThePlayedConsole();
34489
- writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
34875
+ const which = playOf(playAnswer);
34876
+ writeEditorRunNote({
34877
+ projectPath,
34878
+ transcript: transcript.path,
34879
+ startedAt,
34880
+ ...which === null ? {} : { play: which }
34881
+ });
34490
34882
  sweepTranscripts();
34491
34883
  return this.jsonTextResponse({
34492
34884
  started: true,
@@ -34601,7 +34993,7 @@ class GodotServer {
34601
34993
  return 0;
34602
34994
  }
34603
34995
  try {
34604
- return reportAlreadyInTranscript(readFileSync17(game.transcript), readFileSync17(report));
34996
+ return reportAlreadyInTranscript(readFileSync18(game.transcript), readFileSync18(report));
34605
34997
  } catch (error) {
34606
34998
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
34607
34999
  return 0;
@@ -34615,6 +35007,18 @@ class GodotServer {
34615
35007
  game.brokeOn = halt.text;
34616
35008
  game.log.record("error", halt.text);
34617
35009
  }
35010
+ async untilTheEditorStopsPlaying(withinMs = STOP_WAIT_MS) {
35011
+ const deadline = Date.now() + withinMs;
35012
+ for (;; ) {
35013
+ if ((await this.editorPlayingState())?.playing === false) {
35014
+ return true;
35015
+ }
35016
+ if (Date.now() >= deadline) {
35017
+ return false;
35018
+ }
35019
+ await delay3(100);
35020
+ }
35021
+ }
34618
35022
  async endActiveGame(reason, going) {
34619
35023
  const running = this.activeProcess;
34620
35024
  const project = running?.projectPath ?? this.ourProject();
@@ -34633,8 +35037,16 @@ class GodotServer {
34633
35037
  running.endedHere = reason;
34634
35038
  const announced = this.announcedPidOf(running);
34635
35039
  if (running.throughEditor) {
34636
- await this.handleViaBridge("stop_playing", {});
34637
- return await untilGone(announced === undefined ? [] : [announced]) ? "gone" : "lingering";
35040
+ const asked = await this.handleViaBridge("stop_playing", {});
35041
+ if (asked.isError === true) {
35042
+ running.endedHere = null;
35043
+ running.log.record("warning", `This run was not ended here: the editor could not be asked to stop it (${asked.content[0]?.text ?? "no reason given"}).`);
35044
+ return "unreached";
35045
+ }
35046
+ if (announced === undefined) {
35047
+ return await this.untilTheEditorStopsPlaying() ? "gone" : "lingering";
35048
+ }
35049
+ return await untilGone([announced]) ? "gone" : "lingering";
34638
35050
  }
34639
35051
  if (running.pid === null) {
34640
35052
  return "gone";
@@ -34674,7 +35086,7 @@ class GodotServer {
34674
35086
  return gone ? "gone" : "lingering";
34675
35087
  }
34676
35088
  spawnGame(godotPath, cmdArgs, env) {
34677
- const child = spawn3(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
35089
+ const child = spawn4(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
34678
35090
  const cancelled = callSignal();
34679
35091
  if (cancelled !== undefined) {
34680
35092
  const end = () => {
@@ -34714,7 +35126,6 @@ class GodotServer {
34714
35126
  });
34715
35127
  child.on("exit", (code, signal) => {
34716
35128
  this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
34717
- log.finish();
34718
35129
  started.exitCode = code;
34719
35130
  started.exitSignal = code === null ? signal : null;
34720
35131
  });
@@ -34724,7 +35135,28 @@ class GodotServer {
34724
35135
  `);
34725
35136
  started.exitCode = -1;
34726
35137
  });
34727
- return started;
35138
+ const settled = new Promise((resolve) => {
35139
+ let open = 2;
35140
+ const done = () => {
35141
+ log.finish();
35142
+ resolve();
35143
+ };
35144
+ const closed = () => {
35145
+ open -= 1;
35146
+ if (open === 0) {
35147
+ done();
35148
+ }
35149
+ };
35150
+ child.stdout.once("close", closed);
35151
+ child.stderr.once("close", closed);
35152
+ child.once("exit", () => {
35153
+ setTimeout(done, STREAMS_GRACE_MS).unref();
35154
+ });
35155
+ child.once("error", () => {
35156
+ done();
35157
+ });
35158
+ });
35159
+ return Object.assign(started, { settled });
34728
35160
  }
34729
35161
  async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
34730
35162
  const startedAt = Date.now();
@@ -34806,7 +35238,8 @@ class GodotServer {
34806
35238
  this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
34807
35239
  }
34808
35240
  const project = this.godotBridge.getStatus().projectPath ?? null;
34809
- const note = project === null ? null : readEditorRunNote(project);
35241
+ const recorded = project === null ? null : readEditorRunNote(project);
35242
+ const note = recorded !== null && noteIsAbout(recorded, playing.play) ? recorded : null;
34810
35243
  const picked = {
34811
35244
  pid: null,
34812
35245
  log: new GameLog,
@@ -34863,6 +35296,31 @@ class GodotServer {
34863
35296
  const its = await this.tiedThroughTheTree(run, running);
34864
35297
  return its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
34865
35298
  }
35299
+ async theRunsAmong(run, fresh) {
35300
+ if (fresh.length === 1) {
35301
+ return fresh[0] ?? null;
35302
+ }
35303
+ if (fresh.length === 0 || run === null) {
35304
+ return null;
35305
+ }
35306
+ const pid = run.announcedPid ?? await this.tiedThroughTheTree(run, fresh);
35307
+ return fresh.find((one) => one.pid === pid) ?? null;
35308
+ }
35309
+ async tieThePlayedGame(run) {
35310
+ if (!run.throughEditor || run.errorReport !== undefined || run.announcedPid !== undefined) {
35311
+ return;
35312
+ }
35313
+ const announced = runtimesAnnounced().running;
35314
+ if (this.announcedPidOf(run, announced) !== undefined || !stillRunning(run)) {
35315
+ return;
35316
+ }
35317
+ const among = this.announcedOfTheRunsProject(run, announced).map((one) => one.pid).sort((one, other) => one - other).join(",");
35318
+ if (among === "" || run.treeReadFor === among) {
35319
+ return;
35320
+ }
35321
+ run.treeReadFor = among;
35322
+ await this.tiedThroughTheTree(run, announced);
35323
+ }
34866
35324
  async tiedThroughTheTree(run, announced) {
34867
35325
  if (run.announcedPid !== undefined) {
34868
35326
  return run.announcedPid;
@@ -35063,6 +35521,7 @@ class GodotServer {
35063
35521
  resolve(false);
35064
35522
  });
35065
35523
  });
35524
+ await boot.settled;
35066
35525
  const errors = boot.log.count("error");
35067
35526
  const warnings = boot.log.count("warning");
35068
35527
  return this.jsonTextResponse({
@@ -35204,6 +35663,7 @@ class GodotServer {
35204
35663
  if (!run) {
35205
35664
  return this.createErrorResponse(this.nothingOfOursIsRunning());
35206
35665
  }
35666
+ await this.tieThePlayedGame(run);
35207
35667
  this.drainEditorOutput(run);
35208
35668
  this.drainTranscript(run);
35209
35669
  const going = await this.runStillGoing(run);
@@ -35239,7 +35699,7 @@ class GodotServer {
35239
35699
  notes.push(endedWithoutACode({
35240
35700
  throughEditor: run.throughEditor,
35241
35701
  announced: announced !== undefined || run.announcedPid !== undefined,
35242
- couldAnnounce: run.projectPath !== null && existsSync12(join18(run.projectPath, RUNTIME_AUTOLOAD.path)),
35702
+ couldAnnounce: run.projectPath !== null && existsSync14(join19(run.projectPath, RUNTIME_AUTOLOAD.path)),
35243
35703
  endedHere: typeof run.endedHere === "string",
35244
35704
  pickedUp: run.pickedUp === true
35245
35705
  }));
@@ -35247,6 +35707,9 @@ class GodotServer {
35247
35707
  if (run.pickedUpPlaying === true) {
35248
35708
  notes.push(run.transcript !== null ? "The editor was already playing this when this server reached it, and what it printed before that has been read back from its transcript. It is the run the editor is holding now, which is the one the debug_* tools answer for." : run.consoleFromReport === true ? "The editor was already playing this when this server reached it, and the server that started it left no transcript, so what is below was read back from the file the game's runtime writes its console to, which begins where the runtime started: the engine's own start-up before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for." : "The editor was already playing this when this server reached it, and the server that started it left no transcript, so this is not the run from its start: what it printed before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for.");
35249
35709
  }
35710
+ if (errorsUnread(run)) {
35711
+ notes.push("The editor plays this run and no report from its game's runtime addon has been found, so what the game raised with push_error and the engine's own errors are not counted here: the editor's debugger relays what a game prints and not what it reports. errors counts what was heard, and clean is left out rather than said. A game with the runtime addon writes that report once it announces itself.");
35712
+ }
35250
35713
  if (run.consoleLost === true) {
35251
35714
  const back = this.dapClient?.isConnected() === true;
35252
35715
  notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
@@ -35262,8 +35725,9 @@ class GodotServer {
35262
35725
  pid: run.pid ?? announced ?? run.announcedPid ?? null,
35263
35726
  errors: run.log.count("error"),
35264
35727
  warnings: run.log.count("warning"),
35265
- clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
35728
+ clean: cleanVerdict(run),
35266
35729
  consoleLost: run.consoleLost === true ? true : undefined,
35730
+ errorsUnread: errorsUnread(run) ? true : undefined,
35267
35731
  ...hold,
35268
35732
  endedUnwatched: run.endedUnwatched === true ? true : undefined,
35269
35733
  endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
@@ -35298,6 +35762,7 @@ class GodotServer {
35298
35762
  if (elsewhere !== null) {
35299
35763
  return elsewhere;
35300
35764
  }
35765
+ await this.tieThePlayedGame(stopped);
35301
35766
  this.drainEditorOutput(stopped);
35302
35767
  this.drainTranscript(stopped);
35303
35768
  const wasRunning = await this.runStillGoing(stopped);
@@ -35306,7 +35771,7 @@ class GodotServer {
35306
35771
  const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
35307
35772
  this.logDebug("Stopping the running game");
35308
35773
  const ending = await this.endActiveGame("editor_run stop", wasRunning);
35309
- const refused = ending === "refused";
35774
+ const refused = ending === "refused" || ending === "unreached";
35310
35775
  const ended = children === null || refused ? null : endChildrenAmong(children);
35311
35776
  if (wasRunning && !refused) {
35312
35777
  for (const pid of new Set([endedPid, announcedPid])) {
@@ -35321,7 +35786,7 @@ class GodotServer {
35321
35786
  endedPid,
35322
35787
  throughEditor: stopped.throughEditor,
35323
35788
  exitSignal: stopped.exitSignal,
35324
- errors: stopped.log.count("error")
35789
+ clean: cleanVerdict(stopped)
35325
35790
  });
35326
35791
  return this.jsonTextResponse({
35327
35792
  stopped: verdict.stopped,
@@ -35335,6 +35800,7 @@ class GodotServer {
35335
35800
  errors: stopped.log.count("error"),
35336
35801
  warnings: stopped.log.count("warning"),
35337
35802
  clean: verdict.clean,
35803
+ errorsUnread: errorsUnread(stopped) ? true : undefined,
35338
35804
  note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
35339
35805
  entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
35340
35806
  });
@@ -35547,7 +36013,12 @@ class GodotServer {
35547
36013
  const stillAtMissingPaths = atMissingPaths.filter((name) => !dropped.includes(name));
35548
36014
  const reloading = readNonEmptyString(args, "reloadScript");
35549
36015
  let reloaded = {};
35550
- if (reloading !== undefined) {
36016
+ const reloadingSource = reloading === undefined || projectPath === "" ? null : this.sourceOfClass(projectPath, reloading);
36017
+ if (reloading !== undefined && reloadingSource !== null && isToolScript(reloadingSource)) {
36018
+ reloaded = {
36019
+ problem: `${reloading} is a @tool script, which runs inside the editor, so it is not reloaded from inside a call: editor_launch restart recompiles it.`
36020
+ };
36021
+ } else if (reloading !== undefined) {
35551
36022
  try {
35552
36023
  const answer = asParams(await this.godotBridge.invokeTool("reload_script", { ...args, scriptPath: reloading }));
35553
36024
  const methods = readArray(answer, "methods");
@@ -35555,29 +36026,36 @@ class GodotServer {
35555
36026
  const found = readArray(answer, field);
35556
36027
  return found ? { [field]: found.map(String) } : {};
35557
36028
  };
36029
+ const asResource = (path) => path.startsWith("res://") ? path : `res://${path.replace(/^\/+/, "")}`;
36030
+ const script = asResource(readString(answer, "script") ?? reloading);
36031
+ const spelling = script === asResource(reloading) ? {} : { as: script };
35558
36032
  const failed = answer["failed"];
35559
36033
  if (typeof failed === "number" && failed !== 0) {
35560
36034
  reloaded = {
36035
+ ...spelling,
35561
36036
  ...listed("heldBefore"),
35562
36037
  ...listed("heldConstantsBefore"),
35563
36038
  problem: failedReloadNote({
35564
- script: reloading,
36039
+ script,
35565
36040
  code: failed,
35566
36041
  codeName: readString(answer, "failedAs") ?? "",
35567
36042
  heldBefore: listed("heldBefore")["heldBefore"] ?? [],
35568
36043
  heldAfter: methods?.map(String) ?? [],
35569
36044
  constantsBefore: listed("heldConstantsBefore")["heldConstantsBefore"] ?? [],
35570
36045
  constantsAfter: listed("constants")["constants"] ?? [],
35571
- names: projectPath === "" ? [] : this.classesAScriptNames(projectPath, reloading)
36046
+ names: projectPath === "" ? [] : this.classesAScriptNames(projectPath, script),
36047
+ restored: typeof answer["restored"] === "boolean" ? answer["restored"] : null,
36048
+ restoredAs: readString(answer, "restoredAs") ?? ""
35572
36049
  })
35573
36050
  };
35574
36051
  } else {
35575
36052
  reloaded = methods ? {
36053
+ ...spelling,
35576
36054
  methods: methods.map(String),
35577
36055
  ...listed("heldBefore"),
35578
36056
  ...listed("constants"),
35579
36057
  ...listed("heldConstantsBefore")
35580
- } : { problem: `the editor would not say what ${reloading} has after reloading it` };
36058
+ } : { problem: `the editor would not say what ${script} has after reloading it` };
35581
36059
  }
35582
36060
  } catch (error) {
35583
36061
  reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
@@ -35599,7 +36077,7 @@ class GodotServer {
35599
36077
  notes.push("The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Rescan again on its own, which is the measured cure and needs no change to the declaring script, or restart the editor with editor_launch restart.");
35600
36078
  }
35601
36079
  if (dependents !== null && dependents.notReloaded.length > 0) {
35602
- notes.push(`The scan brought in ${broughtIn.join(", ")}, and ${dependents.notReloaded.map((one) => one.scriptPath).join(", ")} ${dependents.notReloaded.length === 1 ? "names one of them and" : "name them and"} could not be reloaded, so the editor still holds what it compiled while it could not see the class and reports that to the language server. editor_rescan with reloadScript on each, or editor_launch restart.`);
36080
+ notes.push(notReloadedNote(broughtIn, dependents.notReloaded));
35603
36081
  }
35604
36082
  for (const group of uidsDuplicatedOnDisk) {
35605
36083
  notes.push(`${group.join(" and ")} name one UID in their .uid files, so Godot keeps one path for it and every engine warns "UID duplicate detected". Delete the .uid of the one that is a copy and rescan: the editor writes it a UID of its own.`);
@@ -35614,6 +36092,7 @@ class GodotServer {
35614
36092
  stillWorking: busy,
35615
36093
  waitedMs: Date.now() - started,
35616
36094
  waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
36095
+ reloadedAs: reloaded.as,
35617
36096
  heldBeforeReload: reloaded.heldBefore,
35618
36097
  reloadedMethods: reloaded.methods,
35619
36098
  heldConstantsBeforeReload: reloaded.heldConstantsBefore,
@@ -35640,7 +36119,8 @@ class GodotServer {
35640
36119
  if (tool) {
35641
36120
  notReloaded.push({
35642
36121
  scriptPath,
35643
- problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it"
36122
+ problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it",
36123
+ tool: true
35644
36124
  });
35645
36125
  continue;
35646
36126
  }
@@ -35648,9 +36128,10 @@ class GodotServer {
35648
36128
  const answer = asParams(await this.godotBridge.invokeTool("reload_script", { scriptPath }));
35649
36129
  const failed = answer["failed"];
35650
36130
  if (typeof failed === "number" && failed !== 0) {
36131
+ 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";
35651
36132
  notReloaded.push({
35652
36133
  scriptPath,
35653
- problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}`
36134
+ problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}${left}`
35654
36135
  });
35655
36136
  } else if (readArray(answer, "methods") === undefined) {
35656
36137
  notReloaded.push({
@@ -35722,6 +36203,35 @@ class GodotServer {
35722
36203
  }
35723
36204
  };
35724
36205
  }
36206
+ async handleCreateScript(args) {
36207
+ const project = this.project(args);
36208
+ if (!project.ok) {
36209
+ return project.response;
36210
+ }
36211
+ const contained = this.containProjectFiles(args);
36212
+ if (!contained.ok) {
36213
+ return contained.response;
36214
+ }
36215
+ const { op: _op, projectPath: _projectPath, ...params } = contained.value;
36216
+ const created = await this.operation("create_script", params, project.value.path);
36217
+ const declared = created.ok ? readString(created.payload, "class_name") ?? "" : "";
36218
+ if (!created.ok || declared === "") {
36219
+ return this.answer(created);
36220
+ }
36221
+ const rebuilt = await this.rebuildClassCache(project.value.path);
36222
+ const registered = cachedClasses(project.value.path)?.get(declared) === readString(created.payload, "full_path");
36223
+ const answered = this.answer({
36224
+ ...created,
36225
+ payload: {
36226
+ ...created.payload,
36227
+ registered,
36228
+ ...registered ? {} : {
36229
+ registrationNote: rebuilt.ok ? `The class list was rebuilt and does not hold ${declared} at this script, so nothing started next resolves it by name. project_import refresh_classes says what the list holds and why.` : `The class list could not be rebuilt, so nothing started next resolves ${declared} by name: ${rebuilt.message}`
36230
+ }
36231
+ }
36232
+ });
36233
+ return this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
36234
+ }
35725
36235
  async alsoSayWhatTheEditorCannotSee(answered, args) {
35726
36236
  const first = answered.content[0];
35727
36237
  if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
@@ -35850,7 +36360,7 @@ class GodotServer {
35850
36360
  async ownRunOnItsWay(projectPath, budgetMs) {
35851
36361
  const run = await this.currentRun();
35852
36362
  const project = projectPath ?? run?.projectPath ?? null;
35853
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync12(join18(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
36363
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync14(join19(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
35854
36364
  return null;
35855
36365
  }
35856
36366
  const already = runtimesAnnounced().running;
@@ -35874,11 +36384,15 @@ class GodotServer {
35874
36384
  const seconds = Math.round((Date.now() - run.startedAt) / 1000);
35875
36385
  return `The game this server started (${which}) has been running for ${seconds}s and has not announced its runtime, so there is nothing to talk to. A game that keeps the runtime off, as a project's own loader can for some scenes, never announces; editor_output says what it has printed.`;
35876
36386
  }
35877
- const endpoint = await announcedSince(project, new Set(already.map((one) => one.pid)), {
36387
+ const fresh = await announcedSince(project, new Set(already.map((one) => one.pid)), {
35878
36388
  budgetMs: Math.min(budgetMs, remaining),
35879
36389
  accept: (one) => this.isTheGameOf(this.activeProcess, one, project),
35880
36390
  giveUp: () => this.activeProcess !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
35881
36391
  });
36392
+ const endpoint = await this.theRunsAmong(run, fresh);
36393
+ if (fresh.length > 0 && endpoint === null) {
36394
+ return null;
36395
+ }
35882
36396
  if (endpoint !== null) {
35883
36397
  const tiedMeanwhile = () => run.announcedPid !== undefined;
35884
36398
  if (!tiedMeanwhile()) {
@@ -35930,7 +36444,7 @@ class GodotServer {
35930
36444
  }
35931
36445
  }
35932
36446
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
35933
- const screenshotPath = screenshotDir ? join18(screenshotDir, "capture.png") : null;
36447
+ const screenshotPath = screenshotDir ? join19(screenshotDir, "capture.png") : null;
35934
36448
  try {
35935
36449
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
35936
36450
  if (!reply.ok && reply.requestId !== undefined) {
@@ -35971,7 +36485,7 @@ class GodotServer {
35971
36485
  type: "text",
35972
36486
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
35973
36487
  },
35974
- { type: "image", data: readFileSync17(screenshotPath).toString("base64"), mimeType: "image/png" }
36488
+ { type: "image", data: readFileSync18(screenshotPath).toString("base64"), mimeType: "image/png" }
35975
36489
  ]
35976
36490
  };
35977
36491
  } finally {