gdharness 1.1.20 → 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 +1194 -668
  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 -467
  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 -297
  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 +24 -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 +1025 -496
  47. package/package.json +1 -1
package/build/cli.js CHANGED
@@ -7712,6 +7712,14 @@ function alongside(execPath, name) {
7712
7712
  }
7713
7713
  var init_runner = () => {};
7714
7714
 
7715
+ // src/dictionary.ts
7716
+ function emptyRecord() {
7717
+ return Object.create(null);
7718
+ }
7719
+ function dictionary(entries) {
7720
+ return Object.assign(emptyRecord(), entries);
7721
+ }
7722
+
7715
7723
  // src/call-signal.ts
7716
7724
  import { AsyncLocalStorage } from "node:async_hooks";
7717
7725
  function withCallSignal(signal, answer) {
@@ -7725,14 +7733,6 @@ var init_call_signal = __esm(() => {
7725
7733
  current = new AsyncLocalStorage;
7726
7734
  });
7727
7735
 
7728
- // src/dictionary.ts
7729
- function emptyRecord() {
7730
- return Object.create(null);
7731
- }
7732
- function dictionary(entries) {
7733
- return Object.assign(emptyRecord(), entries);
7734
- }
7735
-
7736
7736
  // src/game-log.ts
7737
7737
  import { StringDecoder } from "node:string_decoder";
7738
7738
  function announced(line) {
@@ -7850,6 +7850,72 @@ var init_game_log = __esm(() => {
7850
7850
  };
7851
7851
  });
7852
7852
 
7853
+ // src/engine-run.ts
7854
+ import { spawn as spawn2 } from "node:child_process";
7855
+ function runEngine(executable, args, options = {}) {
7856
+ return new Promise((resolve) => {
7857
+ const log = new GameLog;
7858
+ let failure = null;
7859
+ const signal = callSignal();
7860
+ const child = spawn2(executable, [...args], {
7861
+ stdio: ["ignore", "pipe", "pipe"],
7862
+ windowsHide: true,
7863
+ ...signal === undefined ? {} : { signal }
7864
+ });
7865
+ child.stdout.on("data", (chunk) => {
7866
+ log.append("stdout", chunk);
7867
+ });
7868
+ child.stderr.on("data", (chunk) => {
7869
+ log.append("stderr", chunk);
7870
+ });
7871
+ const { timeout } = options;
7872
+ const timer = timeout === undefined ? undefined : setTimeout(() => {
7873
+ failure ??= `it ran past its ${timeout.said}`;
7874
+ child.kill();
7875
+ }, timeout.ms);
7876
+ child.on("error", (error) => {
7877
+ failure ??= error.name === "AbortError" ? "the call was cancelled" : `it could not be started: ${error.message}`;
7878
+ });
7879
+ child.on("close", (code, closedBy) => {
7880
+ clearTimeout(timer);
7881
+ log.finish();
7882
+ const started = child.pid !== undefined;
7883
+ resolve({ log, exitCode: started ? code : null, exitSignal: closedBy, failure });
7884
+ });
7885
+ });
7886
+ }
7887
+ function howItEnded(run) {
7888
+ if (run.failure !== null) {
7889
+ return run.exitSignal === null ? run.failure : `ended by ${run.exitSignal}, because ${run.failure}`;
7890
+ }
7891
+ return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}`;
7892
+ }
7893
+ var init_engine_run = __esm(() => {
7894
+ init_call_signal();
7895
+ init_game_log();
7896
+ });
7897
+
7898
+ // src/reimport.ts
7899
+ import { createHash } from "node:crypto";
7900
+ import { existsSync as existsSync5, readFileSync as readFileSync3, rmSync as rmSync3, statSync as statSync2, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
7901
+ import { basename as basename2, join as join5 } from "node:path";
7902
+ function forceNextImport(projectPath, resourcePath) {
7903
+ const relative = resourcePath.replace(/^res:\/\//, "");
7904
+ const sidecar = join5(projectPath, `${relative}.import`);
7905
+ if (!existsSync5(sidecar)) {
7906
+ return;
7907
+ }
7908
+ const { atime, mtimeMs } = statSync2(sidecar);
7909
+ writeFileSync3(sidecar, readFileSync3(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
7910
+ utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
7911
+ rmSync3(importRecord(projectPath, resourcePath), { force: true });
7912
+ }
7913
+ function importRecord(projectPath, resourcePath) {
7914
+ const digest = createHash("md5").update(resourcePath).digest("hex");
7915
+ return join5(projectPath, ".godot", "imported", `${basename2(resourcePath)}-${digest}.md5`);
7916
+ }
7917
+ var init_reimport = () => {};
7918
+
7853
7919
  // src/alive.ts
7854
7920
  import process5 from "node:process";
7855
7921
  function alive(pid) {
@@ -7866,12 +7932,12 @@ function alive(pid) {
7866
7932
  var init_alive = () => {};
7867
7933
 
7868
7934
  // src/scratch.ts
7869
- import { existsSync as existsSync5, mkdtempSync, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
7935
+ import { existsSync as existsSync6, mkdtempSync, readdirSync as readdirSync2, rmSync as rmSync4 } from "node:fs";
7870
7936
  import { tmpdir } from "node:os";
7871
- import { join as join5 } from "node:path";
7937
+ import { join as join6 } from "node:path";
7872
7938
  import process6 from "node:process";
7873
7939
  function scratchDirectory(kind) {
7874
- return mkdtempSync(join5(tmpdir(), `gdharness-${kind}-${process6.pid}-`));
7940
+ return mkdtempSync(join6(tmpdir(), `gdharness-${kind}-${process6.pid}-`));
7875
7941
  }
7876
7942
  function sweepAbandonedScratch(directory = tmpdir()) {
7877
7943
  let entries;
@@ -7887,7 +7953,7 @@ function sweepAbandonedScratch(directory = tmpdir()) {
7887
7953
  continue;
7888
7954
  }
7889
7955
  try {
7890
- remove(join5(directory, entry));
7956
+ remove(join6(directory, entry));
7891
7957
  } catch {}
7892
7958
  }
7893
7959
  }
@@ -7913,7 +7979,7 @@ function discard(...paths) {
7913
7979
  return left;
7914
7980
  }
7915
7981
  function remove(path) {
7916
- rmSync3(path, SWEPT);
7982
+ rmSync4(path, SWEPT);
7917
7983
  }
7918
7984
  function tryAgain(path, attempt) {
7919
7985
  if (attempt === 0) {
@@ -7925,7 +7991,7 @@ function tryAgain(path, attempt) {
7925
7991
  lastTryOnExit();
7926
7992
  }
7927
7993
  const after = AGAIN_AFTER_MS[attempt];
7928
- if (after === undefined || !existsSync5(path)) {
7994
+ if (after === undefined || !existsSync6(path)) {
7929
7995
  waiting.delete(path);
7930
7996
  if (after === undefined) {
7931
7997
  atExit.add(path);
@@ -7972,10 +8038,8 @@ var init_scratch = __esm(() => {
7972
8038
  });
7973
8039
 
7974
8040
  // src/headless.ts
7975
- import { execFile as execFile3 } from "node:child_process";
7976
- import { writeFileSync as writeFileSync3 } from "node:fs";
7977
- import { join as join6 } from "node:path";
7978
- import { promisify as promisify3 } from "node:util";
8041
+ import { existsSync as existsSync7, readFileSync as readFileSync4, realpathSync, writeFileSync as writeFileSync4 } from "node:fs";
8042
+ import { dirname as dirname4, join as join7 } from "node:path";
7979
8043
  function snakeCased(params) {
7980
8044
  const result = emptyRecord();
7981
8045
  for (const [key, value] of Object.entries(params)) {
@@ -7984,34 +8048,55 @@ function snakeCased(params) {
7984
8048
  }
7985
8049
  return result;
7986
8050
  }
7987
- function lastJsonObject(stdout) {
7988
- const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("{"));
7989
- for (const line of lines.reverse()) {
7990
- try {
7991
- const parsed = JSON.parse(line);
7992
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
7993
- return parsed;
7994
- }
7995
- } catch {}
7996
- }
7997
- return null;
8051
+ function engineExtras(outcome) {
8052
+ return {
8053
+ ...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
8054
+ ...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
8055
+ ...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
8056
+ };
7998
8057
  }
7999
- function problems(stderr) {
8000
- const log = new GameLog;
8001
- log.append("stderr", stderr);
8002
- log.finish();
8003
- return log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
8058
+ function problemsIn(log) {
8059
+ return log.everything().flatMap((entry) => {
8060
+ if (entry.severity !== "info") {
8061
+ return [entry];
8062
+ }
8063
+ if (entry.source === "stderr" && entry.text.startsWith(OWN_ERROR)) {
8064
+ return [{ ...entry, severity: "error", text: entry.text.slice(OWN_ERROR.length) }];
8065
+ }
8066
+ return [];
8067
+ });
8004
8068
  }
8005
- function reason(stdout, stderr) {
8006
- const own = stderr.split(/\r?\n/).filter((line) => line.startsWith("[ERROR] ")).map((line) => line.slice("[ERROR] ".length));
8069
+ function reported(problems) {
8070
+ const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
8071
+ return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
8072
+ }
8073
+ function reason(log, problems) {
8074
+ const own = log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
8007
8075
  if (own.length > 0) {
8008
- return own.join("; ");
8076
+ return own.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; ");
8077
+ }
8078
+ const errors = problems.filter((entry) => entry.severity === "error").map((entry) => entry.text);
8079
+ if (errors.length > 0) {
8080
+ const rest = errors.length - ERRORS_NAMED;
8081
+ return rest > 0 ? `${errors.slice(0, ERRORS_NAMED).join("; ")}; and ${rest} more error${rest === 1 ? "" : "s"}` : errors.join("; ");
8009
8082
  }
8010
- const engine = problems(stderr).filter((entry) => entry.severity === "error").map((entry) => entry.text);
8011
- if (engine.length > 0) {
8012
- return engine.join("; ");
8083
+ const printed = log.everything().filter((entry) => entry.source === "stdout");
8084
+ return printed.at(-1)?.text ?? "no output at all";
8085
+ }
8086
+ function scriptFault(problems, script) {
8087
+ const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
8088
+ const directories = [...new Set([spelled(dirname4(script)), spelled(resolvedOr(dirname4(script)))])];
8089
+ return problems.find((entry) => {
8090
+ const at = entry.detail.find((line) => line.startsWith("at: "))?.replaceAll("\\", "/").toLowerCase();
8091
+ return entry.severity === "error" && at !== undefined && directories.some((directory) => at.includes(directory));
8092
+ });
8093
+ }
8094
+ function resolvedOr(path) {
8095
+ try {
8096
+ return realpathSync.native(path);
8097
+ } catch {
8098
+ return path;
8013
8099
  }
8014
- return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
8015
8100
  }
8016
8101
  function librariesNotCopied(messages) {
8017
8102
  const found = new Set;
@@ -8023,99 +8108,141 @@ function librariesNotCopied(messages) {
8023
8108
  }
8024
8109
  return [...found];
8025
8110
  }
8026
- function extensionRetryNote(libraries) {
8027
- return `The first import could not load ${libraries.join(" and ")}: on Windows an engine running as the editor ` + "loads a GDExtension from a ~ copy beside the library, and the editor open on this project holds its own " + "copy there, so the first engine started beside it cannot make one. That failed attempt moves the " + "editor's copy aside, so the import was run again and loaded the extension, and the messages are from " + "that second run. The editor's copy is left beside the library as a ~RF….TMP file, which can be deleted " + "once the editor has exited.";
8111
+ function extensionRetryNote(libraries, stillNotLoaded = []) {
8112
+ const second = stillNotLoaded.length === 0 ? "so the import was run again and loaded the extension, and the messages are from that second run." : `so the import was run again, and it still could not load ${stillNotLoaded.join(" and ")}: this answer is from an import without ${stillNotLoaded.length === 1 ? "it" : "them"}, and its messages say what the engine hit.`;
8113
+ return `The first import could not load ${libraries.join(" and ")}: on Windows an engine running as the editor ` + "loads a GDExtension from a ~ copy beside the library, and the editor open on this project holds its own " + "copy there, so the first engine started beside it cannot make one. That failed attempt moves the " + `editor's copy aside, ${second} The editor's copy is left beside the library as a ~RF….TMP file, which ` + "can be deleted once the editor has exited.";
8028
8114
  }
8029
- async function runImport(godotPath, projectPath, once = importOnce) {
8030
- const first = await once(godotPath, projectPath);
8115
+ async function runImport(godotPath, projectPath, options = {}) {
8116
+ const once = options.once ?? importOnce;
8117
+ const pass = () => {
8118
+ for (const target of options.forcing ?? []) {
8119
+ forceNextImport(projectPath, target);
8120
+ }
8121
+ return once(godotPath, projectPath);
8122
+ };
8123
+ const first = await pass();
8031
8124
  const collided = librariesNotCopied(first.messages);
8032
8125
  if (collided.length === 0) {
8033
8126
  return first;
8034
8127
  }
8035
- return { ...await once(godotPath, projectPath), librariesRetried: collided };
8128
+ const second = await pass();
8129
+ const still = librariesNotCopied(second.messages);
8130
+ return {
8131
+ ...second,
8132
+ librariesRetried: collided,
8133
+ ...still.length === 0 ? {} : { librariesNotLoaded: still },
8134
+ extensionNote: extensionRetryNote(collided, still)
8135
+ };
8036
8136
  }
8037
8137
  async function importOnce(godotPath, projectPath) {
8038
8138
  const logDir = scratchDirectory("import");
8139
+ let ran;
8039
8140
  try {
8040
- const { stderr } = await run3(godotPath, ["--headless", "--log-file", join6(logDir, "engine.log"), "--path", projectPath, "--import"], { signal: callSignal() });
8041
- return { ok: true, messages: problems(stderr) };
8042
- } catch (error) {
8043
- if (error instanceof Error && "stdout" in error && "stderr" in error) {
8044
- const failed = error;
8045
- return {
8046
- ok: false,
8047
- message: `the import pass failed (exit ${failed.code ?? "unknown"}): ${reason(failed.stdout, failed.stderr)}`,
8048
- messages: problems(failed.stderr)
8049
- };
8050
- }
8051
- return {
8052
- ok: false,
8053
- message: `the import pass could not be run: ${error instanceof Error ? error.message : String(error)}`,
8054
- messages: []
8055
- };
8141
+ ran = await runEngine(godotPath, [
8142
+ "--headless",
8143
+ "--log-file",
8144
+ join7(logDir, "engine.log"),
8145
+ "--path",
8146
+ projectPath,
8147
+ "--import"
8148
+ ]);
8056
8149
  } finally {
8057
8150
  discard(logDir);
8058
8151
  }
8152
+ const problems = problemsIn(ran.log);
8153
+ if (ran.exitCode === 0 && ran.failure === null) {
8154
+ return { ok: true, ...reported(problems) };
8155
+ }
8156
+ return {
8157
+ ok: false,
8158
+ message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
8159
+ ...reported(problems)
8160
+ };
8059
8161
  }
8060
8162
  async function runOperation(engine, operation, params, projectPath) {
8061
8163
  const paramsDir = scratchDirectory("params");
8062
- const paramsFile = join6(paramsDir, `${operation}.json`);
8063
- writeFileSync3(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
8164
+ const paramsFile = join7(paramsDir, `${operation}.json`);
8165
+ const answerFile = join7(paramsDir, "answer.json");
8166
+ writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
8064
8167
  const args = [
8065
8168
  "--headless",
8066
8169
  "--log-file",
8067
- join6(paramsDir, "engine.log"),
8170
+ join7(paramsDir, "engine.log"),
8068
8171
  "--path",
8069
8172
  projectPath,
8070
8173
  "--script",
8071
8174
  engine.script,
8072
8175
  operation,
8073
8176
  `@file:${paramsFile}`,
8177
+ answerFile,
8074
8178
  ...engine.debug ? ["--debug-godot"] : []
8075
8179
  ];
8076
- let stdout;
8077
- let stderr;
8180
+ let ran;
8181
+ let answer;
8078
8182
  try {
8079
- ({ stdout, stderr } = await run3(engine.godotPath, args, { signal: callSignal() }));
8080
- } catch (error) {
8081
- if (error instanceof Error && "stdout" in error && "stderr" in error) {
8082
- const failed = error;
8083
- return {
8084
- ok: false,
8085
- message: `${operation} failed (exit ${failed.code ?? "unknown"}): ${reason(failed.stdout, failed.stderr)}`,
8086
- messages: problems(failed.stderr)
8087
- };
8088
- }
8183
+ ran = await runEngine(engine.godotPath, args);
8184
+ answer = answerIn(answerFile);
8185
+ } finally {
8186
+ discard(paramsDir);
8187
+ }
8188
+ const problems = problemsIn(ran.log);
8189
+ if (answer.kind === "unreadable") {
8089
8190
  return {
8090
8191
  ok: false,
8091
- message: `${operation} could not be run: ${error instanceof Error ? error.message : String(error)}`,
8092
- messages: []
8192
+ message: `${operation} wrote an answer that could not be read (${answer.why})`,
8193
+ ...reported(problems)
8093
8194
  };
8094
- } finally {
8095
- discard(paramsDir);
8096
8195
  }
8097
- const payload = lastJsonObject(stdout);
8098
- if (payload === null) {
8196
+ if (answer.kind === "none") {
8197
+ const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
8099
8198
  return {
8100
8199
  ok: false,
8101
- message: `${operation} produced no result: ${reason(stdout, stderr)}`,
8102
- messages: problems(stderr)
8200
+ message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
8201
+ ...reported(problems)
8103
8202
  };
8104
8203
  }
8105
- return { ok: true, payload, messages: problems(stderr) };
8204
+ const fault = scriptFault(problems, engine.script);
8205
+ if (fault !== undefined) {
8206
+ return {
8207
+ ok: false,
8208
+ 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"})`,
8209
+ ...reported(problems)
8210
+ };
8211
+ }
8212
+ 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.`;
8213
+ return {
8214
+ ok: true,
8215
+ payload: answer.payload,
8216
+ ...reported(problems),
8217
+ ...afterAnswer === undefined ? {} : { afterAnswer }
8218
+ };
8106
8219
  }
8107
- var run3;
8220
+ function answerIn(file) {
8221
+ if (!existsSync7(file)) {
8222
+ return { kind: "none" };
8223
+ }
8224
+ const text = readFileSync4(file, "utf8");
8225
+ try {
8226
+ const parsed = JSON.parse(text);
8227
+ 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)}` };
8228
+ } catch (error) {
8229
+ return {
8230
+ kind: "unreadable",
8231
+ why: `${error instanceof Error ? error.message : String(error)}: ${text.slice(0, 200)}`
8232
+ };
8233
+ }
8234
+ }
8235
+ var OWN_ERROR = "[ERROR] ", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
8108
8236
  var init_headless = __esm(() => {
8109
- init_call_signal();
8110
- init_game_log();
8237
+ init_engine_run();
8238
+ init_reimport();
8111
8239
  init_scratch();
8112
- run3 = promisify3(execFile3);
8113
8240
  });
8114
8241
 
8115
8242
  // src/update-check.ts
8116
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
8243
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
8117
8244
  import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
8118
- import { join as join7 } from "node:path";
8245
+ import { join as join8 } from "node:path";
8119
8246
  function packageUrl(root) {
8120
8247
  return `${root.replace(/\/+$/, "")}/gdharness/latest`;
8121
8248
  }
@@ -8137,28 +8264,28 @@ function cacheDirectory(environment) {
8137
8264
  };
8138
8265
  const home = set("HOME") ?? homedir3();
8139
8266
  if (process.platform === "win32") {
8140
- return join7(set("LOCALAPPDATA") ?? home, "gdharness");
8267
+ return join8(set("LOCALAPPDATA") ?? home, "gdharness");
8141
8268
  }
8142
8269
  if (process.platform === "darwin") {
8143
- return join7(home, "Library", "Caches", "gdharness");
8270
+ return join8(home, "Library", "Caches", "gdharness");
8144
8271
  }
8145
- return join7(set("XDG_CACHE_HOME") ?? join7(home, ".cache"), "gdharness");
8272
+ return join8(set("XDG_CACHE_HOME") ?? join8(home, ".cache"), "gdharness");
8146
8273
  }
8147
8274
  function cacheFile(environment = process.env) {
8148
8275
  try {
8149
8276
  const directory = cacheDirectory(environment);
8150
8277
  mkdirSync3(directory, { recursive: true, mode: 448 });
8151
- return join7(directory, "update-check.json");
8278
+ return join8(directory, "update-check.json");
8152
8279
  } catch {
8153
- return join7(tmpdir2(), "gdharness-update-check.json");
8280
+ return join8(tmpdir2(), "gdharness-update-check.json");
8154
8281
  }
8155
8282
  }
8156
8283
  function readCache(path) {
8157
8284
  try {
8158
- if (!existsSync6(path)) {
8285
+ if (!existsSync8(path)) {
8159
8286
  return null;
8160
8287
  }
8161
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
8288
+ const parsed = JSON.parse(readFileSync5(path, "utf8"));
8162
8289
  if (typeof parsed !== "object" || parsed === null) {
8163
8290
  return null;
8164
8291
  }
@@ -8175,7 +8302,7 @@ function readCache(path) {
8175
8302
  }
8176
8303
  function writeCache(path, entry) {
8177
8304
  try {
8178
- writeFileSync4(path, JSON.stringify(entry), { encoding: "utf8", mode: 384 });
8305
+ writeFileSync5(path, JSON.stringify(entry), { encoding: "utf8", mode: 384 });
8179
8306
  } catch {}
8180
8307
  }
8181
8308
  function parts(version) {
@@ -8306,7 +8433,7 @@ var init_update_check = __esm(() => {
8306
8433
  });
8307
8434
 
8308
8435
  // src/server-version.ts
8309
- import { readFileSync as readFileSync4 } from "node:fs";
8436
+ import { readFileSync as readFileSync6 } from "node:fs";
8310
8437
  function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
8311
8438
  if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
8312
8439
  return addonDigest !== shippedDigest;
@@ -8348,7 +8475,7 @@ var init_server_version = __esm(() => {
8348
8475
  GODOT_DEBUG_MODE_DEFAULT = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE;
8349
8476
  SERVER_VERSION = (() => {
8350
8477
  try {
8351
- const pkg = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8"));
8478
+ const pkg = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
8352
8479
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
8353
8480
  } catch {
8354
8481
  return "0.0.0";
@@ -8357,7 +8484,7 @@ var init_server_version = __esm(() => {
8357
8484
  });
8358
8485
 
8359
8486
  // src/issues.ts
8360
- import { createHash } from "node:crypto";
8487
+ import { createHash as createHash2 } from "node:crypto";
8361
8488
  import process7 from "node:process";
8362
8489
  function feedbackNotice(every) {
8363
8490
  return JSON.stringify({
@@ -8376,7 +8503,7 @@ function runtime() {
8376
8503
  function defectSignature(where, message) {
8377
8504
  const normalised = `${where}
8378
8505
  ${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
8379
- return createHash("sha256").update(normalised).digest("hex").slice(0, 8);
8506
+ return createHash2("sha256").update(normalised).digest("hex").slice(0, 8);
8380
8507
  }
8381
8508
  function facts(where, message, godotVersion) {
8382
8509
  return [
@@ -8482,8 +8609,11 @@ var init_issues = __esm(() => {
8482
8609
  });
8483
8610
 
8484
8611
  // src/class-cache.ts
8485
- import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
8486
- import { join as join8 } from "node:path";
8612
+ import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync3 } from "node:fs";
8613
+ import { join as join9 } from "node:path";
8614
+ function isToolScript(source) {
8615
+ return TOOL.test(codeOf(source));
8616
+ }
8487
8617
  function declaredClasses(projectPath) {
8488
8618
  const declared = new Map;
8489
8619
  eachScript(projectPath, (script, source) => {
@@ -8496,18 +8626,18 @@ function declaredClasses(projectPath) {
8496
8626
  }
8497
8627
  function eachScript(projectPath, each) {
8498
8628
  const visit = (directory, prefix) => {
8499
- if (existsSync7(join8(directory, ".gdignore"))) {
8629
+ if (existsSync9(join9(directory, ".gdignore"))) {
8500
8630
  return;
8501
8631
  }
8502
8632
  for (const entry of readdirSync3(directory, { withFileTypes: true })) {
8503
8633
  if (entry.name.startsWith(".")) {
8504
8634
  continue;
8505
8635
  }
8506
- const path = join8(directory, entry.name);
8636
+ const path = join9(directory, entry.name);
8507
8637
  if (entry.isDirectory()) {
8508
8638
  visit(path, `${prefix}${entry.name}/`);
8509
8639
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
8510
- each(`res://${prefix}${entry.name}`, readFileSync5(path, "utf8"));
8640
+ each(`res://${prefix}${entry.name}`, readFileSync7(path, "utf8"));
8511
8641
  }
8512
8642
  }
8513
8643
  };
@@ -8523,7 +8653,7 @@ function scriptsNaming(projectPath, classes) {
8523
8653
  }
8524
8654
  const code = codeOf(source);
8525
8655
  if (classesNamedIn(code, named).length > 0) {
8526
- naming.push({ script, tool: /^\s*@tool\b/m.test(code) });
8656
+ naming.push({ script, tool: TOOL.test(code) });
8527
8657
  }
8528
8658
  });
8529
8659
  return naming.sort((a, b) => a.script.localeCompare(b.script));
@@ -8556,12 +8686,12 @@ function codeOf(source) {
8556
8686
  return code;
8557
8687
  }
8558
8688
  function cachedClasses(projectPath) {
8559
- const cache = join8(projectPath, ".godot", "global_script_class_cache.cfg");
8560
- if (!existsSync7(cache)) {
8689
+ const cache = join9(projectPath, ".godot", "global_script_class_cache.cfg");
8690
+ if (!existsSync9(cache)) {
8561
8691
  return null;
8562
8692
  }
8563
8693
  const listed = new Map;
8564
- const text = readFileSync5(cache, "utf8");
8694
+ const text = readFileSync7(cache, "utf8");
8565
8695
  for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
8566
8696
  listed.set(entry[1] ?? "", entry[2] ?? "");
8567
8697
  }
@@ -8604,32 +8734,33 @@ function missingMemberIn(message) {
8604
8734
  if (called !== null) {
8605
8735
  return { kind: "static method", member: called[1] ?? "", type: called[2] ?? "" };
8606
8736
  }
8607
- const inBase = /Cannot find member "([A-Za-z_]\w*)" in base "([A-Za-z_]\w*)((?:\.[A-Za-z_]\w*)*)"/.exec(message);
8737
+ const inBase = /Cannot find member "([A-Za-z_]\w*)" in base "([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?"/.exec(message);
8608
8738
  if (inBase === null) {
8609
8739
  return null;
8610
8740
  }
8611
8741
  const member = inBase[1] ?? "";
8612
8742
  const type = inBase[2] ?? "";
8613
- const within = (inBase[3] ?? "").split(".").filter((part) => part !== "");
8614
- const enumName = within.at(-1);
8743
+ const enumName = inBase[3];
8615
8744
  return enumName === undefined ? { kind: "member", member, type } : { kind: "enum member", member, type, enum: enumName };
8616
8745
  }
8617
8746
  function declaresMember(source, missing) {
8618
8747
  const name = missing.member.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8748
+ const top = String.raw`^(?:@\w+(?:\([^)]*\))?\s+)*`;
8749
+ const declared = (kinds) => new RegExp(String.raw`${top}(?:${kinds})\s+${name}\b`, "m").test(source);
8619
8750
  if (missing.kind === "method") {
8620
- return new RegExp(String.raw`^\s*(?:static\s+)?func\s+${name}\s*\(`, "m").test(source);
8751
+ return new RegExp(String.raw`${top}(?:static\s+)?func\s+${name}\s*\(`, "m").test(source);
8621
8752
  }
8622
8753
  if (missing.kind === "static method") {
8623
- return new RegExp(String.raw`^\s*static\s+func\s+${name}\s*\(`, "m").test(source);
8754
+ return new RegExp(String.raw`${top}static\s+func\s+${name}\s*\(`, "m").test(source);
8624
8755
  }
8625
8756
  if (missing.kind === "enum member") {
8626
8757
  return enumValues(source, missing.enum ?? "").includes(missing.member);
8627
8758
  }
8628
- const kinds = missing.kind === "property" ? "var|const|enum|class|signal|func" : "var|const|enum|class|signal";
8629
- return new RegExp(String.raw`^\s*(?:static\s+)?(?:@\w+(?:\([^)]*\))?\s+)*(?:${kinds})\s+${name}\b`, "m").test(source) || enumValues(source, null).includes(missing.member);
8759
+ const kinds = missing.kind === "property" ? String.raw`(?:static\s+)?(?:var|func)|const|enum|class|signal` : String.raw`static\s+(?:var|func)|const|enum|class`;
8760
+ return declared(kinds) || enumValues(source, null).includes(missing.member);
8630
8761
  }
8631
8762
  function enumValues(source, named) {
8632
- const opening = named === null ? /^\s*enum\s*\{([^}]*)\}/gm : new RegExp(String.raw`^\s*enum\s+${named.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\s*\{([^}]*)\}`, "gm");
8763
+ const opening = named === null ? /^enum\s*\{([^}]*)\}/gm : new RegExp(String.raw`^enum\s+${named.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\s*\{([^}]*)\}`, "gm");
8633
8764
  const values = [];
8634
8765
  for (const [, body] of source.matchAll(opening)) {
8635
8766
  const uncommented = (body ?? "").split(`
@@ -8672,21 +8803,22 @@ function classesNamedIn(source, known) {
8672
8803
  return [...named].sort();
8673
8804
  }
8674
8805
  function contradictedDiagnostics(messages, classes, sourceOf) {
8806
+ const keyOf = (missing) => [missing.type, missing.enum ?? "", missing.member].join(".");
8807
+ const denials = messages.map((message) => missingMemberIn(message)).filter((missing) => missing !== null);
8808
+ const calledOnTheClass = new Set(denials.filter((missing) => missing.kind === "static method").map((missing) => keyOf(missing)));
8675
8809
  const read = new Map;
8676
8810
  const found = new Map;
8677
- for (const message of messages) {
8678
- const missing = missingMemberIn(message);
8679
- const declaredIn = missing === null ? undefined : classes.get(missing.type);
8680
- if (missing === null || declaredIn === undefined) {
8811
+ for (const missing of denials) {
8812
+ const declaredIn = classes.get(missing.type);
8813
+ const key = keyOf(missing);
8814
+ if (declaredIn === undefined || missing.kind === "method" && calledOnTheClass.has(key)) {
8681
8815
  continue;
8682
8816
  }
8683
8817
  if (!read.has(declaredIn)) {
8684
8818
  read.set(declaredIn, sourceOf(declaredIn));
8685
8819
  }
8686
8820
  const source = read.get(declaredIn) ?? null;
8687
- const key = [missing.type, missing.enum ?? "", missing.member].join(".");
8688
- const already = found.get(key);
8689
- if (source !== null && declaresMember(source, missing) && already?.kind !== "static method") {
8821
+ if (source !== null && declaresMember(source, missing)) {
8690
8822
  found.set(key, { ...missing, declaredIn });
8691
8823
  }
8692
8824
  }
@@ -8728,6 +8860,19 @@ function classRemedy(entries) {
8728
8860
  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.";
8729
8861
  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.";
8730
8862
  }
8863
+ function notReloadedNote(broughtIn, notReloaded) {
8864
+ const tried = notReloaded.filter((one) => one.tool !== true).map((one) => one.scriptPath);
8865
+ const tools = notReloaded.filter((one) => one.tool === true).map((one) => one.scriptPath);
8866
+ const all = [...tried, ...tools];
8867
+ const remedies = [];
8868
+ if (tried.length > 0) {
8869
+ remedies.push(`For ${tried.join(", ")}: editor_rescan with reloadScript on ${tried.length === 1 ? "it" : "each"}, or editor_launch restart.`);
8870
+ }
8871
+ if (tools.length > 0) {
8872
+ 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.`);
8873
+ }
8874
+ 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(" ")}`;
8875
+ }
8731
8876
  function failedReloadNote(failed) {
8732
8877
  const missing = (before, after) => before.filter((name) => !after.includes(name));
8733
8878
  const lost = [
@@ -8736,9 +8881,19 @@ function failedReloadNote(failed) {
8736
8881
  ];
8737
8882
  const unchanged = lost.length === 0 && missing(failed.heldAfter, failed.heldBefore).length === 0 && missing(failed.constantsAfter, failed.constantsBefore).length === 0;
8738
8883
  const answered = failed.codeName === "" ? `error ${failed.code}` : `error ${failed.code}, ${failed.codeName}`;
8739
- const copy = unchanged ? "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.`;
8884
+ 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.";
8885
+ let copy;
8886
+ if (failed.restored === null) {
8887
+ 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.`;
8888
+ } else if (!failed.restored) {
8889
+ 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.`;
8890
+ } else if (unchanged) {
8891
+ 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.";
8892
+ } else {
8893
+ 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.`;
8894
+ }
8740
8895
  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.`;
8741
- return `${failed.script} did not compile, so nothing was reloaded: Godot answered ${answered}. ${copy} ` + `script_diagnostics on it gives the errors.${order}`;
8896
+ return `${failed.script} did not compile: Godot answered ${answered}. ${copy} ` + `script_diagnostics on it gives the errors.${order}`;
8742
8897
  }
8743
8898
  function uncachedClassNote(types) {
8744
8899
  if (types.length === 0) {
@@ -8748,9 +8903,9 @@ function uncachedClassNote(types) {
8748
8903
  return `${types.join(", ")} ${one ? "is declared" : "are declared"} in this project and missing from ` + `.godot/global_script_class_cache.cfg, so a game launched now would not resolve ${one ? "it" : "them"} ` + "either. Try editor_rescan first: it asks the editor already running and starts nothing, and it " + "cleared this in 314ms in a second project with a 31-worker fan-out still importing. It is not " + "always enough, because a file another engine has already imported reads as settled and the " + "walk does not look inside it. project_import refresh_classes rewrites the cache from the " + "declarations on disk and does reach that case, at the cost of a short headless engine, which " + "is worth knowing when something else is already running against this project.";
8749
8904
  }
8750
8905
  function cacheWrittenAt(projectPath) {
8751
- const cache = join8(projectPath, ".godot", "global_script_class_cache.cfg");
8906
+ const cache = join9(projectPath, ".godot", "global_script_class_cache.cfg");
8752
8907
  try {
8753
- return statSync2(cache).mtimeMs;
8908
+ return statSync3(cache).mtimeMs;
8754
8909
  } catch {
8755
8910
  return null;
8756
8911
  }
@@ -8762,14 +8917,14 @@ function declaredSince(projectPath, paths, names, since) {
8762
8917
  return false;
8763
8918
  }
8764
8919
  try {
8765
- return statSync2(join8(projectPath, path.slice("res://".length))).mtimeMs > since;
8920
+ return statSync3(join9(projectPath, path.slice("res://".length))).mtimeMs > since;
8766
8921
  } catch {
8767
8922
  return false;
8768
8923
  }
8769
8924
  });
8770
8925
  }
8771
8926
  function cachedAtMissingPaths(cached, projectPath) {
8772
- return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync7(join8(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
8927
+ return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync9(join9(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
8773
8928
  }
8774
8929
  function staleAgainst(cached, projectPath) {
8775
8930
  return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
@@ -8786,9 +8941,10 @@ function unseenByEditor(projectPath, editorHolds) {
8786
8941
  const held = new Set(editorHolds);
8787
8942
  return [...declaredClasses(projectPath)].filter(([name]) => !held.has(name)).map(([className, path]) => ({ className, path })).sort((one, other) => one.className < other.className ? -1 : one.className > other.className ? 1 : 0);
8788
8943
  }
8789
- var DECLARATION;
8944
+ var DECLARATION, TOOL;
8790
8945
  var init_class_cache = __esm(() => {
8791
8946
  DECLARATION = /^(?:@[a-z_]+(?:\([^)\n]*\))?\s+)*class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m;
8947
+ TOOL = /^[ \t]*(?:@[a-z_]+(?:\([^)\n]*\))?[ \t]+)*@tool\b/m;
8792
8948
  });
8793
8949
 
8794
8950
  // node_modules/zod/v4/core/util.js
@@ -15895,11 +16051,11 @@ var init_types = __esm(() => {
15895
16051
  });
15896
16052
 
15897
16053
  // src/paths.ts
15898
- import { realpathSync } from "node:fs";
16054
+ import { realpathSync as realpathSync2 } from "node:fs";
15899
16055
  import { isAbsolute, relative, resolve, sep } from "node:path";
15900
16056
  function realPathOr(path) {
15901
16057
  try {
15902
- return realpathSync.native(path);
16058
+ return realpathSync2.native(path);
15903
16059
  } catch {
15904
16060
  return path;
15905
16061
  }
@@ -15920,6 +16076,12 @@ function resolveWithinProject(projectPath, candidatePath) {
15920
16076
  return { ok: false, reason: "Path contains a null byte." };
15921
16077
  }
15922
16078
  const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
16079
+ if (withoutScheme.startsWith("uid://")) {
16080
+ return {
16081
+ ok: false,
16082
+ 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.`
16083
+ };
16084
+ }
15923
16085
  const scheme = OTHER_SCHEME.exec(withoutScheme);
15924
16086
  if (scheme) {
15925
16087
  return {
@@ -15956,7 +16118,7 @@ var init_paths = __esm(() => {
15956
16118
  });
15957
16119
 
15958
16120
  // src/resources.ts
15959
- import { readFileSync as readFileSync6 } from "node:fs";
16121
+ import { readFileSync as readFileSync8 } from "node:fs";
15960
16122
  import { extname, resolve as resolve2 } from "node:path";
15961
16123
  function ensureProjectPath(getProjectPath) {
15962
16124
  const projectPath = getProjectPath();
@@ -16122,7 +16284,7 @@ function readResourceText(uri, getProjectPath) {
16122
16284
  const parsedUri = parseGodotUri(uri);
16123
16285
  if (parsedUri.kind === "project-info") {
16124
16286
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
16125
- const rawProject = readFileSync6(projectFilePath, "utf-8");
16287
+ const rawProject = readFileSync8(projectFilePath, "utf-8");
16126
16288
  const parsedProject = parseProjectGodot(rawProject);
16127
16289
  return {
16128
16290
  mimeType: "application/json",
@@ -16131,7 +16293,7 @@ function readResourceText(uri, getProjectPath) {
16131
16293
  }
16132
16294
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
16133
16295
  ensureAllowedExtension(parsedUri.kind, filePath);
16134
- const text = readFileSync6(filePath, "utf-8");
16296
+ const text = readFileSync8(filePath, "utf-8");
16135
16297
  return {
16136
16298
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
16137
16299
  text
@@ -16260,42 +16422,42 @@ function readParams(params, key) {
16260
16422
 
16261
16423
  // src/setup.ts
16262
16424
  import { spawnSync } from "node:child_process";
16263
- import { createHash as createHash2 } from "node:crypto";
16425
+ import { createHash as createHash3 } from "node:crypto";
16264
16426
  import {
16265
16427
  cpSync,
16266
- existsSync as existsSync8,
16428
+ existsSync as existsSync10,
16267
16429
  mkdirSync as mkdirSync4,
16268
16430
  readdirSync as readdirSync4,
16269
- readFileSync as readFileSync7,
16431
+ readFileSync as readFileSync9,
16270
16432
  rmdirSync,
16271
- rmSync as rmSync4,
16272
- statSync as statSync3,
16273
- writeFileSync as writeFileSync5
16433
+ rmSync as rmSync5,
16434
+ statSync as statSync4,
16435
+ writeFileSync as writeFileSync6
16274
16436
  } from "node:fs";
16275
- import { dirname as dirname4, join as join9 } from "node:path";
16437
+ import { dirname as dirname5, join as join10 } from "node:path";
16276
16438
  import { fileURLToPath } from "node:url";
16277
16439
  function editorAddonDigest(from = shippedAddonsDirectory()) {
16278
- const hash = createHash2("sha256");
16440
+ const hash = createHash3("sha256");
16279
16441
  const walk = (directory, relative) => {
16280
16442
  for (const name of readdirSync4(directory).sort()) {
16281
16443
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
16282
16444
  continue;
16283
16445
  }
16284
- const path = join9(directory, name);
16446
+ const path = join10(directory, name);
16285
16447
  const inside = `${relative}/${name}`;
16286
- if (statSync3(path).isDirectory()) {
16448
+ if (statSync4(path).isDirectory()) {
16287
16449
  walk(path, inside);
16288
16450
  } else {
16289
16451
  hash.update(`${inside}
16290
16452
  `);
16291
- hash.update(readFileSync7(path));
16453
+ hash.update(readFileSync9(path));
16292
16454
  hash.update(`
16293
16455
  `);
16294
16456
  }
16295
16457
  }
16296
16458
  };
16297
16459
  for (const name of EDITOR_PLUGINS) {
16298
- walk(join9(from, name), name);
16460
+ walk(join10(from, name), name);
16299
16461
  }
16300
16462
  return hash.digest("hex");
16301
16463
  }
@@ -16310,58 +16472,58 @@ function shippedEditorDigest() {
16310
16472
  return shippedDigest ?? undefined;
16311
16473
  }
16312
16474
  function installedEditorDigest(projectPath) {
16313
- const marker = join9(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
16314
- if (!existsSync8(marker)) {
16475
+ const marker = join10(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
16476
+ if (!existsSync10(marker)) {
16315
16477
  return null;
16316
16478
  }
16317
- const said = readFileSync7(marker, "utf8").trim();
16479
+ const said = readFileSync9(marker, "utf8").trim();
16318
16480
  return said === "" ? null : said;
16319
16481
  }
16320
16482
  function shippedAddonsDirectory() {
16321
- return join9(dirname4(fileURLToPath(import.meta.url)), "godot", "addons");
16483
+ return join10(dirname5(fileURLToPath(import.meta.url)), "godot", "addons");
16322
16484
  }
16323
16485
  function shippedOperationsScript() {
16324
- return join9(dirname4(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
16486
+ return join10(dirname5(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
16325
16487
  }
16326
16488
  function installAddons(projectPath, from = shippedAddonsDirectory()) {
16327
16489
  const installed = [];
16328
16490
  for (const name of ADDONS) {
16329
- const source = join9(from, name);
16330
- if (!existsSync8(join9(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
16491
+ const source = join10(from, name);
16492
+ if (!existsSync10(join10(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
16331
16493
  throw new Error(`The package holds no ${name} addon at ${source}.`);
16332
16494
  }
16333
- const target = join9(projectPath, "addons", name);
16334
- const replaced = existsSync8(target);
16335
- rmSync4(target, { recursive: true, force: true });
16336
- mkdirSync4(dirname4(target), { recursive: true });
16495
+ const target = join10(projectPath, "addons", name);
16496
+ const replaced = existsSync10(target);
16497
+ rmSync5(target, { recursive: true, force: true });
16498
+ mkdirSync4(dirname5(target), { recursive: true });
16337
16499
  cpSync(source, target, { recursive: true });
16338
- writeFileSync5(join9(target, VERSION_MARKER), `${SERVER_VERSION}
16500
+ writeFileSync6(join10(target, VERSION_MARKER), `${SERVER_VERSION}
16339
16501
  `);
16340
16502
  installed.push({ name, path: target, replaced });
16341
16503
  }
16342
- writeFileSync5(join9(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
16504
+ writeFileSync6(join10(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
16343
16505
  `);
16344
16506
  return installed;
16345
16507
  }
16346
16508
  function installedAddonVersion(projectPath) {
16347
- const marker = join9(projectPath, "addons", ADDONS[0], VERSION_MARKER);
16348
- if (!existsSync8(marker)) {
16509
+ const marker = join10(projectPath, "addons", ADDONS[0], VERSION_MARKER);
16510
+ if (!existsSync10(marker)) {
16349
16511
  return null;
16350
16512
  }
16351
- const said = readFileSync7(marker, "utf8").trim();
16513
+ const said = readFileSync9(marker, "utf8").trim();
16352
16514
  return said === "" ? null : said;
16353
16515
  }
16354
16516
  function removeAddons(projectPath) {
16355
16517
  const removed = [];
16356
16518
  for (const name of ADDONS) {
16357
- const target = join9(projectPath, "addons", name);
16358
- if (existsSync8(target)) {
16359
- rmSync4(target, { recursive: true, force: true });
16519
+ const target = join10(projectPath, "addons", name);
16520
+ if (existsSync10(target)) {
16521
+ rmSync5(target, { recursive: true, force: true });
16360
16522
  removed.push(target);
16361
16523
  }
16362
16524
  }
16363
- const addons = join9(projectPath, "addons");
16364
- if (removed.length > 0 && existsSync8(addons) && readdirSync4(addons).length === 0) {
16525
+ const addons = join10(projectPath, "addons");
16526
+ if (removed.length > 0 && existsSync10(addons) && readdirSync4(addons).length === 0) {
16365
16527
  rmdirSync(addons);
16366
16528
  removed.push(addons);
16367
16529
  }
@@ -16385,7 +16547,7 @@ function autoloadIsOurs(named) {
16385
16547
  return named === null || named === `res://${RUNTIME_AUTOLOAD.path}`;
16386
16548
  }
16387
16549
  async function setRuntime(engine, projectPath, on) {
16388
- return on ? await runOperation(engine, "add_autoload", { name: RUNTIME_AUTOLOAD.name, path: `res://${RUNTIME_AUTOLOAD.path}`, enabled: true }, projectPath) : await runOperation(engine, "remove_autoload", { name: RUNTIME_AUTOLOAD.name }, projectPath);
16550
+ return on ? await runOperation(engine, "add_autoload", { name: RUNTIME_AUTOLOAD.name, path: `res://${RUNTIME_AUTOLOAD.path}`, global: true }, projectPath) : await runOperation(engine, "remove_autoload", { name: RUNTIME_AUTOLOAD.name }, projectPath);
16389
16551
  }
16390
16552
  function loaderAmong(projectPath, named) {
16391
16553
  let byName = null;
@@ -16395,7 +16557,7 @@ function loaderAmong(projectPath, named) {
16395
16557
  }
16396
16558
  let source = null;
16397
16559
  try {
16398
- source = readFileSync7(join9(projectPath, path), "utf8");
16560
+ source = readFileSync9(join10(projectPath, path), "utf8");
16399
16561
  } catch {}
16400
16562
  if (source?.includes(RUNTIME_AUTOLOAD.path)) {
16401
16563
  return { name: entry, path, exists: true };
@@ -16437,10 +16599,10 @@ function enabledPlugins(settings) {
16437
16599
  function inspectProject(projectPath) {
16438
16600
  const problems = [];
16439
16601
  const addons = ADDONS.map((name) => {
16440
- const directory = join9(projectPath, "addons", name);
16441
- const marker = join9(directory, VERSION_MARKER);
16442
- const installed = existsSync8(directory);
16443
- const version = existsSync8(marker) ? readFileSync7(marker, "utf8").trim() : null;
16602
+ const directory = join10(projectPath, "addons", name);
16603
+ const marker = join10(directory, VERSION_MARKER);
16604
+ const installed = existsSync10(directory);
16605
+ const version = existsSync10(marker) ? readFileSync9(marker, "utf8").trim() : null;
16444
16606
  const current = version === SERVER_VERSION;
16445
16607
  if (!installed) {
16446
16608
  problems.push(`addons/${name} is not installed; run gdharness setup`);
@@ -16449,7 +16611,7 @@ function inspectProject(projectPath) {
16449
16611
  }
16450
16612
  return { name, installed, version, current };
16451
16613
  });
16452
- const settings = parseProjectGodot(readFileSync7(join9(projectPath, "project.godot"), "utf8"));
16614
+ const settings = parseProjectGodot(readFileSync9(join10(projectPath, "project.godot"), "utf8"));
16453
16615
  const pluginsEnabled = enabledPlugins(settings);
16454
16616
  for (const name of EDITOR_PLUGINS) {
16455
16617
  if (!pluginsEnabled.includes(name)) {
@@ -16465,7 +16627,7 @@ function inspectProject(projectPath) {
16465
16627
  const tracked = trackedByGit(projectPath, [...named.values()]);
16466
16628
  const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
16467
16629
  for (const [name, path] of named) {
16468
- if (!existsSync8(join9(projectPath, path))) {
16630
+ if (!existsSync10(join10(projectPath, path))) {
16469
16631
  if (!addonMissing(path)) {
16470
16632
  problems.push(`the ${name} autoload names res://${path}, which is not in this project, so it boots with a missing script; restore that file, or point the autoload at one that is here`);
16471
16633
  }
@@ -16625,7 +16787,7 @@ var init_tool_definitions = __esm(() => {
16625
16787
  };
16626
16788
  PROPERTIES = {
16627
16789
  type: "object",
16628
- 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.',
16790
+ 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.',
16629
16791
  additionalProperties: true
16630
16792
  };
16631
16793
  XY = {
@@ -16780,11 +16942,18 @@ var init_tool_definitions = __esm(() => {
16780
16942
  },
16781
16943
  value: {
16782
16944
  blank: true,
16783
- description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}. "" writes an empty string.'
16945
+ 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.'
16784
16946
  },
16785
16947
  name: { type: "string", description: "Autoload name." },
16786
16948
  path: { type: "string", description: "Autoload script or scene inside the project." },
16787
- enabled: { type: "boolean", description: "Autoloads: register enabled. Default true." },
16949
+ enabled: {
16950
+ type: "boolean",
16951
+ 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."
16952
+ },
16953
+ global: {
16954
+ type: "boolean",
16955
+ 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.`
16956
+ },
16788
16957
  scenePath: SCENE_PATH,
16789
16958
  actionName: { type: "string", description: 'Input action name, such as "jump".' },
16790
16959
  events: INPUT_EVENTS,
@@ -16796,9 +16965,15 @@ var init_tool_definitions = __esm(() => {
16796
16965
  },
16797
16966
  pluginName: { type: "string", description: "Folder name under addons/." },
16798
16967
  busName: { type: "string", description: "Audio bus name." },
16799
- parentBusIndex: { type: "number", description: "Audio buses: the bus to send to. Default 0, Master." },
16968
+ parentBusIndex: {
16969
+ type: "number",
16970
+ 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."
16971
+ },
16800
16972
  busIndex: { type: "number", description: "Audio bus index." },
16801
- effectIndex: { type: "number", description: "Slot on the bus for the effect." },
16973
+ effectIndex: {
16974
+ type: "number",
16975
+ 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."
16976
+ },
16802
16977
  effectType: { type: "string", description: 'Effect class, such as "AudioEffectReverb".' },
16803
16978
  volumeDb: { type: "number", description: "Bus volume in decibels." }
16804
16979
  },
@@ -16817,7 +16992,7 @@ var init_tool_definitions = __esm(() => {
16817
16992
  disable_plugin: { summary: "disable an addon", requires: ["pluginName"] },
16818
16993
  add_audio_bus: { summary: "add an audio bus", requires: ["busName"] },
16819
16994
  set_audio_bus_effect: {
16820
- summary: "add or configure an effect on a bus",
16995
+ summary: "put an effect in a slot on a bus",
16821
16996
  requires: ["busIndex", "effectIndex", "effectType"]
16822
16997
  },
16823
16998
  set_audio_bus_volume: { summary: "set a bus volume", requires: ["busIndex", "volumeDb"] }
@@ -16856,7 +17031,11 @@ var init_tool_definitions = __esm(() => {
16856
17031
  type: "boolean",
16857
17032
  description: "forward: include the engine's own res://. resources. Default false."
16858
17033
  },
16859
- fileTypes: { type: "array", items: { type: "string" }, description: "reverse: extensions to look in." }
17034
+ fileTypes: {
17035
+ type: "array",
17036
+ items: { type: "string" },
17037
+ 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.'
17038
+ }
16860
17039
  },
16861
17040
  requires: ["projectPath", "resourcePath"]
16862
17041
  },
@@ -16901,7 +17080,10 @@ var init_tool_definitions = __esm(() => {
16901
17080
  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",
16902
17081
  requires: []
16903
17082
  },
16904
- uid: { summary: "the UID of one file", requires: ["resourcePath"] },
17083
+ uid: {
17084
+ 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",
17085
+ requires: ["resourcePath"]
17086
+ },
16905
17087
  refresh_uids: {
16906
17088
  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",
16907
17089
  requires: []
@@ -16962,11 +17144,11 @@ var init_tool_definitions = __esm(() => {
16962
17144
  },
16963
17145
  {
16964
17146
  name: "scene_create",
16965
- description: "Creates a scene file, saves one, or saves a copy under a new path. Needs the editor connected.",
17147
+ 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.",
16966
17148
  parameters: {
16967
17149
  projectPath: PROJECT_PATH,
16968
17150
  scenePath: SCENE_PATH,
16969
- rootNodeType: { type: "string", description: "create: the root node class. Default Node2D." },
17151
+ rootNodeType: { type: "string", description: "create: the root node class. Default Node." },
16970
17152
  newPath: { type: "string", description: "save_as: where the copy goes." }
16971
17153
  },
16972
17154
  requires: ["projectPath", "scenePath"],
@@ -16990,7 +17172,7 @@ var init_tool_definitions = __esm(() => {
16990
17172
  },
16991
17173
  {
16992
17174
  name: "scene_node",
16993
- 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.",
17175
+ 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.",
16994
17176
  parameters: {
16995
17177
  projectPath: PROJECT_PATH,
16996
17178
  scenePath: SCENE_PATH,
@@ -17008,7 +17190,10 @@ var init_tool_definitions = __esm(() => {
17008
17190
  type: "boolean",
17009
17191
  description: "get: include properties still at their default. Default false."
17010
17192
  },
17011
- layer: { type: "number", description: "set_tilemap_cells: the TileMap layer. Default 0." },
17193
+ layer: {
17194
+ type: "number",
17195
+ description: "set_tilemap_cells: the layer of a TileMap. Default 0. A TileMapLayer is one layer."
17196
+ },
17012
17197
  cells: TILEMAP_CELLS
17013
17198
  },
17014
17199
  requires: ["projectPath", "scenePath"],
@@ -17019,7 +17204,10 @@ var init_tool_definitions = __esm(() => {
17019
17204
  duplicate: { summary: "copy a node and its children", requires: ["nodePath", "newName"] },
17020
17205
  reparent: { summary: "move a node under another parent", requires: ["nodePath", "newParentPath"] },
17021
17206
  delete: { summary: "remove a node and its children", requires: ["nodePath"] },
17022
- set_tilemap_cells: { summary: "place tiles in a TileMap", requires: ["nodePath", "cells"] }
17207
+ set_tilemap_cells: {
17208
+ summary: "place tiles in a TileMapLayer or TileMap",
17209
+ requires: ["nodePath", "cells"]
17210
+ }
17023
17211
  }
17024
17212
  },
17025
17213
  {
@@ -17107,13 +17295,16 @@ var init_tool_definitions = __esm(() => {
17107
17295
  },
17108
17296
  {
17109
17297
  name: "script_edit",
17110
- 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.",
17298
+ 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.",
17111
17299
  parameters: {
17112
17300
  projectPath: PROJECT_PATH,
17113
17301
  scriptPath: SCRIPT_PATH,
17114
- className: { type: "string", description: "create: a class_name for the script." },
17115
- extends: { type: "string", description: "create: the base class. Default Node." },
17116
- content: { type: "string", description: "create: the whole file, instead of a template." },
17302
+ className: { type: "string", description: "create: a class_name for the script. Not with content." },
17303
+ extends: { type: "string", description: "create: the base class. Default Node. Not with content." },
17304
+ content: {
17305
+ type: "string",
17306
+ description: "create: the whole file, written as given, instead of a template, className and extends."
17307
+ },
17117
17308
  template: {
17118
17309
  type: "string",
17119
17310
  enum: ["singleton", "state_machine", "component", "resource"],
@@ -17311,7 +17502,7 @@ var init_tool_definitions = __esm(() => {
17311
17502
  },
17312
17503
  {
17313
17504
  name: "editor_output",
17314
- 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.",
17505
+ 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.",
17315
17506
  parameters: {
17316
17507
  severity: {
17317
17508
  type: "string",
@@ -17375,7 +17566,7 @@ var init_tool_definitions = __esm(() => {
17375
17566
  },
17376
17567
  reloadScript: {
17377
17568
  type: "string",
17378
- 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."
17569
+ 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."
17379
17570
  }
17380
17571
  },
17381
17572
  requires: ["projectPath"]
@@ -17521,7 +17712,7 @@ var init_tool_definitions = __esm(() => {
17521
17712
  },
17522
17713
  {
17523
17714
  name: "runtime_invoke",
17524
- 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.",
17715
+ 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.",
17525
17716
  parameters: {
17526
17717
  projectPath: RUNNING_PROJECT_PATH,
17527
17718
  pid: RUNNING_PID,
@@ -17533,12 +17724,12 @@ var init_tool_definitions = __esm(() => {
17533
17724
  property: {
17534
17725
  type: "string",
17535
17726
  ops: ["set"],
17536
- 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.'
17727
+ 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.'
17537
17728
  },
17538
17729
  value: {
17539
17730
  ops: ["set"],
17540
17731
  blank: true,
17541
- 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.`
17732
+ 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.`
17542
17733
  },
17543
17734
  method: {
17544
17735
  type: "string",
@@ -17548,7 +17739,7 @@ var init_tool_definitions = __esm(() => {
17548
17739
  args: {
17549
17740
  type: "array",
17550
17741
  ops: ["call"],
17551
- 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.`
17742
+ 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.`
17552
17743
  },
17553
17744
  timeoutMs: {
17554
17745
  type: "integer",
@@ -17626,7 +17817,7 @@ var init_tool_definitions = __esm(() => {
17626
17817
  says: {
17627
17818
  type: "string",
17628
17819
  ops: ["click"],
17629
- 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."
17820
+ 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."
17630
17821
  },
17631
17822
  action: {
17632
17823
  type: "string",
@@ -17659,7 +17850,7 @@ var init_tool_definitions = __esm(() => {
17659
17850
  replace: {
17660
17851
  type: "boolean",
17661
17852
  ops: ["text"],
17662
- 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.'
17853
+ 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.'
17663
17854
  },
17664
17855
  index: {
17665
17856
  type: "number",
@@ -17703,11 +17894,11 @@ var init_tool_definitions = __esm(() => {
17703
17894
  requires: [],
17704
17895
  operations: {
17705
17896
  click: {
17706
- 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",
17897
+ 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",
17707
17898
  requires: []
17708
17899
  },
17709
17900
  choose: {
17710
- 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",
17901
+ 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",
17711
17902
  requires: ["nodePath"]
17712
17903
  },
17713
17904
  action: { summary: "press an action, or hold it", requires: ["action"] },
@@ -17716,7 +17907,7 @@ var init_tool_definitions = __esm(() => {
17716
17907
  requires: ["keycode"]
17717
17908
  },
17718
17909
  text: {
17719
- 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",
17910
+ 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",
17720
17911
  requires: ["text"]
17721
17912
  },
17722
17913
  mouse_click: {
@@ -17755,7 +17946,7 @@ var init_tool_definitions = __esm(() => {
17755
17946
  },
17756
17947
  value: {
17757
17948
  ops: ["until"],
17758
- description: "until: the value to wait for, fitted to the property's type. On a property declared to hold an object, null, which the text null also means; any other value is refused, since the property can never hold it."
17949
+ description: "until: the value to wait for, fitted to the property's type. On a property holding an object, whether declared to or untyped and holding one or null, the text null means null. One declared to hold an object refuses any value other than null, since it can never hold it."
17759
17950
  },
17760
17951
  says: {
17761
17952
  type: "string",
@@ -17804,7 +17995,7 @@ var init_tool_definitions = __esm(() => {
17804
17995
  },
17805
17996
  {
17806
17997
  name: "debug_control",
17807
- 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.",
17998
+ 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.",
17808
17999
  parameters: {},
17809
18000
  requires: [],
17810
18001
  operations: {
@@ -17815,12 +18006,12 @@ var init_tool_definitions = __esm(() => {
17815
18006
  },
17816
18007
  {
17817
18008
  name: "debug_state",
17818
- 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.",
18009
+ 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.",
17819
18010
  parameters: {
17820
18011
  frameId: {
17821
18012
  type: "number",
17822
18013
  ops: ["variables"],
17823
- description: "variables: which frame, from a stack answer. Default the innermost."
18014
+ 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."
17824
18015
  }
17825
18016
  },
17826
18017
  requires: [],
@@ -32587,10 +32778,10 @@ var init_stdio2 = __esm(() => {
32587
32778
  });
32588
32779
 
32589
32780
  // src/runtime-client.ts
32590
- import { existsSync as existsSync10, readdirSync as readdirSync6, readFileSync as readFileSync9, statSync as statSync4, unlinkSync } from "node:fs";
32781
+ import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync11, statSync as statSync5, unlinkSync } from "node:fs";
32591
32782
  import { createConnection } from "node:net";
32592
32783
  import { tmpdir as tmpdir3 } from "node:os";
32593
- import { dirname as dirname7, join as join12, resolve as resolve3 } from "node:path";
32784
+ import { dirname as dirname8, join as join13, resolve as resolve3 } from "node:path";
32594
32785
  import { setTimeout as delay } from "node:timers/promises";
32595
32786
  function runtimeDirectory(variables = process.env) {
32596
32787
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
@@ -32598,20 +32789,20 @@ function runtimeDirectory(variables = process.env) {
32598
32789
  return explicit;
32599
32790
  }
32600
32791
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
32601
- return join12(perUser ?? tmpdir3(), "gdharness");
32792
+ return join13(perUser ?? tmpdir3(), "gdharness");
32602
32793
  }
32603
32794
  function runtimeDirectories(variables = process.env) {
32604
32795
  const candidates = [runtimeDirectory(variables)];
32605
32796
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
32606
32797
  for (const base of fallbacks) {
32607
- candidates.push(join12(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
32798
+ candidates.push(join13(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
32608
32799
  }
32609
32800
  return [...new Set(candidates.map((path) => resolve3(path)))];
32610
32801
  }
32611
32802
  function errorReportOf(pid, directories = runtimeDirectories()) {
32612
32803
  for (const directory of directories) {
32613
- const path = join12(directory, `runtime-${pid}.log`);
32614
- if (existsSync10(path)) {
32804
+ const path = join13(directory, `runtime-${pid}.log`);
32805
+ if (existsSync12(path)) {
32615
32806
  return path;
32616
32807
  }
32617
32808
  }
@@ -32642,7 +32833,7 @@ function reportAlreadyInTranscript(transcript, report) {
32642
32833
  function parseAnnouncement(file, pid) {
32643
32834
  let fields;
32644
32835
  try {
32645
- fields = asParams(JSON.parse(readFileSync9(file, "utf8")));
32836
+ fields = asParams(JSON.parse(readFileSync11(file, "utf8")));
32646
32837
  } catch {
32647
32838
  return { kind: "rubbish" };
32648
32839
  }
@@ -32659,7 +32850,7 @@ function parseAnnouncement(file, pid) {
32659
32850
  };
32660
32851
  let announcedAt;
32661
32852
  try {
32662
- announcedAt = statSync4(file).mtimeMs;
32853
+ announcedAt = statSync5(file).mtimeMs;
32663
32854
  } catch {}
32664
32855
  const when = announcedAt === undefined ? {} : { announcedAt };
32665
32856
  if (protocol !== RUNTIME_PROTOCOL) {
@@ -32700,7 +32891,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
32700
32891
  function wentAway(file, pid) {
32701
32892
  let project = "";
32702
32893
  try {
32703
- const fields = asParams(JSON.parse(readFileSync9(file, "utf8")));
32894
+ const fields = asParams(JSON.parse(readFileSync11(file, "utf8")));
32704
32895
  project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
32705
32896
  } catch {
32706
32897
  return;
@@ -32743,7 +32934,7 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
32743
32934
  }).map((one) => one.file));
32744
32935
  }
32745
32936
  function announcedIn(directory) {
32746
- if (!existsSync10(directory)) {
32937
+ if (!existsSync12(directory)) {
32747
32938
  return [];
32748
32939
  }
32749
32940
  const found = [];
@@ -32752,20 +32943,20 @@ function announcedIn(directory) {
32752
32943
  for (const entry of readdirSync6(directory)) {
32753
32944
  const report = ERROR_REPORT_PATTERN.exec(entry);
32754
32945
  if (report) {
32755
- sweepErrorReport(join12(directory, entry), Number.parseInt(report[1] ?? "", 10));
32946
+ sweepErrorReport(join13(directory, entry), Number.parseInt(report[1] ?? "", 10));
32756
32947
  continue;
32757
32948
  }
32758
32949
  const match = ANNOUNCEMENT_PATTERN.exec(entry);
32759
32950
  if (!match) {
32760
32951
  continue;
32761
32952
  }
32762
- const file = join12(directory, entry);
32953
+ const file = join13(directory, entry);
32763
32954
  const pid = Number.parseInt(match[1] ?? "", 10);
32764
32955
  const running = alive(pid);
32765
32956
  entries.push({ file, pid, running });
32766
32957
  if (running) {
32767
32958
  try {
32768
- candidates.push({ file, pid, writtenAt: statSync4(file).mtimeMs });
32959
+ candidates.push({ file, pid, writtenAt: statSync5(file).mtimeMs });
32769
32960
  } catch {}
32770
32961
  }
32771
32962
  }
@@ -32787,16 +32978,16 @@ function announcedIn(directory) {
32787
32978
  found.push(announced);
32788
32979
  }
32789
32980
  const held = new Set(entries.map((one) => one.file));
32790
- const here = join12(directory, ".");
32981
+ const here = join13(directory, ".");
32791
32982
  for (const file of verdicts.keys()) {
32792
- if (dirname7(file) === here && !held.has(file)) {
32983
+ if (dirname8(file) === here && !held.has(file)) {
32793
32984
  verdicts.delete(file);
32794
32985
  }
32795
32986
  }
32796
32987
  return found;
32797
32988
  }
32798
32989
  async function announcementEnded(pid, directories = runtimeDirectories()) {
32799
- const files = directories.map((directory) => join12(directory, `runtime-${pid}.json`)).filter((file) => existsSync10(file));
32990
+ const files = directories.map((directory) => join13(directory, `runtime-${pid}.json`)).filter((file) => existsSync12(file));
32800
32991
  const first = files[0];
32801
32992
  if (first === undefined) {
32802
32993
  return;
@@ -32822,7 +33013,7 @@ function sweepErrorReport(path, pid, now = Date.now()) {
32822
33013
  return;
32823
33014
  }
32824
33015
  try {
32825
- if (now - statSync4(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
33016
+ if (now - statSync5(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
32826
33017
  unlinkSync(path);
32827
33018
  }
32828
33019
  } catch {}
@@ -32831,13 +33022,13 @@ async function announcedSince(projectPath, before, waiting = {}) {
32831
33022
  const directories = waiting.directories ?? runtimeDirectories();
32832
33023
  const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
32833
33024
  for (;; ) {
32834
- const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && isSameDirectory(endpoint.project.path, projectPath) && (waiting.accept?.(endpoint) ?? true));
32835
- if (fresh !== undefined) {
33025
+ const fresh = discoverRuntimes(directories).filter((endpoint) => !before.has(endpoint.pid) && isSameDirectory(endpoint.project.path, projectPath) && (waiting.accept?.(endpoint) ?? true));
33026
+ if (fresh.length > 0) {
32836
33027
  return fresh;
32837
33028
  }
32838
33029
  const left = until - Date.now();
32839
33030
  if (left <= 0 || waiting.giveUp?.() === true) {
32840
- return null;
33031
+ return [];
32841
33032
  }
32842
33033
  await delay(Math.min(LOOK_EVERY_MS, left));
32843
33034
  }
@@ -33096,14 +33287,14 @@ var init_runtime_client = __esm(() => {
33096
33287
  });
33097
33288
 
33098
33289
  // src/boot-note.ts
33099
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
33100
- import { dirname as dirname8, join as join13 } from "node:path";
33290
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
33291
+ import { dirname as dirname9, join as join14 } from "node:path";
33101
33292
  function bootNotePath(projectPath) {
33102
- return join13(projectPath, ".godot", "gdharness-boot.json");
33293
+ return join14(projectPath, ".godot", "gdharness-boot.json");
33103
33294
  }
33104
33295
  function readBootNote(projectPath) {
33105
33296
  try {
33106
- const noted = readNumber(asParams(JSON.parse(readFileSync10(bootNotePath(projectPath), "utf8"))), "announcedAfterMs");
33297
+ const noted = readNumber(asParams(JSON.parse(readFileSync12(bootNotePath(projectPath), "utf8"))), "announcedAfterMs");
33107
33298
  return noted !== undefined && Number.isFinite(noted) && noted > 0 ? noted : null;
33108
33299
  } catch {
33109
33300
  return null;
@@ -33112,8 +33303,8 @@ function readBootNote(projectPath) {
33112
33303
  function writeBootNote(projectPath, announcedAfterMs) {
33113
33304
  try {
33114
33305
  const path = bootNotePath(projectPath);
33115
- mkdirSync6(dirname8(path), { recursive: true });
33116
- writeFileSync7(path, JSON.stringify({ announcedAfterMs: Math.round(announcedAfterMs), at: new Date().toISOString() }, null, 2), "utf8");
33306
+ mkdirSync6(dirname9(path), { recursive: true });
33307
+ writeFileSync8(path, JSON.stringify({ announcedAfterMs: Math.round(announcedAfterMs), at: new Date().toISOString() }, null, 2), "utf8");
33117
33308
  } catch {}
33118
33309
  }
33119
33310
  function halfAsLongAgain(lastBootMs) {
@@ -33131,10 +33322,10 @@ var init_boot_note = __esm(() => {
33131
33322
  });
33132
33323
 
33133
33324
  // src/breakpoint-note.ts
33134
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync11, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
33135
- import { dirname as dirname9, isAbsolute as isAbsolute2, join as join14, relative as relative2 } from "node:path";
33325
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
33326
+ import { dirname as dirname10, isAbsolute as isAbsolute2, join as join15, relative as relative2 } from "node:path";
33136
33327
  function breakpointNotePath(projectPath) {
33137
- return join14(projectPath, ".godot", "gdharness-breakpoints.json");
33328
+ return join15(projectPath, ".godot", "gdharness-breakpoints.json");
33138
33329
  }
33139
33330
  function writeBreakpointNote(projectPath, held) {
33140
33331
  const path = breakpointNotePath(projectPath);
@@ -33142,7 +33333,7 @@ function writeBreakpointNote(projectPath, held) {
33142
33333
  const inside = held.map((one) => ({ scriptPath: realPathOr(one.scriptPath), lines: one.lines })).filter((one) => one.lines.length > 0 && withinProject(root, one.scriptPath));
33143
33334
  try {
33144
33335
  if (inside.length === 0) {
33145
- rmSync6(path, { force: true });
33336
+ rmSync7(path, { force: true });
33146
33337
  return;
33147
33338
  }
33148
33339
  const note = {
@@ -33151,8 +33342,8 @@ function writeBreakpointNote(projectPath, held) {
33151
33342
  lines: [...one.lines]
33152
33343
  }))
33153
33344
  };
33154
- mkdirSync7(dirname9(path), { recursive: true });
33155
- writeFileSync8(path, `${JSON.stringify(note, null, 2)}
33345
+ mkdirSync7(dirname10(path), { recursive: true });
33346
+ writeFileSync9(path, `${JSON.stringify(note, null, 2)}
33156
33347
  `, "utf8");
33157
33348
  } catch (error) {
33158
33349
  console.error(`[SERVER] Could not record the breakpoints at ${path}: ${errorMessage(error)}`);
@@ -33160,7 +33351,7 @@ function writeBreakpointNote(projectPath, held) {
33160
33351
  }
33161
33352
  function readBreakpointNote(projectPath) {
33162
33353
  try {
33163
- const parsed = JSON.parse(readFileSync11(breakpointNotePath(projectPath), "utf8"));
33354
+ const parsed = JSON.parse(readFileSync13(breakpointNotePath(projectPath), "utf8"));
33164
33355
  if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
33165
33356
  return [];
33166
33357
  }
@@ -33175,7 +33366,7 @@ function readBreakpointNote(projectPath) {
33175
33366
  }
33176
33367
  const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
33177
33368
  if (lines.length > 0) {
33178
- held.push({ scriptPath: realPathOr(join14(projectPath, script)), lines });
33369
+ held.push({ scriptPath: realPathOr(join15(projectPath, script)), lines });
33179
33370
  }
33180
33371
  }
33181
33372
  return held;
@@ -33193,14 +33384,14 @@ var init_breakpoint_note = __esm(() => {
33193
33384
  });
33194
33385
 
33195
33386
  // src/class-note.ts
33196
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
33197
- import { dirname as dirname10, join as join15 } from "node:path";
33387
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "node:fs";
33388
+ import { dirname as dirname11, join as join16 } from "node:path";
33198
33389
  function classNotePath(projectPath) {
33199
- return join15(projectPath, ".godot", "gdharness-classes.json");
33390
+ return join16(projectPath, ".godot", "gdharness-classes.json");
33200
33391
  }
33201
33392
  function readClassNote(projectPath) {
33202
33393
  try {
33203
- const fields = asParams(JSON.parse(readFileSync12(classNotePath(projectPath), "utf8")));
33394
+ const fields = asParams(JSON.parse(readFileSync14(classNotePath(projectPath), "utf8")));
33204
33395
  const rebuiltWith = readArray(fields, "rebuiltWith");
33205
33396
  if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
33206
33397
  return null;
@@ -33217,24 +33408,24 @@ function readClassNote(projectPath) {
33217
33408
  function writeClassNote(projectPath, note) {
33218
33409
  try {
33219
33410
  const path = classNotePath(projectPath);
33220
- mkdirSync8(dirname10(path), { recursive: true });
33221
- writeFileSync9(path, `${JSON.stringify(note, null, 2)}
33411
+ mkdirSync8(dirname11(path), { recursive: true });
33412
+ writeFileSync10(path, `${JSON.stringify(note, null, 2)}
33222
33413
  `, "utf8");
33223
33414
  } catch {}
33224
33415
  }
33225
33416
  var init_class_note = () => {};
33226
33417
 
33227
33418
  // src/config-pin.ts
33228
- import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
33229
- import { join as join16 } from "node:path";
33419
+ import { existsSync as existsSync13, readFileSync as readFileSync15 } from "node:fs";
33420
+ import { join as join17 } from "node:path";
33230
33421
  function versionsNamedInConfig(projectPath) {
33231
- const config = join16(projectPath, ".mcp.json");
33232
- if (!existsSync11(config)) {
33422
+ const config = join17(projectPath, ".mcp.json");
33423
+ if (!existsSync13(config)) {
33233
33424
  return [];
33234
33425
  }
33235
33426
  let text;
33236
33427
  try {
33237
- text = readFileSync13(config, "utf8");
33428
+ text = readFileSync15(config, "utf8");
33238
33429
  JSON.parse(text);
33239
33430
  } catch {
33240
33431
  return [];
@@ -33383,6 +33574,9 @@ var init_ports = __esm(() => {
33383
33574
  // src/dap_client.ts
33384
33575
  import { createConnection as createConnection2 } from "node:net";
33385
33576
  import { setTimeout as delay2 } from "node:timers/promises";
33577
+ function fileKey(path) {
33578
+ return process.platform === "win32" ? path.replaceAll("\\", "/").toLowerCase() : path;
33579
+ }
33386
33580
  function said(body, field) {
33387
33581
  const value = body?.[field];
33388
33582
  return typeof value === "string" ? value : "";
@@ -33399,17 +33593,25 @@ class GodotDAPClient {
33399
33593
  outputBuffer = [];
33400
33594
  onOutput = null;
33401
33595
  maxOutputLines = 1000;
33596
+ outputDropped = 0;
33597
+ outputTaken = 0;
33402
33598
  initialized = false;
33403
33599
  attached = false;
33404
33600
  lastThreadId = 1;
33405
33601
  halt = null;
33406
33602
  holdKnown = false;
33603
+ stops = 0;
33407
33604
  breakpoints = new Map;
33605
+ spelled = new Map;
33408
33606
  inEditor = new Map;
33409
33607
  onBreakpointsChanged = null;
33410
- constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
33608
+ scopesPatienceMs;
33609
+ stepPatienceMs;
33610
+ constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1", patience = {}) {
33411
33611
  this.port = port;
33412
33612
  this.host = host;
33613
+ this.scopesPatienceMs = patience.scopesMs ?? SCOPES_TIMEOUT_MS;
33614
+ this.stepPatienceMs = patience.stepMs ?? STEP_LANDS_WITHIN_MS;
33413
33615
  this.pendingRequests = new Map;
33414
33616
  }
33415
33617
  async connect() {
@@ -33570,6 +33772,7 @@ class GodotDAPClient {
33570
33772
  }
33571
33773
  }
33572
33774
  if (this.outputBuffer.length > this.maxOutputLines) {
33775
+ this.outputDropped += this.outputBuffer.length - this.maxOutputLines;
33573
33776
  this.outputBuffer = this.outputBuffer.slice(this.outputBuffer.length - this.maxOutputLines);
33574
33777
  }
33575
33778
  return;
@@ -33586,6 +33789,7 @@ class GodotDAPClient {
33586
33789
  text: said(body, "text")
33587
33790
  };
33588
33791
  this.holdKnown = true;
33792
+ this.stops += 1;
33589
33793
  return;
33590
33794
  }
33591
33795
  if (eventName === "continued") {
@@ -33602,20 +33806,24 @@ class GodotDAPClient {
33602
33806
  if (typeof path !== "string" || typeof line !== "number") {
33603
33807
  return;
33604
33808
  }
33809
+ const key = fileKey(path);
33810
+ if (!this.spelled.has(key)) {
33811
+ this.spelled.set(key, path);
33812
+ }
33605
33813
  if (said(body, "reason") === "removed") {
33606
- this.inEditor.get(path)?.delete(line);
33607
- const held = this.breakpoints.get(path);
33814
+ this.inEditor.get(key)?.delete(line);
33815
+ const held = this.breakpoints.get(key);
33608
33816
  if (held?.delete(line) === true) {
33609
33817
  if (held.size === 0) {
33610
- this.breakpoints.delete(path);
33818
+ this.breakpoints.delete(key);
33611
33819
  }
33612
33820
  this.onBreakpointsChanged?.(this.breakpointsHeld());
33613
33821
  }
33614
33822
  return;
33615
33823
  }
33616
- const known = this.inEditor.get(path) ?? new Set;
33824
+ const known = this.inEditor.get(key) ?? new Set;
33617
33825
  known.add(line);
33618
- this.inEditor.set(path, known);
33826
+ this.inEditor.set(key, known);
33619
33827
  return;
33620
33828
  }
33621
33829
  if (eventName === "terminated" || eventName === "exited") {
@@ -33698,24 +33906,44 @@ class GodotDAPClient {
33698
33906
  getOutput(clear = false) {
33699
33907
  const lines = [...this.outputBuffer];
33700
33908
  if (clear) {
33909
+ this.outputTaken += lines.length;
33701
33910
  this.outputBuffer = [];
33702
33911
  }
33703
33912
  return lines;
33704
33913
  }
33914
+ outputSoFar() {
33915
+ const output = this.getOutput(false);
33916
+ const notShown = this.outputDropped + this.outputTaken;
33917
+ if (notShown === 0) {
33918
+ return { lines: output.length, output };
33919
+ }
33920
+ const where = [
33921
+ this.outputDropped > 0 ? `${this.outputDropped} fell out of this buffer, which keeps the last ${this.maxOutputLines}` : "",
33922
+ this.outputTaken > 0 ? `${this.outputTaken} were moved into the run's log by an earlier read` : ""
33923
+ ].filter((part) => part !== "");
33924
+ return {
33925
+ lines: output.length,
33926
+ output,
33927
+ notShown,
33928
+ 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.`
33929
+ };
33930
+ }
33705
33931
  async setBreakpoint(filePath, line) {
33706
- const wanted = new Set(this.breakpoints.get(filePath) ?? []);
33932
+ const wanted = new Set(this.breakpoints.get(fileKey(filePath)) ?? []);
33707
33933
  wanted.add(line);
33708
33934
  return await this.sendBreakpoints(filePath, wanted);
33709
33935
  }
33710
33936
  async removeBreakpoint(filePath, line) {
33711
- const wanted = new Set(this.breakpoints.get(filePath) ?? []);
33937
+ const wanted = new Set(this.breakpoints.get(fileKey(filePath)) ?? []);
33712
33938
  wanted.delete(line);
33713
33939
  return await this.sendBreakpoints(filePath, wanted, line);
33714
33940
  }
33715
33941
  async sendBreakpoints(filePath, mine, clearing) {
33716
33942
  await this.ensureConnected();
33717
33943
  await this.initialize();
33718
- const lines = new Set([...mine, ...this.inEditor.get(filePath) ?? []]);
33944
+ const key = fileKey(filePath);
33945
+ this.spelled.set(key, filePath);
33946
+ const lines = new Set([...mine, ...this.inEditor.get(key) ?? []]);
33719
33947
  if (clearing !== undefined) {
33720
33948
  lines.delete(clearing);
33721
33949
  }
@@ -33725,14 +33953,14 @@ class GodotDAPClient {
33725
33953
  breakpoints: sorted.map((breakpointLine) => ({ line: breakpointLine }))
33726
33954
  });
33727
33955
  if (mine.size === 0) {
33728
- this.breakpoints.delete(filePath);
33956
+ this.breakpoints.delete(key);
33729
33957
  } else {
33730
- this.breakpoints.set(filePath, new Set(mine));
33958
+ this.breakpoints.set(key, new Set(mine));
33731
33959
  }
33732
33960
  if (lines.size === 0) {
33733
- this.inEditor.delete(filePath);
33961
+ this.inEditor.delete(key);
33734
33962
  } else {
33735
- this.inEditor.set(filePath, lines);
33963
+ this.inEditor.set(key, lines);
33736
33964
  }
33737
33965
  return answer;
33738
33966
  }
@@ -33741,11 +33969,11 @@ class GodotDAPClient {
33741
33969
  }
33742
33970
  breakpointsInEditor() {
33743
33971
  const theirs = [];
33744
- for (const [scriptPath, lines] of this.inEditor) {
33745
- const mine = this.breakpoints.get(scriptPath);
33972
+ for (const [key, lines] of this.inEditor) {
33973
+ const mine = this.breakpoints.get(key);
33746
33974
  const rest = Array.from(lines).filter((line) => !mine?.has(line)).sort((a, b) => a - b);
33747
33975
  if (rest.length > 0) {
33748
- theirs.push({ scriptPath, lines: rest });
33976
+ theirs.push({ scriptPath: this.spelled.get(key) ?? key, lines: rest });
33749
33977
  }
33750
33978
  }
33751
33979
  return theirs;
@@ -33753,12 +33981,13 @@ class GodotDAPClient {
33753
33981
  async reapplyBreakpoints() {
33754
33982
  const applied = [];
33755
33983
  const refused = [];
33756
- for (const [filePath, lines] of Array.from(this.breakpoints)) {
33984
+ for (const [key, lines] of Array.from(this.breakpoints)) {
33985
+ const filePath = this.spelled.get(key) ?? key;
33757
33986
  try {
33758
33987
  await this.sendBreakpoints(filePath, lines);
33759
33988
  applied.push({ scriptPath: filePath, lines: Array.from(lines).sort((a, b) => a - b) });
33760
33989
  } catch (error) {
33761
- this.breakpoints.delete(filePath);
33990
+ this.breakpoints.delete(key);
33762
33991
  refused.push({
33763
33992
  scriptPath: filePath,
33764
33993
  reason: error instanceof Error ? error.message : String(error)
@@ -33771,19 +34000,21 @@ class GodotDAPClient {
33771
34000
  return { applied, refused };
33772
34001
  }
33773
34002
  breakpointsHeld() {
33774
- return Array.from(this.breakpoints, ([scriptPath, lines]) => ({
33775
- scriptPath,
34003
+ return Array.from(this.breakpoints, ([key, lines]) => ({
34004
+ scriptPath: this.spelled.get(key) ?? key,
33776
34005
  lines: Array.from(lines).sort((a, b) => a - b)
33777
34006
  }));
33778
34007
  }
33779
34008
  holdBreakpoints(held) {
33780
34009
  for (const { scriptPath, lines } of held) {
33781
- const fileBreakpoints = this.breakpoints.get(scriptPath) ?? new Set;
34010
+ const key = fileKey(scriptPath);
34011
+ const fileBreakpoints = this.breakpoints.get(key) ?? new Set;
33782
34012
  for (const line of lines) {
33783
34013
  fileBreakpoints.add(line);
33784
34014
  }
33785
34015
  if (fileBreakpoints.size > 0) {
33786
- this.breakpoints.set(scriptPath, fileBreakpoints);
34016
+ this.breakpoints.set(key, fileBreakpoints);
34017
+ this.spelled.set(key, scriptPath);
33787
34018
  }
33788
34019
  }
33789
34020
  }
@@ -33794,15 +34025,16 @@ class GodotDAPClient {
33794
34025
  this.halt = null;
33795
34026
  this.holdKnown = true;
33796
34027
  }
33797
- async stepOver(threadId) {
34028
+ async step(command) {
33798
34029
  await this.attach();
33799
- const resolvedThreadId = await this.resolveThreadId(threadId);
33800
- await this.sendRequest("next", { threadId: resolvedThreadId });
33801
- }
33802
- async stepInto(threadId) {
33803
- await this.attach();
33804
- const resolvedThreadId = await this.resolveThreadId(threadId);
33805
- await this.sendRequest("stepIn", { threadId: resolvedThreadId });
34030
+ const resolvedThreadId = await this.resolveThreadId();
34031
+ const before = this.stops;
34032
+ await this.sendRequest(command, { threadId: resolvedThreadId });
34033
+ const deadline = Date.now() + this.stepPatienceMs;
34034
+ while (this.stops === before && this.connected && Date.now() < deadline) {
34035
+ await delay2(50);
34036
+ }
34037
+ return this.stops !== before;
33806
34038
  }
33807
34039
  async getStackTrace(threadId) {
33808
34040
  await this.attach();
@@ -33828,16 +34060,16 @@ class GodotDAPClient {
33828
34060
  } catch {}
33829
34061
  await delay2(200);
33830
34062
  }
33831
- return [];
34063
+ return null;
33832
34064
  }
33833
34065
  async getScopes(frameId) {
33834
34066
  await this.attach();
33835
34067
  const frames = await this.getStackTrace();
33836
34068
  const frame = frameId === undefined ? frames[0] : frames.find((each) => each["id"] === frameId);
33837
34069
  if (frame === undefined) {
33838
- return [];
34070
+ 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`);
33839
34071
  }
33840
- const deadline = Date.now() + SCOPES_TIMEOUT_MS;
34072
+ const deadline = Date.now() + this.scopesPatienceMs;
33841
34073
  let scopes = [];
33842
34074
  while (scopes.length === 0 && Date.now() < deadline) {
33843
34075
  try {
@@ -33849,6 +34081,9 @@ class GodotDAPClient {
33849
34081
  } catch {}
33850
34082
  await delay2(200);
33851
34083
  }
34084
+ if (scopes.length === 0) {
34085
+ 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`);
34086
+ }
33852
34087
  const named = [];
33853
34088
  for (const scope of scopes) {
33854
34089
  const reference = scope["variablesReference"];
@@ -33912,9 +34147,8 @@ async function handleDAPTool(client, toolName, args) {
33912
34147
  try {
33913
34148
  switch (toolName) {
33914
34149
  case "dap_get_output": {
33915
- const output = client.getOutput(false);
33916
34150
  return {
33917
- content: [{ type: "text", text: JSON.stringify({ lines: output.length, output }, null, 2) }]
34151
+ content: [{ type: "text", text: JSON.stringify(client.outputSoFar(), null, 2) }]
33918
34152
  };
33919
34153
  }
33920
34154
  case "dap_set_breakpoint": {
@@ -33941,19 +34175,13 @@ async function handleDAPTool(client, toolName, args) {
33941
34175
  }
33942
34176
  case "dap_step_over":
33943
34177
  case "dap_step_into": {
33944
- if (toolName === "dap_step_into") {
33945
- await client.stepInto();
33946
- } else {
33947
- await client.stepOver();
33948
- }
33949
- return {
33950
- content: [
33951
- {
33952
- type: "text",
33953
- text: JSON.stringify({ stepped: true, stack: await client.getStackTrace() }, null, 2)
33954
- }
33955
- ]
34178
+ const heldAgain = await client.step(toolName === "dap_step_into" ? "stepIn" : "next");
34179
+ const answer = heldAgain ? { stepped: true, heldAgain, stack: await client.getStackTrace() } : {
34180
+ stepped: true,
34181
+ heldAgain,
34182
+ 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.`
33956
34183
  };
34184
+ return { content: [{ type: "text", text: JSON.stringify(answer, null, 2) }] };
33957
34185
  }
33958
34186
  case "dap_get_stack_trace": {
33959
34187
  const stack = await client.getStackTrace();
@@ -33964,8 +34192,14 @@ async function handleDAPTool(client, toolName, args) {
33964
34192
  case "dap_get_variables": {
33965
34193
  const frameId = typeof safeArgs.frameId === "number" ? safeArgs.frameId : undefined;
33966
34194
  const scopes = await client.getScopes(frameId);
34195
+ const missing = scopes.filter((scope) => scope.variables === null).map((scope) => scope.name);
34196
+ const answer = missing.length === 0 ? { scopes } : {
34197
+ scopes,
34198
+ valuesMissing: missing,
34199
+ 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.`
34200
+ };
33967
34201
  return {
33968
- content: [{ type: "text", text: JSON.stringify({ scopes }, null, 2) }]
34202
+ content: [{ type: "text", text: JSON.stringify(answer, null, 2) }]
33969
34203
  };
33970
34204
  }
33971
34205
  default:
@@ -33973,18 +34207,22 @@ async function handleDAPTool(client, toolName, args) {
33973
34207
  }
33974
34208
  } catch (error) {
33975
34209
  const message = error instanceof Error ? error.message : String(error);
33976
- 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.";
34210
+ 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.";
33977
34211
  return {
33978
34212
  isError: true,
33979
- content: [{ type: "text", text: `DAP tool '${toolName}' failed: ${message}. ${cure}` }]
34213
+ content: [
34214
+ { type: "text", text: `DAP tool '${toolName}' failed: ${message}.${cure === "" ? "" : ` ${cure}`}` }
34215
+ ]
33980
34216
  };
33981
34217
  }
33982
34218
  }
33983
- var DEFAULT_DAP_PORT = 6006, DAP_REQUEST_TIMEOUT_MS = 1e4, SCOPES_TIMEOUT_MS = 20000, VARIABLES_POLL_TIMEOUT_MS = 2000;
34219
+ var DEFAULT_DAP_PORT = 6006, DAP_REQUEST_TIMEOUT_MS = 1e4, SCOPES_TIMEOUT_MS = 20000, STEP_LANDS_WITHIN_MS = 1e4, VARIABLES_POLL_TIMEOUT_MS = 2000, ReadRefusal;
33984
34220
  var init_dap_client = __esm(() => {
33985
34221
  init_errors();
33986
34222
  init_framing();
33987
34223
  init_ports();
34224
+ ReadRefusal = class ReadRefusal extends Refusal {
34225
+ };
33988
34226
  });
33989
34227
 
33990
34228
  // src/desktop.ts
@@ -33994,25 +34232,25 @@ var init_desktop = () => {};
33994
34232
  // src/editor-log.ts
33995
34233
  import {
33996
34234
  closeSync,
33997
- existsSync as existsSync12,
34235
+ existsSync as existsSync14,
33998
34236
  mkdirSync as mkdirSync9,
33999
34237
  openSync,
34000
- readFileSync as readFileSync14,
34238
+ readFileSync as readFileSync16,
34001
34239
  readSync,
34002
- rmSync as rmSync7,
34003
- statSync as statSync5,
34004
- writeFileSync as writeFileSync10
34240
+ rmSync as rmSync8,
34241
+ statSync as statSync6,
34242
+ writeFileSync as writeFileSync11
34005
34243
  } from "node:fs";
34006
- import { dirname as dirname11, join as join17 } from "node:path";
34244
+ import { dirname as dirname12, join as join18 } from "node:path";
34007
34245
  function editorLogPath(projectPath) {
34008
- return join17(projectPath, ".godot", "gdharness-editor.log");
34246
+ return join18(projectPath, ".godot", "gdharness-editor.log");
34009
34247
  }
34010
34248
  function editorLogNotePath(projectPath) {
34011
- return join17(projectPath, ".godot", "gdharness-editor.json");
34249
+ return join18(projectPath, ".godot", "gdharness-editor.json");
34012
34250
  }
34013
34251
  function readEditorLogNote(projectPath) {
34014
34252
  try {
34015
- const fields = asParams(JSON.parse(readFileSync14(editorLogNotePath(projectPath), "utf8")));
34253
+ const fields = asParams(JSON.parse(readFileSync16(editorLogNotePath(projectPath), "utf8")));
34016
34254
  const pid = readNumber(fields, "pid");
34017
34255
  const startedAt = readString(fields, "startedAt");
34018
34256
  return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
@@ -34023,16 +34261,16 @@ function readEditorLogNote(projectPath) {
34023
34261
  function clearEditorLog(projectPath) {
34024
34262
  try {
34025
34263
  const path = editorLogPath(projectPath);
34026
- mkdirSync9(dirname11(path), { recursive: true });
34027
- rmSync7(path, { force: true });
34028
- rmSync7(editorLogNotePath(projectPath), { force: true });
34264
+ mkdirSync9(dirname12(path), { recursive: true });
34265
+ rmSync8(path, { force: true });
34266
+ rmSync8(editorLogNotePath(projectPath), { force: true });
34029
34267
  } catch {}
34030
34268
  }
34031
34269
  function writeEditorLogNote(projectPath, pid) {
34032
34270
  try {
34033
34271
  const path = editorLogNotePath(projectPath);
34034
- mkdirSync9(dirname11(path), { recursive: true });
34035
- writeFileSync10(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
34272
+ mkdirSync9(dirname12(path), { recursive: true });
34273
+ writeFileSync11(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
34036
34274
  } catch {}
34037
34275
  }
34038
34276
  function theConsoleWasNotCaptured(why, projectPath) {
@@ -34053,7 +34291,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
34053
34291
  return { kind: "older server", notedPid: noted?.pid ?? null };
34054
34292
  }
34055
34293
  const path = editorLogPath(projectPath);
34056
- if (!existsSync12(path)) {
34294
+ if (!existsSync14(path)) {
34057
34295
  return { kind: "not written" };
34058
34296
  }
34059
34297
  const log = new GameLog;
@@ -34063,9 +34301,9 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
34063
34301
  return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
34064
34302
  }
34065
34303
  function theFirstOf(path) {
34066
- const whole = statSync5(path).size;
34304
+ const whole = statSync6(path).size;
34067
34305
  if (whole <= MOST_CONSOLE_BYTES) {
34068
- return { text: readFileSync14(path, "utf8") };
34306
+ return { text: readFileSync16(path, "utf8") };
34069
34307
  }
34070
34308
  const handle = openSync(path, "r");
34071
34309
  try {
@@ -37759,13 +37997,17 @@ function parseJUnit(xml) {
37759
37997
  const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
37760
37998
  const suites = suiteElements.map((suite) => {
37761
37999
  const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
38000
+ const failures = cases.filter((entry) => entry.status === "failed").length;
38001
+ const errors = cases.filter((entry) => entry.status === "error").length;
37762
38002
  return {
37763
38003
  name: suite.attributes["name"] ?? "",
37764
38004
  path: pathOf(suite),
37765
38005
  tests: cases.length,
37766
38006
  discovered: count(suite.attributes, "tests"),
37767
- failures: cases.filter((entry) => entry.status === "failed").length,
37768
- errors: cases.filter((entry) => entry.status === "error").length,
38007
+ failures,
38008
+ errors,
38009
+ hookFailures: Math.max(0, count(suite.attributes, "failures") - failures),
38010
+ hookErrors: Math.max(0, count(suite.attributes, "errors") - errors),
37769
38011
  skipped: cases.filter((entry) => entry.status === "skipped").length,
37770
38012
  time: count(suite.attributes, "time"),
37771
38013
  cases
@@ -37776,11 +38018,19 @@ function parseJUnit(xml) {
37776
38018
  tests: all.length,
37777
38019
  failures: all.filter((entry) => entry.status === "failed").length,
37778
38020
  errors: all.filter((entry) => entry.status === "error").length,
38021
+ hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
38022
+ hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
37779
38023
  skipped: all.filter((entry) => entry.status === "skipped").length,
37780
38024
  time: suites.reduce((sum, suite) => sum + suite.time, 0),
37781
38025
  suites
37782
38026
  };
37783
38027
  }
38028
+ function hookFailuresPrinted(printed) {
38029
+ return printed.flatMap((line) => {
38030
+ const found = HOOK_FAILURE.exec(line);
38031
+ return found === null ? [] : [{ hook: found[2] ?? "", path: found[3] ?? "", line: Number(found[4]), message: found[1] ?? "" }];
38032
+ });
38033
+ }
37784
38034
  function whyNoReport(printed, asked) {
37785
38035
  const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
37786
38036
  if (missing !== undefined) {
@@ -37845,13 +38095,17 @@ function withActualsPrinted(failed, printed) {
37845
38095
  at: match[5] ?? "",
37846
38096
  ...readDiff(match[1] ?? "")
37847
38097
  }));
38098
+ const used = new Set;
37848
38099
  return failed.map((entry) => {
37849
38100
  if (entry.detail === null) {
37850
38101
  return entry;
37851
38102
  }
37852
38103
  let detail = entry.detail;
37853
38104
  let recovered = false;
37854
- for (const diff of diffs) {
38105
+ for (const [index, diff] of diffs.entries()) {
38106
+ if (used.has(index)) {
38107
+ continue;
38108
+ }
37855
38109
  const wrong = ` but was
37856
38110
  '${diff.merged.replaceAll("\r", "")}'${diff.suffix}
37857
38111
  at ${diff.at}`;
@@ -37860,6 +38114,7 @@ function withActualsPrinted(failed, printed) {
37860
38114
  '${diff.actual}'${diff.suffix}
37861
38115
  at ${diff.at}`);
37862
38116
  recovered = true;
38117
+ used.add(index);
37863
38118
  }
37864
38119
  }
37865
38120
  const quoted = QUOTED_EQUALITY.exec(detail);
@@ -37911,7 +38166,7 @@ function orphansPrinted(printed) {
37911
38166
  suites
37912
38167
  };
37913
38168
  }
37914
- var MalformedReportError, ENTITIES, ATTRIBUTE, NOTHING_RAN, NO_SUCH_PATH, DISCOVERY_FAILED = "Script errors were detected during test discovery!", ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, IGNORING_CASE = " \\(ignoring case\\)", PRINTED_ACTUAL, QUOTED_EQUALITY, COLOUR2, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RENDERED_AWAY_NOTE = '[gdharness: the two sides print alike, so what tells them apart is something gdUnit4 does not print. The usual cause is BBCode in the expected string, which gdUnit4 renders as formatting instead of showing; the value after "but was" is the one the assertion found.]', MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary";
38169
+ var MalformedReportError, ENTITIES, ATTRIBUTE, HOOK_FAILURE, NOTHING_RAN, NO_SUCH_PATH, DISCOVERY_FAILED = "Script errors were detected during test discovery!", ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, IGNORING_CASE = " \\(ignoring case\\)", PRINTED_ACTUAL, QUOTED_EQUALITY, COLOUR2, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RENDERED_AWAY_NOTE = '[gdharness: the two sides print alike, so what tells them apart is something gdUnit4 does not print. The usual cause is BBCode in the expected string, which gdUnit4 renders as formatting instead of showing; the value after "but was" is the one the assertion found.]', MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary";
37915
38170
  var init_junit = __esm(() => {
37916
38171
  MalformedReportError = class MalformedReportError extends Error {
37917
38172
  constructor(message) {
@@ -37921,8 +38176,9 @@ var init_junit = __esm(() => {
37921
38176
  };
37922
38177
  ENTITIES = { lt: "<", gt: ">", amp: "&", quot: '"', apos: "'" };
37923
38178
  ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
38179
+ HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
37924
38180
  NOTHING_RAN = /no test cases found/i;
37925
- NO_SUCH_PATH = /given directory or file does not exists:\s*(\S+)/i;
38181
+ NO_SUCH_PATH = /given directory or file does not exists:\s*(.+?)\s*$/i;
37926
38182
  ESCAPE = String.fromCharCode(27);
37927
38183
  PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)(${IGNORING_CASE})?((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
37928
38184
  QUOTED_EQUALITY = new RegExp(`^Expecting(?: error message)?:\\n '([\\s\\S]*)'\\n but was\\n '([\\s\\S]*)'(?:${IGNORING_CASE})?\\n\\tat `);
@@ -37940,15 +38196,15 @@ var init_junit = __esm(() => {
37940
38196
  "<DEL>": ""
37941
38197
  };
37942
38198
  PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
37943
- RUNNING_SUITE = /Run Test Suite:\s*(\S+)/;
38199
+ RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
37944
38200
  ORPHANS = /(\d+)\s+orphans/;
37945
38201
  });
37946
38202
 
37947
38203
  // src/lsp_client.ts
37948
- import { realpathSync as realpathSync2 } from "node:fs";
38204
+ import { existsSync as existsSync15, realpathSync as realpathSync3 } from "node:fs";
37949
38205
  import { readFile, realpath } from "node:fs/promises";
37950
38206
  import { createConnection as createConnection3 } from "node:net";
37951
- import { dirname as dirname12, resolve as resolve4 } from "node:path";
38207
+ import { dirname as dirname13, join as join19, resolve as resolve4 } from "node:path";
37952
38208
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
37953
38209
  function diagnosticsKey(uri) {
37954
38210
  let decoded;
@@ -37961,7 +38217,7 @@ function diagnosticsKey(uri) {
37961
38217
  return decoded;
37962
38218
  }
37963
38219
  try {
37964
- return realpathSync2.native(fileURLToPath3(decoded)).toLowerCase();
38220
+ return realpathSync3.native(fileURLToPath3(decoded)).toLowerCase();
37965
38221
  } catch {
37966
38222
  return decoded.toLowerCase();
37967
38223
  }
@@ -37978,8 +38234,10 @@ class GodotLSPClient {
37978
38234
  connectPromise = null;
37979
38235
  initialized = false;
37980
38236
  rootPath = null;
38237
+ servedWorkspace = null;
37981
38238
  diagnosticsWaiters = new Map;
37982
38239
  documentVersions = new Map;
38240
+ documentTurns = new Map;
37983
38241
  constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1") {
37984
38242
  this.port = port;
37985
38243
  this.host = host;
@@ -38076,7 +38334,7 @@ class GodotLSPClient {
38076
38334
  if (this.initialized) {
38077
38335
  return;
38078
38336
  }
38079
- const rootPath = this.rootPath ?? dirname12(resolve4(filePath));
38337
+ const rootPath = this.rootPath ?? dirname13(resolve4(filePath));
38080
38338
  await this.initialize(rootPath);
38081
38339
  }
38082
38340
  async sendRequest(method, params) {
@@ -38151,6 +38409,10 @@ class GodotLSPClient {
38151
38409
  }
38152
38410
  }
38153
38411
  }
38412
+ if (message["method"] === "gdscript_client/changeWorkspace") {
38413
+ const path = message["params"]?.["path"];
38414
+ this.servedWorkspace = typeof path === "string" ? path : null;
38415
+ }
38154
38416
  if (message["method"] === "textDocument/publishDiagnostics") {
38155
38417
  const params = message["params"];
38156
38418
  const paramsObject = params && typeof params === "object" ? params : null;
@@ -38253,10 +38515,14 @@ class GodotLSPClient {
38253
38515
  this.sendNotification("textDocument/didClose", { textDocument: { uri } });
38254
38516
  } catch {}
38255
38517
  }
38518
+ forgetServedWorkspace() {
38519
+ this.servedWorkspace = null;
38520
+ }
38256
38521
  async initialize(rootPath) {
38257
38522
  await this.ensureConnected();
38258
38523
  const resolvedRootPath = resolve4(rootPath);
38259
38524
  const rootUri = pathToFileURL(resolvedRootPath).href;
38525
+ this.forgetServedWorkspace();
38260
38526
  const result = await this.sendRequest("initialize", {
38261
38527
  processId: process.pid,
38262
38528
  rootPath: resolvedRootPath,
@@ -38282,60 +38548,82 @@ class GodotLSPClient {
38282
38548
  }
38283
38549
  ]
38284
38550
  });
38551
+ const served = this.servedWorkspace;
38552
+ if (served !== null && !isSameDirectory(served, resolvedRootPath)) {
38553
+ throw new AnotherProjectsServer(this.port, served, resolvedRootPath);
38554
+ }
38285
38555
  this.sendNotification("initialized", {});
38286
38556
  this.initialized = true;
38287
38557
  this.rootPath = resolvedRootPath;
38288
38558
  return result;
38289
38559
  }
38290
- async getDiagnostics(filePath, content) {
38291
- await this.ensureConnected();
38292
- await this.ensureInitializedForFile(filePath);
38560
+ async withDocument(filePath, work) {
38293
38561
  const key = diagnosticsKey(this.toFileUri(filePath));
38294
- const diagnosticsPromise = new Promise((resolveDiagnostics, rejectDiagnostics) => {
38295
- const existing = this.diagnosticsWaiters.get(key);
38296
- if (existing) {
38297
- clearTimeout(existing.timer);
38298
- }
38299
- const timer = setTimeout(() => {
38300
- this.diagnosticsWaiters.delete(key);
38301
- 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."));
38302
- }, DIAGNOSTICS_TIMEOUT_MS);
38303
- this.diagnosticsWaiters.set(key, {
38304
- resolve: resolveDiagnostics,
38305
- reject: rejectDiagnostics,
38306
- timer
38307
- });
38562
+ const before = this.documentTurns.get(key) ?? Promise.resolve();
38563
+ let done = () => {};
38564
+ const mine = new Promise((finished) => {
38565
+ done = finished;
38308
38566
  });
38309
- let opened = null;
38567
+ const turn = before.then(() => mine);
38568
+ this.documentTurns.set(key, turn);
38569
+ await before;
38310
38570
  try {
38311
- opened = this.syncDocument(filePath, content);
38312
- return await diagnosticsPromise;
38313
- } catch (error) {
38314
- const waiter = this.diagnosticsWaiters.get(key);
38315
- if (waiter) {
38316
- clearTimeout(waiter.timer);
38317
- this.diagnosticsWaiters.delete(key);
38318
- }
38319
- throw error;
38571
+ return await work();
38320
38572
  } finally {
38321
- if (opened !== null) {
38322
- this.closeDocument(opened);
38573
+ done();
38574
+ if (this.documentTurns.get(key) === turn) {
38575
+ this.documentTurns.delete(key);
38323
38576
  }
38324
38577
  }
38325
38578
  }
38326
- async getCompletions(filePath, content, line, character) {
38579
+ async getDiagnostics(filePath, content) {
38327
38580
  await this.ensureConnected();
38328
38581
  await this.ensureInitializedForFile(filePath);
38329
- const uri = this.syncDocument(filePath, content);
38330
- let result;
38331
- try {
38332
- result = await this.sendRequest("textDocument/completion", {
38333
- textDocument: { uri },
38334
- position: { line, character }
38582
+ return this.withDocument(filePath, async () => {
38583
+ const key = diagnosticsKey(this.toFileUri(filePath));
38584
+ const diagnosticsPromise = new Promise((resolveDiagnostics, rejectDiagnostics) => {
38585
+ const timer = setTimeout(() => {
38586
+ this.diagnosticsWaiters.delete(key);
38587
+ 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."));
38588
+ }, DIAGNOSTICS_TIMEOUT_MS);
38589
+ this.diagnosticsWaiters.set(key, {
38590
+ resolve: resolveDiagnostics,
38591
+ reject: rejectDiagnostics,
38592
+ timer
38593
+ });
38335
38594
  });
38336
- } finally {
38337
- this.closeDocument(uri);
38338
- }
38595
+ let opened = null;
38596
+ try {
38597
+ opened = this.syncDocument(filePath, content);
38598
+ return await diagnosticsPromise;
38599
+ } catch (error) {
38600
+ const waiter = this.diagnosticsWaiters.get(key);
38601
+ if (waiter) {
38602
+ clearTimeout(waiter.timer);
38603
+ this.diagnosticsWaiters.delete(key);
38604
+ }
38605
+ throw error;
38606
+ } finally {
38607
+ if (opened !== null) {
38608
+ this.closeDocument(opened);
38609
+ }
38610
+ }
38611
+ });
38612
+ }
38613
+ async getCompletions(filePath, content, line, character) {
38614
+ await this.ensureConnected();
38615
+ await this.ensureInitializedForFile(filePath);
38616
+ const result = await this.withDocument(filePath, async () => {
38617
+ const uri = this.syncDocument(filePath, content);
38618
+ try {
38619
+ return await this.sendRequest("textDocument/completion", {
38620
+ textDocument: { uri },
38621
+ position: { line, character }
38622
+ });
38623
+ } finally {
38624
+ this.closeDocument(uri);
38625
+ }
38626
+ });
38339
38627
  if (Array.isArray(result)) {
38340
38628
  return result;
38341
38629
  }
@@ -38350,28 +38638,31 @@ class GodotLSPClient {
38350
38638
  async getHover(filePath, content, line, character) {
38351
38639
  await this.ensureConnected();
38352
38640
  await this.ensureInitializedForFile(filePath);
38353
- const uri = this.syncDocument(filePath, content);
38354
- try {
38355
- return await this.sendRequest("textDocument/hover", {
38356
- textDocument: { uri },
38357
- position: { line, character }
38358
- });
38359
- } finally {
38360
- this.closeDocument(uri);
38361
- }
38641
+ return this.withDocument(filePath, async () => {
38642
+ const uri = this.syncDocument(filePath, content);
38643
+ try {
38644
+ return await this.sendRequest("textDocument/hover", {
38645
+ textDocument: { uri },
38646
+ position: { line, character }
38647
+ });
38648
+ } finally {
38649
+ this.closeDocument(uri);
38650
+ }
38651
+ });
38362
38652
  }
38363
38653
  async getDocumentSymbols(filePath, content) {
38364
38654
  await this.ensureConnected();
38365
38655
  await this.ensureInitializedForFile(filePath);
38366
- const uri = this.syncDocument(filePath, content);
38367
- let result;
38368
- try {
38369
- result = await this.sendRequest("textDocument/documentSymbol", {
38370
- textDocument: { uri }
38371
- });
38372
- } finally {
38373
- this.closeDocument(uri);
38374
- }
38656
+ const result = await this.withDocument(filePath, async () => {
38657
+ const uri = this.syncDocument(filePath, content);
38658
+ try {
38659
+ return await this.sendRequest("textDocument/documentSymbol", {
38660
+ textDocument: { uri }
38661
+ });
38662
+ } finally {
38663
+ this.closeDocument(uri);
38664
+ }
38665
+ });
38375
38666
  return Array.isArray(result) ? result : [];
38376
38667
  }
38377
38668
  isConnected() {
@@ -38403,36 +38694,39 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
38403
38694
  try {
38404
38695
  projectPath = await realpath(requestedProjectPath);
38405
38696
  } catch {
38406
- throw new Refusal(`Project path does not exist: ${requestedProjectPath}`);
38697
+ throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
38698
+ }
38699
+ if (!existsSync15(join19(projectPath, "project.godot"))) {
38700
+ throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
38407
38701
  }
38408
38702
  const contained = resolveWithinProject(projectPath, scriptPathValue);
38409
38703
  if (!contained.ok) {
38410
- throw new Refusal(contained.reason);
38704
+ throw new ArgumentRefusal(contained.reason);
38411
38705
  }
38412
38706
  let scriptPath;
38413
38707
  try {
38414
38708
  scriptPath = await realpath(contained.absolutePath);
38415
38709
  } catch {
38416
- throw new Refusal(`Script file does not exist: ${contained.absolutePath}`);
38710
+ throw new ArgumentRefusal(`Script file does not exist: ${contained.absolutePath}`);
38417
38711
  }
38418
38712
  if (!isWithinRoot(projectPath, scriptPath)) {
38419
- throw new Refusal("scriptPath resolves outside the project root boundary.");
38713
+ throw new ArgumentRefusal("scriptPath resolves outside the project root boundary.");
38420
38714
  }
38421
38715
  return { projectPath, scriptPath };
38422
38716
  }
38423
38717
  async function handleLSPTool(client, toolName, args) {
38424
38718
  try {
38425
38719
  if (!args || typeof args !== "object") {
38426
- throw new Refusal("Tool arguments must be an object.");
38720
+ throw new ArgumentRefusal("Tool arguments must be an object.");
38427
38721
  }
38428
38722
  const parsedArgs = args;
38429
38723
  const projectPathValue = parsedArgs["projectPath"];
38430
38724
  const scriptPathValue = parsedArgs["scriptPath"];
38431
38725
  if (typeof projectPathValue !== "string" || projectPathValue.length === 0) {
38432
- throw new Refusal("Missing required argument: projectPath");
38726
+ throw new ArgumentRefusal("Missing required argument: projectPath");
38433
38727
  }
38434
38728
  if (typeof scriptPathValue !== "string" || scriptPathValue.length === 0) {
38435
- throw new Refusal("Missing required argument: scriptPath");
38729
+ throw new ArgumentRefusal("Missing required argument: scriptPath");
38436
38730
  }
38437
38731
  const { projectPath, scriptPath } = await resolveLSPPaths(projectPathValue, scriptPathValue);
38438
38732
  const content = await readFile(scriptPath, "utf8");
@@ -38446,7 +38740,7 @@ async function handleLSPTool(client, toolName, args) {
38446
38740
  const line = Number(parsedArgs["line"]);
38447
38741
  const character = Number(parsedArgs["character"]);
38448
38742
  if (!Number.isFinite(line) || !Number.isFinite(character)) {
38449
- throw new Refusal("Arguments line and character must be numbers.");
38743
+ throw new ArgumentRefusal("Arguments line and character must be numbers.");
38450
38744
  }
38451
38745
  const completions = await client.getCompletions(scriptPath, content, line, character);
38452
38746
  return asToolResponse({ completions });
@@ -38455,7 +38749,7 @@ async function handleLSPTool(client, toolName, args) {
38455
38749
  const line = Number(parsedArgs["line"]);
38456
38750
  const character = Number(parsedArgs["character"]);
38457
38751
  if (!Number.isFinite(line) || !Number.isFinite(character)) {
38458
- throw new Refusal("Arguments line and character must be numbers.");
38752
+ throw new ArgumentRefusal("Arguments line and character must be numbers.");
38459
38753
  }
38460
38754
  const hover = await client.getHover(scriptPath, content, line, character);
38461
38755
  return asToolResponse({ hover });
@@ -38469,20 +38763,31 @@ async function handleLSPTool(client, toolName, args) {
38469
38763
  }
38470
38764
  } catch (error) {
38471
38765
  return asToolResponse({
38472
- error: normalizeLSPError(error, client.port)
38766
+ error: normalizeLSPError(error, client.port),
38767
+ ...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
38768
+ ...error instanceof ArgumentRefusal ? { refusedArguments: true } : {}
38473
38769
  });
38474
38770
  }
38475
38771
  }
38476
- var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000;
38772
+ var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, ArgumentRefusal, AnotherProjectsServer;
38477
38773
  var init_lsp_client = __esm(() => {
38478
38774
  init_errors();
38479
38775
  init_framing();
38480
38776
  init_paths();
38481
38777
  init_ports();
38778
+ ArgumentRefusal = class ArgumentRefusal extends Refusal {
38779
+ };
38780
+ AnotherProjectsServer = class AnotherProjectsServer extends Refusal {
38781
+ serves;
38782
+ constructor(port, serves, asked) {
38783
+ super(`The language server on port ${port} belongs to the editor of ${serves}, not ${asked}.`);
38784
+ this.serves = serves;
38785
+ }
38786
+ };
38482
38787
  });
38483
38788
 
38484
38789
  // src/outside.ts
38485
- import { spawn as spawn2 } from "node:child_process";
38790
+ import { spawn as spawn3 } from "node:child_process";
38486
38791
  import { fileURLToPath as fileURLToPath4 } from "node:url";
38487
38792
  function changesFrom(env, base) {
38488
38793
  const changes = {};
@@ -38515,7 +38820,7 @@ function readLaunched(printed) {
38515
38820
  }
38516
38821
  async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
38517
38822
  return await new Promise((resolve) => {
38518
- const launcher = spawn2(runtime, [keeper, "launch"], {
38823
+ const launcher = spawn3(runtime, [keeper, "launch"], {
38519
38824
  stdio: ["pipe", "pipe", "pipe"],
38520
38825
  windowsHide: true
38521
38826
  });
@@ -38550,8 +38855,8 @@ var init_outside = __esm(() => {
38550
38855
  });
38551
38856
 
38552
38857
  // src/process-time.ts
38553
- import { execFile as execFile4 } from "node:child_process";
38554
- import { promisify as promisify4 } from "node:util";
38858
+ import { execFile as execFile3 } from "node:child_process";
38859
+ import { promisify as promisify3 } from "node:util";
38555
38860
  async function cpuSecondsOf(pid) {
38556
38861
  try {
38557
38862
  return process.platform === "win32" ? await secondsOnWindows(pid) : await askPosix2(pid);
@@ -38565,7 +38870,7 @@ async function secondsOnWindows(pid) {
38565
38870
  return Number.isFinite(seconds) ? seconds : undefined;
38566
38871
  }
38567
38872
  async function askPosix2(pid) {
38568
- const { stdout } = await run4("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
38873
+ const { stdout } = await run3("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
38569
38874
  return secondsFromClock(stdout.trim());
38570
38875
  }
38571
38876
  function secondsFromClock(printed) {
@@ -38583,20 +38888,20 @@ function secondsFromClock(printed) {
38583
38888
  const extra = days === null ? 0 : Number(days) * 86400;
38584
38889
  return Number.isFinite(extra) ? seconds + extra : undefined;
38585
38890
  }
38586
- var run4, ASK_TIMEOUT_MS2 = 6000;
38891
+ var run3, ASK_TIMEOUT_MS2 = 6000;
38587
38892
  var init_process_time = __esm(() => {
38588
38893
  init_windows_ask();
38589
- run4 = promisify4(execFile4);
38894
+ run3 = promisify3(execFile3);
38590
38895
  });
38591
38896
 
38592
38897
  // src/project-scan.ts
38593
- import { existsSync as existsSync13, readdirSync as readdirSync7, readFileSync as readFileSync15 } from "node:fs";
38594
- import { join as join18 } from "node:path";
38898
+ import { existsSync as existsSync16, readdirSync as readdirSync7, readFileSync as readFileSync17 } from "node:fs";
38899
+ import { join as join20 } from "node:path";
38595
38900
  function skipped(name) {
38596
38901
  return name.startsWith(".") || SKIPPED.has(name);
38597
38902
  }
38598
38903
  function steppedOver(directory) {
38599
- return existsSync13(join18(directory, ".gdignore"));
38904
+ return existsSync16(join20(directory, ".gdignore"));
38600
38905
  }
38601
38906
  function projectStructure(projectPath) {
38602
38907
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
@@ -38609,7 +38914,7 @@ function projectStructure(projectPath) {
38609
38914
  continue;
38610
38915
  }
38611
38916
  if (entry.isDirectory()) {
38612
- visit(join18(directory, entry.name));
38917
+ visit(join20(directory, entry.name));
38613
38918
  } else if (entry.isFile()) {
38614
38919
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
38615
38920
  if (extension === "tscn") {
@@ -38639,14 +38944,14 @@ function scriptsWithoutUid(projectPath) {
38639
38944
  }
38640
38945
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
38641
38946
  if (entry.isDirectory()) {
38642
- visit(join18(directory, entry.name), spelled);
38947
+ visit(join20(directory, entry.name), spelled);
38643
38948
  continue;
38644
38949
  }
38645
38950
  if (!entry.isFile()) {
38646
38951
  continue;
38647
38952
  }
38648
38953
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
38649
- if (CARRIES_A_UID.has(extension) && !existsSync13(join18(directory, `${entry.name}.uid`))) {
38954
+ if (CARRIES_A_UID.has(extension) && !existsSync16(join20(directory, `${entry.name}.uid`))) {
38650
38955
  found.push(spelled);
38651
38956
  }
38652
38957
  }
@@ -38681,7 +38986,7 @@ function searchProject(projectPath, options) {
38681
38986
  if (skipped(entry.name)) {
38682
38987
  continue;
38683
38988
  }
38684
- const entryPath = join18(directory, entry.name);
38989
+ const entryPath = join20(directory, entry.name);
38685
38990
  if (entry.isDirectory()) {
38686
38991
  visit(entryPath);
38687
38992
  continue;
@@ -38692,7 +38997,7 @@ function searchProject(projectPath, options) {
38692
38997
  }
38693
38998
  result.summary.files_searched += 1;
38694
38999
  const matches = [];
38695
- for (const [index, line] of readFileSync15(entryPath, "utf8").split(`
39000
+ for (const [index, line] of readFileSync17(entryPath, "utf8").split(`
38696
39001
  `).entries()) {
38697
39002
  if (full()) {
38698
39003
  break;
@@ -38720,37 +39025,16 @@ var init_project_scan = __esm(() => {
38720
39025
  CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
38721
39026
  });
38722
39027
 
38723
- // src/reimport.ts
38724
- import { createHash as createHash3 } from "node:crypto";
38725
- import { existsSync as existsSync14, readFileSync as readFileSync16, rmSync as rmSync8, statSync as statSync6, utimesSync, writeFileSync as writeFileSync11 } from "node:fs";
38726
- import { basename as basename3, join as join19 } from "node:path";
38727
- function forceNextImport(projectPath, resourcePath) {
38728
- const relative = resourcePath.replace(/^res:\/\//, "");
38729
- const sidecar = join19(projectPath, `${relative}.import`);
38730
- if (!existsSync14(sidecar)) {
38731
- return;
38732
- }
38733
- const { atime, mtimeMs } = statSync6(sidecar);
38734
- writeFileSync11(sidecar, readFileSync16(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
38735
- utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
38736
- rmSync8(importRecord(projectPath, resourcePath), { force: true });
38737
- }
38738
- function importRecord(projectPath, resourcePath) {
38739
- const digest = createHash3("md5").update(resourcePath).digest("hex");
38740
- return join19(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
38741
- }
38742
- var init_reimport = () => {};
38743
-
38744
39028
  // src/restart-note.ts
38745
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync17, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "node:fs";
38746
- import { dirname as dirname13, join as join20 } from "node:path";
39029
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync18, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "node:fs";
39030
+ import { dirname as dirname14, join as join21 } from "node:path";
38747
39031
  function restartNotePath(projectPath) {
38748
- return join20(projectPath, ".godot", "gdharness-restart.json");
39032
+ return join21(projectPath, ".godot", "gdharness-restart.json");
38749
39033
  }
38750
39034
  function noteRestartBegun(note) {
38751
39035
  const path = restartNotePath(note.projectPath);
38752
39036
  try {
38753
- mkdirSync10(dirname13(path), { recursive: true });
39037
+ mkdirSync10(dirname14(path), { recursive: true });
38754
39038
  writeFileSync12(path, `${JSON.stringify(note, null, 2)}
38755
39039
  `, "utf8");
38756
39040
  } catch (error) {
@@ -38759,7 +39043,7 @@ function noteRestartBegun(note) {
38759
39043
  }
38760
39044
  function restartOwed(projectPath) {
38761
39045
  try {
38762
- const parsed = JSON.parse(readFileSync17(restartNotePath(projectPath), "utf8"));
39046
+ const parsed = JSON.parse(readFileSync18(restartNotePath(projectPath), "utf8"));
38763
39047
  if (typeof parsed !== "object" || parsed === null) {
38764
39048
  return null;
38765
39049
  }
@@ -38784,31 +39068,31 @@ var init_restart_note = __esm(() => {
38784
39068
  });
38785
39069
 
38786
39070
  // src/run-record.ts
38787
- import { execFile as execFile5 } from "node:child_process";
39071
+ import { execFile as execFile4 } from "node:child_process";
38788
39072
  import { createHash as createHash4 } from "node:crypto";
38789
39073
  import {
38790
39074
  mkdirSync as mkdirSync11,
38791
39075
  openSync as openSync2,
38792
39076
  readdirSync as readdirSync8,
38793
- readFileSync as readFileSync18,
38794
- realpathSync as realpathSync3,
39077
+ readFileSync as readFileSync19,
39078
+ realpathSync as realpathSync4,
38795
39079
  rmSync as rmSync10,
38796
39080
  statSync as statSync7,
38797
39081
  writeFileSync as writeFileSync13
38798
39082
  } from "node:fs";
38799
- import { basename as basename4, dirname as dirname14, join as join21, resolve as resolve5 } from "node:path";
38800
- import { promisify as promisify5 } from "node:util";
39083
+ import { basename as basename4, dirname as dirname15, join as join22, resolve as resolve5 } from "node:path";
39084
+ import { promisify as promisify4 } from "node:util";
38801
39085
  function runsDirectory() {
38802
- return join21(runtimeDirectory(), "runs");
39086
+ return join22(runtimeDirectory(), "runs");
38803
39087
  }
38804
39088
  function realAsFarAsItExists(path) {
38805
39089
  const missing = [];
38806
39090
  let at = path;
38807
39091
  for (;; ) {
38808
39092
  try {
38809
- return join21(realpathSync3.native(at), ...missing);
39093
+ return join22(realpathSync4.native(at), ...missing);
38810
39094
  } catch {
38811
- const parent = dirname14(at);
39095
+ const parent = dirname15(at);
38812
39096
  if (parent === at) {
38813
39097
  return path;
38814
39098
  }
@@ -38823,15 +39107,18 @@ function projectKey(projectPath) {
38823
39107
  return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
38824
39108
  }
38825
39109
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
38826
- return join21(directory, "runs", `note-${projectKey(projectPath)}.json`);
39110
+ return join22(directory, "runs", `note-${projectKey(projectPath)}.json`);
38827
39111
  }
38828
39112
  function openTranscript(startedAt) {
38829
39113
  mkdirSync11(runsDirectory(), { recursive: true });
38830
- const path = join21(runsDirectory(), `run-${startedAt}.log`);
39114
+ const path = join22(runsDirectory(), `run-${startedAt}.log`);
38831
39115
  return { path, fd: openSync2(path, "a") };
38832
39116
  }
39117
+ function noteIsAbout(note, playing) {
39118
+ return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
39119
+ }
38833
39120
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
38834
- return join21(directory, "runs", `editor-${projectKey(projectPath)}.json`);
39121
+ return join22(directory, "runs", `editor-${projectKey(projectPath)}.json`);
38835
39122
  }
38836
39123
  function writeEditorRunNote(note) {
38837
39124
  mkdirSync11(runsDirectory(), { recursive: true });
@@ -38839,7 +39126,7 @@ function writeEditorRunNote(note) {
38839
39126
  }
38840
39127
  function readEditorRunNote(projectPath) {
38841
39128
  for (const directory of runtimeDirectories()) {
38842
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join21(directory, "runs", "editor-run.json"));
39129
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join22(directory, "runs", "editor-run.json"));
38843
39130
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
38844
39131
  return note;
38845
39132
  }
@@ -38849,7 +39136,7 @@ function readEditorRunNote(projectPath) {
38849
39136
  function editorNoteAt(path) {
38850
39137
  let parsed;
38851
39138
  try {
38852
- parsed = JSON.parse(readFileSync18(path, "utf8"));
39139
+ parsed = JSON.parse(readFileSync19(path, "utf8"));
38853
39140
  } catch {
38854
39141
  return null;
38855
39142
  }
@@ -38863,7 +39150,9 @@ function editorNoteAt(path) {
38863
39150
  if (typeof projectPath !== "string" || typeof transcript !== "string" || typeof startedAt !== "number") {
38864
39151
  return null;
38865
39152
  }
38866
- return { projectPath, transcript, startedAt };
39153
+ const play = fields["play"];
39154
+ const which = typeof play === "object" && play !== null && typeof play["play"] === "number" && typeof play["editorPid"] === "number" ? play : undefined;
39155
+ return { projectPath, transcript, startedAt, ...which === undefined ? {} : { play: which } };
38867
39156
  }
38868
39157
  function readRunRecord(projectPath) {
38869
39158
  for (const directory of runtimeDirectories()) {
@@ -38871,7 +39160,7 @@ function readRunRecord(projectPath) {
38871
39160
  if (own !== null) {
38872
39161
  return own;
38873
39162
  }
38874
- const shared = recordAt(join21(directory, "runs", SHARED_NOTE));
39163
+ const shared = recordAt(join22(directory, "runs", SHARED_NOTE));
38875
39164
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
38876
39165
  return shared;
38877
39166
  }
@@ -38883,13 +39172,13 @@ function everyRunRecord() {
38883
39172
  for (const directory of runtimeDirectories()) {
38884
39173
  let entries;
38885
39174
  try {
38886
- entries = readdirSync8(join21(directory, "runs"));
39175
+ entries = readdirSync8(join22(directory, "runs"));
38887
39176
  } catch {
38888
39177
  continue;
38889
39178
  }
38890
39179
  for (const entry of entries) {
38891
39180
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
38892
- const record = recordAt(join21(directory, "runs", entry));
39181
+ const record = recordAt(join22(directory, "runs", entry));
38893
39182
  if (record !== null) {
38894
39183
  found.push(record);
38895
39184
  }
@@ -38901,7 +39190,7 @@ function everyRunRecord() {
38901
39190
  function recordAt(path) {
38902
39191
  let parsed;
38903
39192
  try {
38904
- parsed = JSON.parse(readFileSync18(path, "utf8"));
39193
+ parsed = JSON.parse(readFileSync19(path, "utf8"));
38905
39194
  } catch {
38906
39195
  return null;
38907
39196
  }
@@ -38947,7 +39236,7 @@ async function runningAs(pid) {
38947
39236
  const began = await startedAt(pid, null);
38948
39237
  const when = began === null ? {} : { startedAt: began };
38949
39238
  if (process.platform === "linux") {
38950
- const raw = readFileSync18(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
39239
+ const raw = readFileSync19(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
38951
39240
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
38952
39241
  }
38953
39242
  const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
@@ -39075,7 +39364,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
39075
39364
  }
39076
39365
  function clearRunRecord(projectPath) {
39077
39366
  for (const directory of runtimeDirectories()) {
39078
- const shared = join21(directory, "runs", SHARED_NOTE);
39367
+ const shared = join22(directory, "runs", SHARED_NOTE);
39079
39368
  const sharedRecord = recordAt(shared);
39080
39369
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
39081
39370
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
@@ -39105,7 +39394,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
39105
39394
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
39106
39395
  continue;
39107
39396
  }
39108
- const path = join21(directory, entry);
39397
+ const path = join22(directory, entry);
39109
39398
  if (kept.has(path) || ageMs(path, now) < keepMs) {
39110
39399
  continue;
39111
39400
  }
@@ -39120,31 +39409,31 @@ var init_run_record = __esm(() => {
39120
39409
  init_process_children();
39121
39410
  init_runtime_client();
39122
39411
  init_windows_ask();
39123
- execFileAsync = promisify5(execFile5);
39412
+ execFileAsync = promisify4(execFile4);
39124
39413
  });
39125
39414
 
39126
39415
  // src/server.ts
39127
- import { execFile as execFile6, spawn as spawn3 } from "node:child_process";
39416
+ import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
39128
39417
  import { randomUUID as randomUUID2 } from "node:crypto";
39129
39418
  import {
39130
39419
  appendFileSync,
39131
39420
  closeSync as closeSync2,
39132
39421
  constants,
39133
39422
  copyFileSync,
39134
- existsSync as existsSync15,
39423
+ existsSync as existsSync17,
39135
39424
  fstatSync,
39136
39425
  mkdirSync as mkdirSync12,
39137
39426
  openSync as openSync3,
39138
39427
  readdirSync as readdirSync9,
39139
- readFileSync as readFileSync19,
39428
+ readFileSync as readFileSync20,
39140
39429
  readSync as readSync2,
39141
39430
  rmdirSync as rmdirSync2,
39142
39431
  statSync as statSync8
39143
39432
  } from "node:fs";
39144
- import { basename as basename5, dirname as dirname15, extname as extname2, isAbsolute as isAbsolute3, join as join22, normalize as normalize2, relative as relative3 } from "node:path";
39433
+ import { basename as basename5, dirname as dirname16, extname as extname2, isAbsolute as isAbsolute3, join as join23, normalize as normalize2, relative as relative3 } from "node:path";
39145
39434
  import { setTimeout as delay3 } from "node:timers/promises";
39146
39435
  import { fileURLToPath as fileURLToPath5 } from "node:url";
39147
- import { promisify as promisify6 } from "node:util";
39436
+ import { promisify as promisify5 } from "node:util";
39148
39437
  function captureDestinationRefusal(outputPath, projects) {
39149
39438
  if (/^(res|user):\/\//.test(outputPath)) {
39150
39439
  return `outputPath ${outputPath} is a Godot path; a capture is saved outside the project, at an absolute path on this machine.`;
@@ -39155,16 +39444,16 @@ function captureDestinationRefusal(outputPath, projects) {
39155
39444
  if (extname2(outputPath).toLowerCase() !== ".png") {
39156
39445
  return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
39157
39446
  }
39158
- const directory = realPathOr(dirname15(outputPath));
39159
- if (!existsSync15(directory)) {
39160
- return `outputPath's directory ${dirname15(outputPath)} does not exist; a capture creates no directories.`;
39447
+ const directory = realPathOr(dirname16(outputPath));
39448
+ if (!existsSync17(directory)) {
39449
+ return `outputPath's directory ${dirname16(outputPath)} does not exist; a capture creates no directories.`;
39161
39450
  }
39162
- const where = join22(directory, basename5(outputPath));
39451
+ const where = join23(directory, basename5(outputPath));
39163
39452
  const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
39164
39453
  if (inside !== undefined) {
39165
39454
  return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
39166
39455
  }
39167
- if (existsSync15(where)) {
39456
+ if (existsSync17(where)) {
39168
39457
  return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
39169
39458
  }
39170
39459
  return null;
@@ -39195,16 +39484,18 @@ function describeHalt(halt) {
39195
39484
  function exited(run) {
39196
39485
  return run.exitCode !== null || run.exitSignal !== null;
39197
39486
  }
39198
- function endingOf(failed) {
39199
- return {
39200
- exitCode: typeof failed.code === "number" ? failed.code : null,
39201
- exitSignal: typeof failed.signal === "string" ? failed.signal : null,
39202
- failure: typeof failed.code === "string" ? failed.code : null
39203
- };
39204
- }
39205
39487
  function howItExited(run) {
39206
39488
  return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}, with no exit code`;
39207
39489
  }
39490
+ function errorsUnread(run) {
39491
+ return run.throughEditor && typeof run.errorReport !== "string";
39492
+ }
39493
+ function cleanVerdict(run) {
39494
+ if (run.consoleLost === true || errorsUnread(run)) {
39495
+ return;
39496
+ }
39497
+ return run.log.count("error") === 0;
39498
+ }
39208
39499
  function stillRunning(run) {
39209
39500
  if (run === null || exited(run) || run.endedUnwatched === true) {
39210
39501
  return false;
@@ -39220,6 +39511,11 @@ function stillRunning(run) {
39220
39511
  }
39221
39512
  return true;
39222
39513
  }
39514
+ function playOf(answer) {
39515
+ const play = readNumber(answer, "play");
39516
+ const editorPid = readNumber(answer, "editorPid");
39517
+ return play === undefined || editorPid === undefined ? null : { play, editorPid };
39518
+ }
39223
39519
  function runIsUp(run, editorSays, now = Date.now()) {
39224
39520
  if (run?.throughEditor !== true) {
39225
39521
  return stillRunning(run);
@@ -39267,11 +39563,19 @@ function stopVerdict(stop) {
39267
39563
  return {
39268
39564
  stopped: true,
39269
39565
  endedPid: stop.endedPid,
39270
- clean: stop.errors === 0,
39566
+ clean: stop.clean,
39271
39567
  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.",
39272
39568
  withChildren: false
39273
39569
  };
39274
39570
  }
39571
+ if (stop.ending === "unreached") {
39572
+ return {
39573
+ stopped: false,
39574
+ notSignalled: stop.endedPid,
39575
+ 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.",
39576
+ withChildren: false
39577
+ };
39578
+ }
39275
39579
  if (stop.ending === "refused") {
39276
39580
  return {
39277
39581
  stopped: false,
@@ -39283,13 +39587,13 @@ function stopVerdict(stop) {
39283
39587
  return {
39284
39588
  stopped: true,
39285
39589
  endedPid: stop.endedPid,
39286
- clean: stop.errors === 0,
39590
+ clean: stop.clean,
39287
39591
  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.`}`,
39288
39592
  withChildren: true
39289
39593
  };
39290
39594
  }
39291
39595
  function endedPreviousRun(ended) {
39292
- if (ended === null || ended.ending === "refused") {
39596
+ if (ended === null || ended.ending === "refused" || ended.ending === "unreached") {
39293
39597
  return;
39294
39598
  }
39295
39599
  return ended.pid ?? true;
@@ -39371,6 +39675,9 @@ function endedToStartThis(ended) {
39371
39675
  if (ended.ending === "refused") {
39372
39676
  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.`;
39373
39677
  }
39678
+ if (ended.ending === "unreached") {
39679
+ 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.`;
39680
+ }
39374
39681
  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.` : "";
39375
39682
  return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.${after}`;
39376
39683
  }
@@ -39496,19 +39803,58 @@ function withScanWait(outcome, scanned) {
39496
39803
  if (scanned.waitedMs === 0) {
39497
39804
  return outcome;
39498
39805
  }
39499
- const said = scanWaitAnswer(scanned);
39806
+ const said = scanWaitAnswer(scanned, "engine");
39500
39807
  if (outcome.ok) {
39501
39808
  return { ...outcome, payload: { ...outcome.payload, ...said } };
39502
39809
  }
39503
39810
  return said.scanNote === undefined ? outcome : { ...outcome, message: `${outcome.message} ${said.scanNote}` };
39504
39811
  }
39505
- function scanWaitAnswer(scanned) {
39812
+ function notRunNote(notRun, failFast) {
39813
+ if (notRun <= 0) {
39814
+ return;
39815
+ }
39816
+ const cases = `${notRun} case${notRun === 1 ? "" : "s"}`;
39817
+ 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.`;
39818
+ }
39819
+ function exportAnswer(ending, asked, before, after) {
39820
+ const { log } = ending;
39821
+ const written = after !== null && after !== before;
39822
+ const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
39823
+ const verdict = {
39824
+ exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
39825
+ ...asked,
39826
+ exitCode: ending.exitCode,
39827
+ exitSignal: ending.exitSignal ?? undefined,
39828
+ failure: ending.failure ?? undefined,
39829
+ errors: log.count("error"),
39830
+ warnings: log.count("warning"),
39831
+ entries: forAnswer(problems.entries),
39832
+ ...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
39833
+ };
39834
+ if (verdict.exported) {
39835
+ return { content: [{ type: "text", text: JSON.stringify(verdict, null, 2) }] };
39836
+ }
39837
+ const stale = !written && after !== null ? ` The ${asked.outputPath} there is from before this export, which did not write it.` : "";
39838
+ const why = ending.exitSignal === null ? stale : ` The engine was ${howItEnded(ending)}, so it has no exit code.${stale}`;
39839
+ return {
39840
+ content: [
39841
+ {
39842
+ type: "text",
39843
+ text: `Export with preset '${asked.preset}' did not produce ${asked.outputPath}.${why}`
39844
+ },
39845
+ { type: "text", text: JSON.stringify(verdict, null, 2) }
39846
+ ],
39847
+ isError: true
39848
+ };
39849
+ }
39850
+ function scanWaitAnswer(scanned, started = "game") {
39506
39851
  if (scanned.waitedMs === 0) {
39507
39852
  return {};
39508
39853
  }
39854
+ const still = `The editor was still scanning the project after ${SCAN_WAIT_MS / 1000} seconds, so`;
39509
39855
  return {
39510
39856
  waitedForEditorScanMs: scanned.waitedMs,
39511
- 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
39857
+ 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."
39512
39858
  };
39513
39859
  }
39514
39860
  function changedSettings(before, after) {
@@ -39563,16 +39909,17 @@ function importStatuses(payload) {
39563
39909
  };
39564
39910
  });
39565
39911
  }
39566
- function escapingPropertyValue(properties) {
39567
- if (typeof properties !== "object" || properties === null) {
39912
+ function escapingPropertyValue(value) {
39913
+ if (typeof value === "string") {
39914
+ return /^(res|uid):\/\//.test(value) && value.split(/[/\\]/).includes("..") ? JSON.stringify(value) : undefined;
39915
+ }
39916
+ if (typeof value !== "object" || value === null) {
39568
39917
  return;
39569
39918
  }
39570
- for (const value of Object.values(properties)) {
39571
- if (typeof value !== "string" || !value.includes("/")) {
39572
- continue;
39573
- }
39574
- if (value.split(/[/\\]/).includes("..")) {
39575
- return JSON.stringify(value);
39919
+ for (const item of Object.values(value)) {
39920
+ const found = escapingPropertyValue(item);
39921
+ if (found !== undefined) {
39922
+ return found;
39576
39923
  }
39577
39924
  }
39578
39925
  return;
@@ -39593,7 +39940,7 @@ function aboveTheRunner(entry) {
39593
39940
  }
39594
39941
  function elsewhereIn(projectPath, asked) {
39595
39942
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
39596
- if (wanted === "" || existsSync15(join22(projectPath, wanted))) {
39943
+ if (wanted === "" || existsSync17(join23(projectPath, wanted))) {
39597
39944
  return "";
39598
39945
  }
39599
39946
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -39607,7 +39954,7 @@ function elsewhereIn(projectPath, asked) {
39607
39954
  return near.length === 0 ? "" : ` The project does have ${there}.`;
39608
39955
  }
39609
39956
  function hasMainScene(projectFile) {
39610
- const scene = parseProjectGodot(readFileSync19(projectFile, "utf8"))["application"]?.["run/main_scene"];
39957
+ const scene = parseProjectGodot(readFileSync20(projectFile, "utf8"))["application"]?.["run/main_scene"];
39611
39958
  return typeof scene === "string" && scene !== "";
39612
39959
  }
39613
39960
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -39615,13 +39962,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
39615
39962
  return false;
39616
39963
  }
39617
39964
  try {
39618
- return readFileSync19(join22(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
39965
+ return readFileSync20(join23(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
39619
39966
  } catch {
39620
39967
  return false;
39621
39968
  }
39622
39969
  }
39623
39970
  function editorPlaysHeadless(projectFile) {
39624
- const runArgs = parseProjectGodot(readFileSync19(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
39971
+ const runArgs = parseProjectGodot(readFileSync20(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
39625
39972
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
39626
39973
  }
39627
39974
  function camelCased(params) {
@@ -39640,7 +39987,7 @@ function needsItsOwnProcess(asked) {
39640
39987
  class GodotServer {
39641
39988
  mcp;
39642
39989
  locator = new GodotLocator;
39643
- operationsScript = join22(__dirname2, "godot", "operations", "godot_operations.gd");
39990
+ operationsScript = join23(__dirname2, "godot", "operations", "godot_operations.gd");
39644
39991
  godotBridge;
39645
39992
  tools = buildToolDefinitions();
39646
39993
  activeProcess = null;
@@ -40140,6 +40487,9 @@ class GodotServer {
40140
40487
  const contained = this.containProjectFiles(asks);
40141
40488
  return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
40142
40489
  }
40490
+ if (headless === "create_script") {
40491
+ return await this.handleCreateScript(answerable);
40492
+ }
40143
40493
  if (headless !== "refresh_class_cache") {
40144
40494
  return await this.headless(headless, answerable);
40145
40495
  }
@@ -40310,7 +40660,7 @@ class GodotServer {
40310
40660
  return await this.handleRuntimeCommand("click", {
40311
40661
  ...whichGame(args),
40312
40662
  path: readNonEmptyString(args, "nodePath") ?? "",
40313
- button: readString(args, "button") ?? "left",
40663
+ button: args["button"] ?? "left",
40314
40664
  double: readBoolean(args, "doubleClick") ?? false,
40315
40665
  ...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
40316
40666
  ...args["index"] === undefined ? {} : { index: args["index"] }
@@ -40382,8 +40732,8 @@ class GodotServer {
40382
40732
  if (path === undefined) {
40383
40733
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
40384
40734
  }
40385
- const file = join22(path, "project.godot");
40386
- if (!existsSync15(file)) {
40735
+ const file = join23(path, "project.godot");
40736
+ if (!existsSync17(file)) {
40387
40737
  return {
40388
40738
  ok: false,
40389
40739
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -40593,13 +40943,30 @@ class GodotServer {
40593
40943
  }
40594
40944
  contained[key] = `res://${location.relativePath}`;
40595
40945
  }
40596
- const escaping = escapingPropertyValue(args["properties"]);
40946
+ const escaping = escapingPropertyValue([args["properties"], args["track"]]);
40597
40947
  if (escaping !== undefined) {
40598
40948
  return {
40599
40949
  ok: false,
40600
40950
  response: this.createErrorResponse(`The property value ${escaping} resolves outside the project directory.`, PATH_SOLUTIONS)
40601
40951
  };
40602
40952
  }
40953
+ const sources = args["sources"];
40954
+ if (Array.isArray(sources)) {
40955
+ const resolved = [];
40956
+ for (const source of sources) {
40957
+ const texture = typeof source === "object" && source !== null ? source["texture"] : undefined;
40958
+ if (typeof texture !== "string") {
40959
+ resolved.push(source);
40960
+ continue;
40961
+ }
40962
+ const location = resolveWithinProject(projectPath, texture);
40963
+ if (!location.ok) {
40964
+ return { ok: false, response: this.createErrorResponse(location.reason, PATH_SOLUTIONS) };
40965
+ }
40966
+ resolved.push({ ...source, texture: `res://${location.relativePath}` });
40967
+ }
40968
+ contained["sources"] = resolved;
40969
+ }
40603
40970
  const pluginName = readNonEmptyString(args, "pluginName");
40604
40971
  if (pluginName !== undefined) {
40605
40972
  const plugin = resolveWithinProject(projectPath, `addons/${pluginName}/plugin.cfg`);
@@ -40647,11 +41014,12 @@ class GodotServer {
40647
41014
  return this.answer(withScanWait({
40648
41015
  ok: true,
40649
41016
  messages: imported.messages,
41017
+ ...imported.messagesOmitted === undefined ? {} : { messagesOmitted: imported.messagesOmitted },
40650
41018
  payload: {
40651
41019
  uidsCreated: given,
40652
41020
  stillWithoutUid: after,
40653
41021
  note: uidsLeftNote(after.length),
40654
- ...imported.librariesRetried === undefined ? {} : { extensionNote: extensionRetryNote(imported.librariesRetried) }
41022
+ ...imported.extensionNote === undefined ? {} : { extensionNote: imported.extensionNote }
40655
41023
  }
40656
41024
  }, scanned));
40657
41025
  }
@@ -40697,16 +41065,25 @@ class GodotServer {
40697
41065
  if (changed.length === 0) {
40698
41066
  return written;
40699
41067
  }
40700
- const taken = await this.editorTakes("adopt_project_settings", { settings: changed });
41068
+ const adopting = {};
41069
+ const taken = await this.editorTakes("adopt_project_settings", { settings: changed }, adopting);
41070
+ if (taken["editorNote"] !== undefined) {
41071
+ return this.jsonTextResponse({ ...answer, ...taken });
41072
+ }
41073
+ const adopted = Array.isArray(adopting["adopted"]) ? adopting["adopted"].map(String) : [];
41074
+ const passedOver = changed.filter((name) => !adopted.includes(name));
40701
41075
  return this.jsonTextResponse({
40702
41076
  ...answer,
40703
- ...taken,
40704
- ...taken["editorNote"] === undefined ? { editorAdopted: changed } : {}
41077
+ editorAdopted: adopted,
41078
+ ...passedOver.length > 0 ? { editorPassedOver: passedOver } : {}
40705
41079
  });
40706
41080
  }
40707
- async editorTakes(command, args) {
41081
+ async editorTakes(command, args, answered = {}) {
40708
41082
  try {
40709
- await this.godotBridge.invokeTool(command, args);
41083
+ const result = await this.godotBridge.invokeTool(command, args);
41084
+ if (typeof result === "object" && result !== null) {
41085
+ Object.assign(answered, result);
41086
+ }
40710
41087
  return {};
40711
41088
  } catch (error) {
40712
41089
  const message = errorMessage(error);
@@ -40744,7 +41121,19 @@ class GodotServer {
40744
41121
  }
40745
41122
  const applied = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
40746
41123
  if (!applied.ok) {
40747
- return applied.response;
41124
+ const listed = written.payload["updated_options"];
41125
+ const updated = Array.isArray(listed) ? listed.map(String) : [];
41126
+ const which = updated.length === 0 ? "The options were" : `${updated.join(", ")} ${updated.length === 1 ? "was" : "were"}`;
41127
+ return {
41128
+ ...applied.response,
41129
+ content: [
41130
+ {
41131
+ type: "text",
41132
+ 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:`
41133
+ },
41134
+ ...applied.response.content
41135
+ ]
41136
+ };
40748
41137
  }
40749
41138
  return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
40750
41139
  }
@@ -40762,7 +41151,14 @@ class GodotServer {
40762
41151
  response: this.createErrorResponse(`${missingSource[0]} is not on disk, so there is nothing to import.`)
40763
41152
  };
40764
41153
  }
40765
- const targets = listed.filter((one) => one.status !== "missing_source" && (force || one.status !== "up_to_date")).map((one) => one.path);
41154
+ const never = listed.find((one) => one.status === "not_imported");
41155
+ if (resourcePath !== undefined && never !== undefined) {
41156
+ return {
41157
+ ok: false,
41158
+ response: this.createErrorResponse(`${never.path} is not something the engine imports: ${never.reason ?? "it has no importer"}.`)
41159
+ };
41160
+ }
41161
+ const targets = listed.filter((one) => one.status !== "missing_source" && one.status !== "not_imported" && (force || one.status !== "up_to_date")).map((one) => one.path);
40766
41162
  if (targets.length === 0) {
40767
41163
  return {
40768
41164
  ok: true,
@@ -40796,19 +41192,14 @@ class GodotServer {
40796
41192
  if (!engine.ok) {
40797
41193
  return { ok: false, response: engine.response };
40798
41194
  }
40799
- for (const target of targets) {
40800
- forceNextImport(projectPath, target);
40801
- }
40802
41195
  const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
40803
- const imported = await runImport(engine.value, projectPath);
41196
+ const imported = await runImport(engine.value, projectPath, { forcing: targets });
40804
41197
  if (!imported.ok) {
40805
41198
  return { ok: false, response: this.answer(withScanWait(imported, scanned)) };
40806
41199
  }
40807
- if (imported.messages.length > 0) {
40808
- extra["engine_messages"] = imported.messages;
40809
- }
40810
- if (imported.librariesRetried !== undefined) {
40811
- extra["extensionNote"] = extensionRetryNote(imported.librariesRetried);
41200
+ Object.assign(extra, engineExtras(imported));
41201
+ if (imported.extensionNote !== undefined) {
41202
+ extra["extensionNote"] = imported.extensionNote;
40812
41203
  }
40813
41204
  }
40814
41205
  const after = await this.operation("get_import_status", resourcePath === undefined ? { includeUpToDate: true } : { resourcePath }, projectPath);
@@ -40889,17 +41280,15 @@ class GodotServer {
40889
41280
  return withScanWait(outcome, scanned);
40890
41281
  }
40891
41282
  answer(outcome) {
41283
+ const extras = engineExtras(outcome);
40892
41284
  if (!outcome.ok) {
40893
41285
  const response = this.createErrorResponse(outcome.message);
40894
- if (outcome.messages.length > 0) {
40895
- response.content.push({
40896
- type: "text",
40897
- text: JSON.stringify({ engine_messages: outcome.messages }, null, 2)
40898
- });
41286
+ if (Object.keys(extras).length > 0) {
41287
+ response.content.push({ type: "text", text: JSON.stringify(extras, null, 2) });
40899
41288
  }
40900
41289
  return response;
40901
41290
  }
40902
- return this.jsonTextResponse(outcome.messages.length > 0 ? { ...outcome.payload, engine_messages: outcome.messages } : outcome.payload);
41291
+ return this.jsonTextResponse({ ...outcome.payload, ...extras });
40903
41292
  }
40904
41293
  async handleProjectInfo(args) {
40905
41294
  const include = readStringArray(args, "include") ?? [];
@@ -40915,7 +41304,7 @@ class GodotServer {
40915
41304
  if (!engine.ok) {
40916
41305
  return engine.response;
40917
41306
  }
40918
- const application = parseProjectGodot(readFileSync19(project.value.file, "utf8"))["application"] ?? {};
41307
+ const application = parseProjectGodot(readFileSync20(project.value.file, "utf8"))["application"] ?? {};
40919
41308
  const name = application["config/name"];
40920
41309
  const mainScene = application["run/main_scene"];
40921
41310
  const info = {
@@ -40932,7 +41321,7 @@ class GodotServer {
40932
41321
  continue;
40933
41322
  }
40934
41323
  const outcome = await this.operation(spec.operation, spec.params(args, detailed), project.value.path);
40935
- info[section] = outcome.ok ? outcome.messages.length > 0 ? { ...outcome.payload, engine_messages: outcome.messages } : outcome.payload : { error: outcome.message, engine_messages: outcome.messages };
41324
+ info[section] = outcome.ok ? { ...outcome.payload, ...engineExtras(outcome) } : { error: outcome.message, ...engineExtras(outcome) };
40936
41325
  }
40937
41326
  return this.jsonTextResponse(info);
40938
41327
  }
@@ -40976,18 +41365,18 @@ class GodotServer {
40976
41365
  if (!engine.ok) {
40977
41366
  return engine.response;
40978
41367
  }
40979
- const folder = dirname15(output.absolutePath);
41368
+ const folder = dirname16(output.absolutePath);
40980
41369
  try {
40981
41370
  mkdirSync12(folder, { recursive: true });
40982
41371
  } catch (error) {
40983
- return this.createErrorResponse(`Cannot create ${dirname15(output.relativePath)} to export into: ${errorMessage(error)}`);
41372
+ return this.createErrorResponse(`Cannot create ${dirname16(output.relativePath)} to export into: ${errorMessage(error)}`);
40984
41373
  }
40985
41374
  const debug = readBoolean(args, "debug") ?? false;
40986
41375
  const exportLogs = scratchDirectory("export");
40987
41376
  const exportArgs = [
40988
41377
  "--headless",
40989
41378
  "--log-file",
40990
- join22(exportLogs, "engine.log"),
41379
+ join23(exportLogs, "engine.log"),
40991
41380
  "--path",
40992
41381
  project.value.path,
40993
41382
  debug ? "--export-debug" : "--export-release",
@@ -40995,55 +41384,18 @@ class GodotServer {
40995
41384
  output.absolutePath
40996
41385
  ];
40997
41386
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
40998
- const log = new GameLog;
40999
- let ending = { exitCode: 0, exitSignal: null, failure: null };
41387
+ const writtenAt = () => existsSync17(output.absolutePath) ? statSync8(output.absolutePath).mtimeMs : null;
41388
+ const before = writtenAt();
41389
+ let ending;
41000
41390
  try {
41001
- const { stdout, stderr } = await run5(engine.value, exportArgs, {
41002
- timeout: 300000,
41003
- maxBuffer: 64 * 1024 * 1024,
41004
- signal: callSignal()
41005
- });
41006
- log.append("stdout", stdout);
41007
- log.append("stderr", stderr);
41008
- } catch (error) {
41009
- if (!(error instanceof Error && ("stdout" in error) && ("stderr" in error))) {
41010
- return this.createErrorResponse(`Export could not be run: ${errorMessage(error)}`);
41011
- }
41012
- const failed = error;
41013
- log.append("stdout", failed.stdout);
41014
- log.append("stderr", failed.stderr);
41015
- ending = endingOf(failed);
41391
+ ending = await runEngine(engine.value, exportArgs, { timeout: { ms: 300000, said: "five minutes" } });
41016
41392
  } finally {
41017
41393
  discard(exportLogs);
41018
41394
  }
41019
- log.finish();
41020
- const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
41021
- const verdict = {
41022
- exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync15(output.absolutePath),
41023
- preset,
41024
- outputPath: output.relativePath,
41025
- debug,
41026
- exitCode: ending.exitCode,
41027
- exitSignal: ending.exitSignal ?? undefined,
41028
- failure: ending.failure ?? undefined,
41029
- errors: log.count("error"),
41030
- warnings: log.count("warning"),
41031
- entries: forAnswer(problems.entries)
41032
- };
41033
- if (!verdict.exported) {
41034
- 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.`;
41035
- return {
41036
- content: [
41037
- {
41038
- type: "text",
41039
- text: `Export with preset '${preset}' did not produce ${output.relativePath}.${why}`
41040
- },
41041
- { type: "text", text: JSON.stringify(verdict, null, 2) }
41042
- ],
41043
- isError: true
41044
- };
41395
+ if (ending.exitSignal === null && ending.exitCode === null) {
41396
+ return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
41045
41397
  }
41046
- return this.jsonTextResponse(verdict);
41398
+ return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt());
41047
41399
  }
41048
41400
  async handleRunTests(args) {
41049
41401
  const project = this.project(args);
@@ -41055,7 +41407,7 @@ class GodotServer {
41055
41407
  return contained.response;
41056
41408
  }
41057
41409
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
41058
- if (!existsSync15(join22(project.value.path, runner))) {
41410
+ if (!existsSync17(join23(project.value.path, runner))) {
41059
41411
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
41060
41412
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
41061
41413
  ]);
@@ -41121,21 +41473,22 @@ class GodotServer {
41121
41473
  if (timedOut && run.process.pid !== undefined) {
41122
41474
  await untilGone([run.process.pid]);
41123
41475
  }
41124
- const reportsDir = join22(project.value.path, ".godot", ...ours.split("/"));
41476
+ await run.settled;
41477
+ const reportsDir = join23(project.value.path, ".godot", ...ours.split("/"));
41125
41478
  let report = null;
41126
41479
  let reportProblem = null;
41127
41480
  try {
41128
- const written = existsSync15(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
41481
+ const written = existsSync17(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
41129
41482
  const newest = written.at(-1);
41130
41483
  if (newest !== undefined) {
41131
- report = parseJUnit(readFileSync19(join22(reportsDir, newest, "results.xml"), "utf8"));
41484
+ report = parseJUnit(readFileSync20(join23(reportsDir, newest, "results.xml"), "utf8"));
41132
41485
  }
41133
41486
  } catch (error) {
41134
41487
  reportProblem = errorMessage(error);
41135
41488
  } finally {
41136
41489
  discard(reportsDir, userData);
41137
41490
  try {
41138
- rmdirSync2(dirname15(reportsDir));
41491
+ rmdirSync2(dirname16(reportsDir));
41139
41492
  } catch {}
41140
41493
  }
41141
41494
  const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
@@ -41148,7 +41501,11 @@ class GodotServer {
41148
41501
  105: "script errors"
41149
41502
  };
41150
41503
  const exitCode = run.exitCode;
41151
- const printed = run.log.select({ severity: "info", sinceLastCall: false, limit: 200 }).entries;
41504
+ const printed = run.log.select({
41505
+ severity: "info",
41506
+ sinceLastCall: false,
41507
+ limit: Number.POSITIVE_INFINITY
41508
+ }).entries;
41152
41509
  const said = printed.map((entry) => entry.text);
41153
41510
  const nothingRan = timedOut ? null : whyNoReport(said, asked);
41154
41511
  const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
@@ -41164,7 +41521,7 @@ class GodotServer {
41164
41521
  ...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
41165
41522
  };
41166
41523
  if (report === null) {
41167
- 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}` : ""}).`;
41524
+ 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}).`;
41168
41525
  return {
41169
41526
  content: [
41170
41527
  { type: "text", text: note },
@@ -41201,10 +41558,21 @@ class GodotServer {
41201
41558
  orphans: suite.orphans,
41202
41559
  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().`
41203
41560
  }));
41204
- const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
41561
+ 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);
41562
+ const hooksPrinted = hookFailuresPrinted(said);
41563
+ const hookFailures = report.suites.filter((suite) => suite.hookFailures + suite.hookErrors > 0).flatMap((suite) => {
41564
+ const printedHere = hooksPrinted.filter((hook) => hook.path === suite.path);
41565
+ return printedHere.length > 0 ? printedHere.map((hook) => ({ suite: suite.name, ...hook })) : [
41566
+ {
41567
+ suite: suite.name,
41568
+ path: suite.path,
41569
+ 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."
41570
+ }
41571
+ ];
41572
+ });
41205
41573
  const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
41206
41574
  return this.jsonTextResponse({
41207
- passed: !timedOut && exitCode === 0 && report.failures === 0 && report.errors === 0,
41575
+ passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0,
41208
41576
  verdict,
41209
41577
  ...timedOut ? { timedOut, hung, silentForMs } : {},
41210
41578
  exitCode,
@@ -41215,13 +41583,14 @@ class GodotServer {
41215
41583
  skipped: report.skipped,
41216
41584
  time: report.time,
41217
41585
  failed,
41586
+ hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
41218
41587
  warnings: warnings.length > 0 ? warnings : undefined,
41219
41588
  orphans: orphans.total > 0 ? orphans.total : undefined,
41220
41589
  notRun: notRun > 0 ? notRun : undefined,
41221
41590
  ...printedLines,
41222
41591
  savesNote,
41223
41592
  ...scanned,
41224
- 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,
41593
+ 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),
41225
41594
  suites: unclean.map((suite) => ({
41226
41595
  name: suite.name,
41227
41596
  path: suite.path,
@@ -41229,6 +41598,7 @@ class GodotServer {
41229
41598
  notRun: suite.discovered > suite.tests ? suite.discovered - suite.tests : undefined,
41230
41599
  failures: suite.failures,
41231
41600
  errors: suite.errors,
41601
+ hookFailures: suite.hookFailures + suite.hookErrors > 0 ? suite.hookFailures + suite.hookErrors : undefined,
41232
41602
  skipped: suite.skipped,
41233
41603
  orphans: orphansIn(suite.path) > 0 ? orphansIn(suite.path) : undefined,
41234
41604
  time: suite.time
@@ -41242,7 +41612,7 @@ class GodotServer {
41242
41612
  }
41243
41613
  async godotVersion(godotPath) {
41244
41614
  try {
41245
- const { stdout } = await run5(godotPath, ["--version"], { timeout: 1e4 });
41615
+ const { stdout } = await run4(godotPath, ["--version"], { timeout: 1e4 });
41246
41616
  this.lastGodotVersion = stdout.trim();
41247
41617
  return this.lastGodotVersion;
41248
41618
  } catch (error) {
@@ -41254,8 +41624,18 @@ class GodotServer {
41254
41624
  const answer = await this.handleLSP("lsp_get_diagnostics", args);
41255
41625
  const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
41256
41626
  if (payload["error"] !== undefined) {
41257
- const reason = payload["error"];
41258
- return this.createErrorResponse(`Diagnostics unavailable: ${typeof reason === "string" ? reason : JSON.stringify(reason)}`, [
41627
+ const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
41628
+ if (payload["refusedArguments"] === true) {
41629
+ return this.createErrorResponse(reason);
41630
+ }
41631
+ if (typeof payload["serves"] === "string") {
41632
+ return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
41633
+ "editor_status names the editor on the bridge and the port it says it serves",
41634
+ "editor_launch opens this project's own editor, on a language server port of its own",
41635
+ "GDHARNESS_LSP_PORT points this server at the right one"
41636
+ ]);
41637
+ }
41638
+ return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
41259
41639
  `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
41260
41640
  "GDHARNESS_LSP_PORT points this server at another one"
41261
41641
  ]);
@@ -41286,11 +41666,11 @@ class GodotServer {
41286
41666
  const cached = cachedClasses(projectPath);
41287
41667
  const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
41288
41668
  const contained = resolveWithinProject(projectPath, resourcePath);
41289
- if (!contained.ok || !existsSync15(contained.absolutePath)) {
41669
+ if (!contained.ok || !existsSync17(contained.absolutePath)) {
41290
41670
  return null;
41291
41671
  }
41292
41672
  try {
41293
- return readFileSync19(contained.absolutePath, "utf8");
41673
+ return readFileSync20(contained.absolutePath, "utf8");
41294
41674
  } catch {
41295
41675
  return null;
41296
41676
  }
@@ -41299,7 +41679,7 @@ class GodotServer {
41299
41679
  const notes = [];
41300
41680
  if (contradicted.length > 0) {
41301
41681
  const dependsOn = new Set;
41302
- for (const one of contradicted) {
41682
+ for (const one of contradicted.filter((entry) => entry.kind === "method" || entry.kind === "property")) {
41303
41683
  const source = cached === null ? null : this.sourceOfClass(projectPath, one.declaredIn);
41304
41684
  if (source === null || cached === null) {
41305
41685
  continue;
@@ -41339,17 +41719,18 @@ class GodotServer {
41339
41719
  });
41340
41720
  }
41341
41721
  classesAScriptNames(projectPath, scriptPath) {
41342
- const source = this.sourceOfClass(projectPath, scriptPath);
41722
+ const resourcePath = scriptPath.startsWith("res://") ? scriptPath : `res://${scriptPath.replace(/^\/+/, "")}`;
41723
+ const source = this.sourceOfClass(projectPath, resourcePath);
41343
41724
  const known = cachedClasses(projectPath) ?? declaredClasses(projectPath);
41344
- return source === null ? [] : classesNamedIn(source, known).filter((name) => known.get(name) !== scriptPath);
41725
+ return source === null ? [] : classesNamedIn(source, known).filter((name) => known.get(name) !== resourcePath);
41345
41726
  }
41346
41727
  sourceOfClass(projectPath, resourcePath) {
41347
41728
  const contained = resolveWithinProject(projectPath, resourcePath);
41348
- if (!contained.ok || !existsSync15(contained.absolutePath)) {
41729
+ if (!contained.ok || !existsSync17(contained.absolutePath)) {
41349
41730
  return null;
41350
41731
  }
41351
41732
  try {
41352
- return readFileSync19(contained.absolutePath, "utf8");
41733
+ return readFileSync20(contained.absolutePath, "utf8");
41353
41734
  } catch {
41354
41735
  return null;
41355
41736
  }
@@ -41537,7 +41918,8 @@ class GodotServer {
41537
41918
  return {
41538
41919
  playing: readBoolean(asParams(answer), "playing") ?? false,
41539
41920
  scene: readString(asParams(answer), "scenePath") ?? "",
41540
- debugPort: readNumber(asParams(answer), "debugPort")
41921
+ debugPort: readNumber(asParams(answer), "debugPort"),
41922
+ play: playOf(asParams(answer))
41541
41923
  };
41542
41924
  } catch {
41543
41925
  return null;
@@ -41633,7 +42015,7 @@ class GodotServer {
41633
42015
  return null;
41634
42016
  }
41635
42017
  try {
41636
- return settingKeys(readFileSync19(join22(projectPath, "project.godot"), "utf8"));
42018
+ return settingKeys(readFileSync20(join23(projectPath, "project.godot"), "utf8"));
41637
42019
  } catch {
41638
42020
  return null;
41639
42021
  }
@@ -41796,6 +42178,13 @@ class GodotServer {
41796
42178
  if (!project.ok) {
41797
42179
  return project.response;
41798
42180
  }
42181
+ const open = this.godotBridge.isConnected() ? this.godotBridge.getStatus().projectPath : undefined;
42182
+ if (op !== "check" && open !== undefined && open !== "" && !isSameDirectory(open, project.value.path)) {
42183
+ return this.createErrorResponse(`This start names ${project.value.path}, and the editor on this bridge has ${open} open.`, [
42184
+ "editor_status names the project this server's editor is showing",
42185
+ "One server serves one editor: start a second server for the other project"
42186
+ ]);
42187
+ }
41799
42188
  const scene = readNonEmptyString(args, "scene");
41800
42189
  const sceneToRun = scene !== undefined ? resolveWithinProject(project.value.path, scene) : null;
41801
42190
  if (sceneToRun && !sceneToRun.ok) {
@@ -41890,7 +42279,7 @@ class GodotServer {
41890
42279
  }
41891
42280
  async runtimeUp(projectPath, before, budget, withArgs) {
41892
42281
  const budgetMs = budget.ms;
41893
- if (!existsSync15(join22(projectPath, RUNTIME_AUTOLOAD.path))) {
42282
+ if (!existsSync17(join23(projectPath, RUNTIME_AUTOLOAD.path))) {
41894
42283
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
41895
42284
  }
41896
42285
  let asking = null;
@@ -41899,7 +42288,7 @@ class GodotServer {
41899
42288
  let editorAsks = 0;
41900
42289
  const waitBegan = Date.now();
41901
42290
  const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
41902
- const endpoint = await announcedSince(projectPath, before, {
42291
+ const fresh = await announcedSince(projectPath, before, {
41903
42292
  budgetMs,
41904
42293
  accept: (one) => this.isTheGameOf(this.activeProcess, one, projectPath),
41905
42294
  giveUp: () => {
@@ -41923,11 +42312,19 @@ class GodotServer {
41923
42312
  }
41924
42313
  });
41925
42314
  const asked = editorAsks === 0 ? "editor not asked" : `editor asked ${editorAsks === 1 ? "once" : `${editorAsks} times`}, last said ${lastSaid()}`;
41926
- this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, ${asked}`);
42315
+ this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${fresh.length === 0 ? "nothing announced" : `pid ${fresh.map((one) => one.pid).join(", ")} announced`}, ${asked}`);
41927
42316
  const going = await this.currentRun();
42317
+ const endpoint = await this.theRunsAmong(going, fresh);
41928
42318
  if (endpoint !== null && going !== null && going.announcedPid === undefined) {
41929
42319
  this.tie(going, endpoint);
41930
42320
  }
42321
+ if (endpoint === null && fresh.length > 1) {
42322
+ return {
42323
+ listening: true,
42324
+ pids: fresh.map((one) => one.pid),
42325
+ 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.`
42326
+ };
42327
+ }
41931
42328
  return runtimeVerdict(endpoint, {
41932
42329
  addon: true,
41933
42330
  budgetMs,
@@ -42011,7 +42408,13 @@ class GodotServer {
42011
42408
  };
42012
42409
  this.activeProcess = played;
42013
42410
  this.watchThePlayedConsole();
42014
- writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
42411
+ const which = playOf(playAnswer);
42412
+ writeEditorRunNote({
42413
+ projectPath,
42414
+ transcript: transcript.path,
42415
+ startedAt,
42416
+ ...which === null ? {} : { play: which }
42417
+ });
42015
42418
  sweepTranscripts();
42016
42419
  return this.jsonTextResponse({
42017
42420
  started: true,
@@ -42126,7 +42529,7 @@ class GodotServer {
42126
42529
  return 0;
42127
42530
  }
42128
42531
  try {
42129
- return reportAlreadyInTranscript(readFileSync19(game.transcript), readFileSync19(report));
42532
+ return reportAlreadyInTranscript(readFileSync20(game.transcript), readFileSync20(report));
42130
42533
  } catch (error) {
42131
42534
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
42132
42535
  return 0;
@@ -42140,6 +42543,18 @@ class GodotServer {
42140
42543
  game.brokeOn = halt.text;
42141
42544
  game.log.record("error", halt.text);
42142
42545
  }
42546
+ async untilTheEditorStopsPlaying(withinMs = STOP_WAIT_MS) {
42547
+ const deadline = Date.now() + withinMs;
42548
+ for (;; ) {
42549
+ if ((await this.editorPlayingState())?.playing === false) {
42550
+ return true;
42551
+ }
42552
+ if (Date.now() >= deadline) {
42553
+ return false;
42554
+ }
42555
+ await delay3(100);
42556
+ }
42557
+ }
42143
42558
  async endActiveGame(reason, going) {
42144
42559
  const running = this.activeProcess;
42145
42560
  const project = running?.projectPath ?? this.ourProject();
@@ -42158,8 +42573,16 @@ class GodotServer {
42158
42573
  running.endedHere = reason;
42159
42574
  const announced = this.announcedPidOf(running);
42160
42575
  if (running.throughEditor) {
42161
- await this.handleViaBridge("stop_playing", {});
42162
- return await untilGone(announced === undefined ? [] : [announced]) ? "gone" : "lingering";
42576
+ const asked = await this.handleViaBridge("stop_playing", {});
42577
+ if (asked.isError === true) {
42578
+ running.endedHere = null;
42579
+ 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"}).`);
42580
+ return "unreached";
42581
+ }
42582
+ if (announced === undefined) {
42583
+ return await this.untilTheEditorStopsPlaying() ? "gone" : "lingering";
42584
+ }
42585
+ return await untilGone([announced]) ? "gone" : "lingering";
42163
42586
  }
42164
42587
  if (running.pid === null) {
42165
42588
  return "gone";
@@ -42199,7 +42622,7 @@ class GodotServer {
42199
42622
  return gone ? "gone" : "lingering";
42200
42623
  }
42201
42624
  spawnGame(godotPath, cmdArgs, env) {
42202
- const child = spawn3(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
42625
+ const child = spawn4(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
42203
42626
  const cancelled = callSignal();
42204
42627
  if (cancelled !== undefined) {
42205
42628
  const end = () => {
@@ -42239,7 +42662,6 @@ class GodotServer {
42239
42662
  });
42240
42663
  child.on("exit", (code, signal) => {
42241
42664
  this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
42242
- log.finish();
42243
42665
  started.exitCode = code;
42244
42666
  started.exitSignal = code === null ? signal : null;
42245
42667
  });
@@ -42249,7 +42671,28 @@ class GodotServer {
42249
42671
  `);
42250
42672
  started.exitCode = -1;
42251
42673
  });
42252
- return started;
42674
+ const settled = new Promise((resolve) => {
42675
+ let open = 2;
42676
+ const done = () => {
42677
+ log.finish();
42678
+ resolve();
42679
+ };
42680
+ const closed = () => {
42681
+ open -= 1;
42682
+ if (open === 0) {
42683
+ done();
42684
+ }
42685
+ };
42686
+ child.stdout.once("close", closed);
42687
+ child.stderr.once("close", closed);
42688
+ child.once("exit", () => {
42689
+ setTimeout(done, STREAMS_GRACE_MS).unref();
42690
+ });
42691
+ child.once("error", () => {
42692
+ done();
42693
+ });
42694
+ });
42695
+ return Object.assign(started, { settled });
42253
42696
  }
42254
42697
  async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
42255
42698
  const startedAt = Date.now();
@@ -42331,7 +42774,8 @@ class GodotServer {
42331
42774
  this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
42332
42775
  }
42333
42776
  const project = this.godotBridge.getStatus().projectPath ?? null;
42334
- const note = project === null ? null : readEditorRunNote(project);
42777
+ const recorded = project === null ? null : readEditorRunNote(project);
42778
+ const note = recorded !== null && noteIsAbout(recorded, playing.play) ? recorded : null;
42335
42779
  const picked = {
42336
42780
  pid: null,
42337
42781
  log: new GameLog,
@@ -42388,6 +42832,31 @@ class GodotServer {
42388
42832
  const its = await this.tiedThroughTheTree(run, running);
42389
42833
  return its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
42390
42834
  }
42835
+ async theRunsAmong(run, fresh) {
42836
+ if (fresh.length === 1) {
42837
+ return fresh[0] ?? null;
42838
+ }
42839
+ if (fresh.length === 0 || run === null) {
42840
+ return null;
42841
+ }
42842
+ const pid = run.announcedPid ?? await this.tiedThroughTheTree(run, fresh);
42843
+ return fresh.find((one) => one.pid === pid) ?? null;
42844
+ }
42845
+ async tieThePlayedGame(run) {
42846
+ if (!run.throughEditor || run.errorReport !== undefined || run.announcedPid !== undefined) {
42847
+ return;
42848
+ }
42849
+ const announced = runtimesAnnounced().running;
42850
+ if (this.announcedPidOf(run, announced) !== undefined || !stillRunning(run)) {
42851
+ return;
42852
+ }
42853
+ const among = this.announcedOfTheRunsProject(run, announced).map((one) => one.pid).sort((one, other) => one - other).join(",");
42854
+ if (among === "" || run.treeReadFor === among) {
42855
+ return;
42856
+ }
42857
+ run.treeReadFor = among;
42858
+ await this.tiedThroughTheTree(run, announced);
42859
+ }
42391
42860
  async tiedThroughTheTree(run, announced) {
42392
42861
  if (run.announcedPid !== undefined) {
42393
42862
  return run.announcedPid;
@@ -42588,6 +43057,7 @@ class GodotServer {
42588
43057
  resolve(false);
42589
43058
  });
42590
43059
  });
43060
+ await boot.settled;
42591
43061
  const errors = boot.log.count("error");
42592
43062
  const warnings = boot.log.count("warning");
42593
43063
  return this.jsonTextResponse({
@@ -42729,6 +43199,7 @@ class GodotServer {
42729
43199
  if (!run) {
42730
43200
  return this.createErrorResponse(this.nothingOfOursIsRunning());
42731
43201
  }
43202
+ await this.tieThePlayedGame(run);
42732
43203
  this.drainEditorOutput(run);
42733
43204
  this.drainTranscript(run);
42734
43205
  const going = await this.runStillGoing(run);
@@ -42764,7 +43235,7 @@ class GodotServer {
42764
43235
  notes.push(endedWithoutACode({
42765
43236
  throughEditor: run.throughEditor,
42766
43237
  announced: announced !== undefined || run.announcedPid !== undefined,
42767
- couldAnnounce: run.projectPath !== null && existsSync15(join22(run.projectPath, RUNTIME_AUTOLOAD.path)),
43238
+ couldAnnounce: run.projectPath !== null && existsSync17(join23(run.projectPath, RUNTIME_AUTOLOAD.path)),
42768
43239
  endedHere: typeof run.endedHere === "string",
42769
43240
  pickedUp: run.pickedUp === true
42770
43241
  }));
@@ -42772,6 +43243,9 @@ class GodotServer {
42772
43243
  if (run.pickedUpPlaying === true) {
42773
43244
  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.");
42774
43245
  }
43246
+ if (errorsUnread(run)) {
43247
+ 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.");
43248
+ }
42775
43249
  if (run.consoleLost === true) {
42776
43250
  const back = this.dapClient?.isConnected() === true;
42777
43251
  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.`);
@@ -42787,8 +43261,9 @@ class GodotServer {
42787
43261
  pid: run.pid ?? announced ?? run.announcedPid ?? null,
42788
43262
  errors: run.log.count("error"),
42789
43263
  warnings: run.log.count("warning"),
42790
- clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
43264
+ clean: cleanVerdict(run),
42791
43265
  consoleLost: run.consoleLost === true ? true : undefined,
43266
+ errorsUnread: errorsUnread(run) ? true : undefined,
42792
43267
  ...hold,
42793
43268
  endedUnwatched: run.endedUnwatched === true ? true : undefined,
42794
43269
  endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
@@ -42823,6 +43298,7 @@ class GodotServer {
42823
43298
  if (elsewhere !== null) {
42824
43299
  return elsewhere;
42825
43300
  }
43301
+ await this.tieThePlayedGame(stopped);
42826
43302
  this.drainEditorOutput(stopped);
42827
43303
  this.drainTranscript(stopped);
42828
43304
  const wasRunning = await this.runStillGoing(stopped);
@@ -42831,7 +43307,7 @@ class GodotServer {
42831
43307
  const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
42832
43308
  this.logDebug("Stopping the running game");
42833
43309
  const ending = await this.endActiveGame("editor_run stop", wasRunning);
42834
- const refused = ending === "refused";
43310
+ const refused = ending === "refused" || ending === "unreached";
42835
43311
  const ended = children === null || refused ? null : endChildrenAmong(children);
42836
43312
  if (wasRunning && !refused) {
42837
43313
  for (const pid of new Set([endedPid, announcedPid])) {
@@ -42846,7 +43322,7 @@ class GodotServer {
42846
43322
  endedPid,
42847
43323
  throughEditor: stopped.throughEditor,
42848
43324
  exitSignal: stopped.exitSignal,
42849
- errors: stopped.log.count("error")
43325
+ clean: cleanVerdict(stopped)
42850
43326
  });
42851
43327
  return this.jsonTextResponse({
42852
43328
  stopped: verdict.stopped,
@@ -42860,6 +43336,7 @@ class GodotServer {
42860
43336
  errors: stopped.log.count("error"),
42861
43337
  warnings: stopped.log.count("warning"),
42862
43338
  clean: verdict.clean,
43339
+ errorsUnread: errorsUnread(stopped) ? true : undefined,
42863
43340
  note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
42864
43341
  entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
42865
43342
  });
@@ -43072,7 +43549,12 @@ class GodotServer {
43072
43549
  const stillAtMissingPaths = atMissingPaths.filter((name) => !dropped.includes(name));
43073
43550
  const reloading = readNonEmptyString(args, "reloadScript");
43074
43551
  let reloaded = {};
43075
- if (reloading !== undefined) {
43552
+ const reloadingSource = reloading === undefined || projectPath === "" ? null : this.sourceOfClass(projectPath, reloading);
43553
+ if (reloading !== undefined && reloadingSource !== null && isToolScript(reloadingSource)) {
43554
+ reloaded = {
43555
+ 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.`
43556
+ };
43557
+ } else if (reloading !== undefined) {
43076
43558
  try {
43077
43559
  const answer = asParams(await this.godotBridge.invokeTool("reload_script", { ...args, scriptPath: reloading }));
43078
43560
  const methods = readArray(answer, "methods");
@@ -43080,29 +43562,36 @@ class GodotServer {
43080
43562
  const found = readArray(answer, field);
43081
43563
  return found ? { [field]: found.map(String) } : {};
43082
43564
  };
43565
+ const asResource = (path) => path.startsWith("res://") ? path : `res://${path.replace(/^\/+/, "")}`;
43566
+ const script = asResource(readString(answer, "script") ?? reloading);
43567
+ const spelling = script === asResource(reloading) ? {} : { as: script };
43083
43568
  const failed = answer["failed"];
43084
43569
  if (typeof failed === "number" && failed !== 0) {
43085
43570
  reloaded = {
43571
+ ...spelling,
43086
43572
  ...listed("heldBefore"),
43087
43573
  ...listed("heldConstantsBefore"),
43088
43574
  problem: failedReloadNote({
43089
- script: reloading,
43575
+ script,
43090
43576
  code: failed,
43091
43577
  codeName: readString(answer, "failedAs") ?? "",
43092
43578
  heldBefore: listed("heldBefore")["heldBefore"] ?? [],
43093
43579
  heldAfter: methods?.map(String) ?? [],
43094
43580
  constantsBefore: listed("heldConstantsBefore")["heldConstantsBefore"] ?? [],
43095
43581
  constantsAfter: listed("constants")["constants"] ?? [],
43096
- names: projectPath === "" ? [] : this.classesAScriptNames(projectPath, reloading)
43582
+ names: projectPath === "" ? [] : this.classesAScriptNames(projectPath, script),
43583
+ restored: typeof answer["restored"] === "boolean" ? answer["restored"] : null,
43584
+ restoredAs: readString(answer, "restoredAs") ?? ""
43097
43585
  })
43098
43586
  };
43099
43587
  } else {
43100
43588
  reloaded = methods ? {
43589
+ ...spelling,
43101
43590
  methods: methods.map(String),
43102
43591
  ...listed("heldBefore"),
43103
43592
  ...listed("constants"),
43104
43593
  ...listed("heldConstantsBefore")
43105
- } : { problem: `the editor would not say what ${reloading} has after reloading it` };
43594
+ } : { problem: `the editor would not say what ${script} has after reloading it` };
43106
43595
  }
43107
43596
  } catch (error) {
43108
43597
  reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
@@ -43124,7 +43613,7 @@ class GodotServer {
43124
43613
  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.");
43125
43614
  }
43126
43615
  if (dependents !== null && dependents.notReloaded.length > 0) {
43127
- 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.`);
43616
+ notes.push(notReloadedNote(broughtIn, dependents.notReloaded));
43128
43617
  }
43129
43618
  for (const group of uidsDuplicatedOnDisk) {
43130
43619
  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.`);
@@ -43139,6 +43628,7 @@ class GodotServer {
43139
43628
  stillWorking: busy,
43140
43629
  waitedMs: Date.now() - started,
43141
43630
  waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
43631
+ reloadedAs: reloaded.as,
43142
43632
  heldBeforeReload: reloaded.heldBefore,
43143
43633
  reloadedMethods: reloaded.methods,
43144
43634
  heldConstantsBeforeReload: reloaded.heldConstantsBefore,
@@ -43165,7 +43655,8 @@ class GodotServer {
43165
43655
  if (tool) {
43166
43656
  notReloaded.push({
43167
43657
  scriptPath,
43168
- problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it"
43658
+ problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it",
43659
+ tool: true
43169
43660
  });
43170
43661
  continue;
43171
43662
  }
@@ -43173,9 +43664,10 @@ class GodotServer {
43173
43664
  const answer = asParams(await this.godotBridge.invokeTool("reload_script", { scriptPath }));
43174
43665
  const failed = answer["failed"];
43175
43666
  if (typeof failed === "number" && failed !== 0) {
43667
+ 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";
43176
43668
  notReloaded.push({
43177
43669
  scriptPath,
43178
- problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}`
43670
+ problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}${left}`
43179
43671
  });
43180
43672
  } else if (readArray(answer, "methods") === undefined) {
43181
43673
  notReloaded.push({
@@ -43247,6 +43739,35 @@ class GodotServer {
43247
43739
  }
43248
43740
  };
43249
43741
  }
43742
+ async handleCreateScript(args) {
43743
+ const project = this.project(args);
43744
+ if (!project.ok) {
43745
+ return project.response;
43746
+ }
43747
+ const contained = this.containProjectFiles(args);
43748
+ if (!contained.ok) {
43749
+ return contained.response;
43750
+ }
43751
+ const { op: _op, projectPath: _projectPath, ...params } = contained.value;
43752
+ const created = await this.operation("create_script", params, project.value.path);
43753
+ const declared = created.ok ? readString(created.payload, "class_name") ?? "" : "";
43754
+ if (!created.ok || declared === "") {
43755
+ return this.answer(created);
43756
+ }
43757
+ const rebuilt = await this.rebuildClassCache(project.value.path);
43758
+ const registered = cachedClasses(project.value.path)?.get(declared) === readString(created.payload, "full_path");
43759
+ const answered = this.answer({
43760
+ ...created,
43761
+ payload: {
43762
+ ...created.payload,
43763
+ registered,
43764
+ ...registered ? {} : {
43765
+ 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}`
43766
+ }
43767
+ }
43768
+ });
43769
+ return this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
43770
+ }
43250
43771
  async alsoSayWhatTheEditorCannotSee(answered, args) {
43251
43772
  const first = answered.content[0];
43252
43773
  if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
@@ -43375,7 +43896,7 @@ class GodotServer {
43375
43896
  async ownRunOnItsWay(projectPath, budgetMs) {
43376
43897
  const run = await this.currentRun();
43377
43898
  const project = projectPath ?? run?.projectPath ?? null;
43378
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync15(join22(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
43899
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync17(join23(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
43379
43900
  return null;
43380
43901
  }
43381
43902
  const already = runtimesAnnounced().running;
@@ -43399,11 +43920,15 @@ class GodotServer {
43399
43920
  const seconds = Math.round((Date.now() - run.startedAt) / 1000);
43400
43921
  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.`;
43401
43922
  }
43402
- const endpoint = await announcedSince(project, new Set(already.map((one) => one.pid)), {
43923
+ const fresh = await announcedSince(project, new Set(already.map((one) => one.pid)), {
43403
43924
  budgetMs: Math.min(budgetMs, remaining),
43404
43925
  accept: (one) => this.isTheGameOf(this.activeProcess, one, project),
43405
43926
  giveUp: () => this.activeProcess !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
43406
43927
  });
43928
+ const endpoint = await this.theRunsAmong(run, fresh);
43929
+ if (fresh.length > 0 && endpoint === null) {
43930
+ return null;
43931
+ }
43407
43932
  if (endpoint !== null) {
43408
43933
  const tiedMeanwhile = () => run.announcedPid !== undefined;
43409
43934
  if (!tiedMeanwhile()) {
@@ -43455,7 +43980,7 @@ class GodotServer {
43455
43980
  }
43456
43981
  }
43457
43982
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
43458
- const screenshotPath = screenshotDir ? join22(screenshotDir, "capture.png") : null;
43983
+ const screenshotPath = screenshotDir ? join23(screenshotDir, "capture.png") : null;
43459
43984
  try {
43460
43985
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
43461
43986
  if (!reply.ok && reply.requestId !== undefined) {
@@ -43496,7 +44021,7 @@ class GodotServer {
43496
44021
  type: "text",
43497
44022
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
43498
44023
  },
43499
- { type: "image", data: readFileSync19(screenshotPath).toString("base64"), mimeType: "image/png" }
44024
+ { type: "image", data: readFileSync20(screenshotPath).toString("base64"), mimeType: "image/png" }
43500
44025
  ]
43501
44026
  };
43502
44027
  } finally {
@@ -43596,7 +44121,7 @@ async function runGodotServer() {
43596
44121
  const server = new GodotServer;
43597
44122
  await server.run();
43598
44123
  }
43599
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
44124
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
43600
44125
  var init_server3 = __esm(() => {
43601
44126
  init_mcp();
43602
44127
  init_stdio2();
@@ -43612,6 +44137,7 @@ var init_server3 = __esm(() => {
43612
44137
  init_dap_client();
43613
44138
  init_desktop();
43614
44139
  init_editor_log();
44140
+ init_engine_run();
43615
44141
  init_errors();
43616
44142
  init_game_log();
43617
44143
  init_godot_bridge();
@@ -43627,7 +44153,6 @@ var init_server3 = __esm(() => {
43627
44153
  init_process_children();
43628
44154
  init_process_time();
43629
44155
  init_project_scan();
43630
- init_reimport();
43631
44156
  init_resources();
43632
44157
  init_restart_note();
43633
44158
  init_run_record();
@@ -43637,8 +44162,8 @@ var init_server3 = __esm(() => {
43637
44162
  init_setup();
43638
44163
  init_tool_definitions();
43639
44164
  init_update_check();
43640
- run5 = promisify6(execFile6);
43641
- __dirname2 = dirname15(fileURLToPath5(import.meta.url));
44165
+ run4 = promisify5(execFile5);
44166
+ __dirname2 = dirname16(fileURLToPath5(import.meta.url));
43642
44167
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
43643
44168
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
43644
44169
  VALUE_NAMES = {
@@ -43687,8 +44212,8 @@ var init_server3 = __esm(() => {
43687
44212
  init_bridge_announce();
43688
44213
  init_errors();
43689
44214
  init_godot_path();
43690
- import { existsSync as existsSync16 } from "node:fs";
43691
- import { join as join23, resolve as resolve6 } from "node:path";
44215
+ import { existsSync as existsSync18 } from "node:fs";
44216
+ import { join as join24, resolve as resolve6 } from "node:path";
43692
44217
 
43693
44218
  // src/harnesses.ts
43694
44219
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
@@ -44587,8 +45112,8 @@ init_setup();
44587
45112
  // src/skill.ts
44588
45113
  init_setup();
44589
45114
  init_tool_definitions();
44590
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
44591
- import { basename as basename2, dirname as dirname5, join as join10 } from "node:path";
45115
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
45116
+ import { basename as basename3, dirname as dirname6, join as join11 } from "node:path";
44592
45117
 
44593
45118
  // src/tool-reference.ts
44594
45119
  init_headless_operations();
@@ -44684,7 +45209,7 @@ function renderToolsMarkdown() {
44684
45209
 
44685
45210
  // src/skill.ts
44686
45211
  var SKILL_NAME = "gdharness";
44687
- var SHARED_SKILLS = join10(".agents", "skills");
45212
+ var SHARED_SKILLS = join11(".agents", "skills");
44688
45213
  function skillMarkdown(version) {
44689
45214
  return `---
44690
45215
  name: ${SKILL_NAME}
@@ -44721,9 +45246,10 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
44721
45246
  - Read \`editor_output\` after every run. It returns the engine's errors and warnings as entries
44722
45247
  with their backtraces, and a \`clean\` verdict, so a run that printed an error is one call away
44723
45248
  from being known. For a run the editor plays, the errors come from the game through the runtime
44724
- addon, so a played game without the addon is judged on its prints alone and \`clean\` there says
44725
- less. For a long run it also names a transcript file: read that for output and this
44726
- answer for state. Never decide a run has ended by watching the file stop growing, or a process
45249
+ addon, so a played game without the addon answers \`errorsUnread\` and no \`clean\`, since
45250
+ what it raised reached nothing the server reads. For a long run it also names a transcript file:
45251
+ read that for output and this answer for state. Never decide a run has ended by watching the
45252
+ file stop growing, or a process
44727
45253
  list, or a timeout; a bench between prints looks exactly like a bench that died to everything else.
44728
45254
  \`endedBy\` says whether gdharness ended it, and \`running\` is asked of the operating system
44729
45255
  whenever there is a process to ask about: every run started through \`editor_run\`, and every
@@ -44875,27 +45401,27 @@ whatever the engine said on stderr comes back under \`engine_messages\`.
44875
45401
  function skillFiles(version) {
44876
45402
  return new Map([
44877
45403
  ["SKILL.md", skillMarkdown(version)],
44878
- [join10("references", "tools.md"), renderToolsMarkdown()]
45404
+ [join11("references", "tools.md"), renderToolsMarkdown()]
44879
45405
  ]);
44880
45406
  }
44881
45407
  function skillDirectories(harnesses, projectPath, exists) {
44882
- const directories = [join10(projectPath, SHARED_SKILLS, SKILL_NAME)];
45408
+ const directories = [join11(projectPath, SHARED_SKILLS, SKILL_NAME)];
44883
45409
  for (const harness of harnesses) {
44884
45410
  if (harness.skills === undefined) {
44885
45411
  continue;
44886
45412
  }
44887
- const own = join10(projectPath, harness.skills.dir);
44888
- if ((!harness.skills.shared || exists(own)) && !directories.includes(join10(own, SKILL_NAME))) {
44889
- directories.push(join10(own, SKILL_NAME));
45413
+ const own = join11(projectPath, harness.skills.dir);
45414
+ if ((!harness.skills.shared || exists(own)) && !directories.includes(join11(own, SKILL_NAME))) {
45415
+ directories.push(join11(own, SKILL_NAME));
44890
45416
  }
44891
45417
  }
44892
45418
  return directories;
44893
45419
  }
44894
45420
  function everySkillDirectory(projectPath, harnesses) {
44895
- const directories = new Set([join10(projectPath, SHARED_SKILLS, SKILL_NAME)]);
45421
+ const directories = new Set([join11(projectPath, SHARED_SKILLS, SKILL_NAME)]);
44896
45422
  for (const harness of harnesses) {
44897
45423
  if (harness.skills !== undefined) {
44898
- directories.add(join10(projectPath, harness.skills.dir, SKILL_NAME));
45424
+ directories.add(join11(projectPath, harness.skills.dir, SKILL_NAME));
44899
45425
  }
44900
45426
  }
44901
45427
  return [...directories];
@@ -44903,52 +45429,52 @@ function everySkillDirectory(projectPath, harnesses) {
44903
45429
  function removeSkill(directories, projectPath) {
44904
45430
  const removed = [];
44905
45431
  for (const directory of directories) {
44906
- if (existsSync9(directory)) {
44907
- rmSync5(directory, { recursive: true, force: true });
44908
- pruneEmpty(dirname5(directory), projectPath);
45432
+ if (existsSync11(directory)) {
45433
+ rmSync6(directory, { recursive: true, force: true });
45434
+ pruneEmpty(dirname6(directory), projectPath);
44909
45435
  removed.push(directory);
44910
45436
  }
44911
45437
  }
44912
45438
  return removed;
44913
45439
  }
44914
45440
  function pruneEmpty(directory, projectPath) {
44915
- const shared = join10(projectPath, ".agents");
45441
+ const shared = join11(projectPath, ".agents");
44916
45442
  let path = directory;
44917
- while (path !== projectPath && (basename2(path) === "skills" || path === shared)) {
45443
+ while (path !== projectPath && (basename3(path) === "skills" || path === shared)) {
44918
45444
  if (readdirSync5(path).length > 0) {
44919
45445
  return;
44920
45446
  }
44921
- rmSync5(path, { recursive: true, force: true });
44922
- path = dirname5(path);
45447
+ rmSync6(path, { recursive: true, force: true });
45448
+ path = dirname6(path);
44923
45449
  }
44924
45450
  }
44925
45451
  function writeSkill(directories, version) {
44926
45452
  const files = skillFiles(version);
44927
45453
  return directories.map((directory) => {
44928
- const replaced = existsSync9(directory);
44929
- rmSync5(directory, { recursive: true, force: true });
45454
+ const replaced = existsSync11(directory);
45455
+ rmSync6(directory, { recursive: true, force: true });
44930
45456
  for (const [name, contents] of files) {
44931
- const path = join10(directory, name);
44932
- mkdirSync5(dirname5(path), { recursive: true });
44933
- writeFileSync6(path, contents, "utf8");
45457
+ const path = join11(directory, name);
45458
+ mkdirSync5(dirname6(path), { recursive: true });
45459
+ writeFileSync7(path, contents, "utf8");
44934
45460
  }
44935
45461
  return { path: directory, replaced };
44936
45462
  });
44937
45463
  }
44938
45464
 
44939
45465
  // src/version.ts
44940
- import { readFileSync as readFileSync8 } from "node:fs";
44941
- import { dirname as dirname6, join as join11 } from "node:path";
45466
+ import { readFileSync as readFileSync10 } from "node:fs";
45467
+ import { dirname as dirname7, join as join12 } from "node:path";
44942
45468
  import { fileURLToPath as fileURLToPath2 } from "node:url";
44943
45469
  function getLocalVersion() {
44944
- const currentDirectory = dirname6(fileURLToPath2(import.meta.url));
45470
+ const currentDirectory = dirname7(fileURLToPath2(import.meta.url));
44945
45471
  const packageCandidates = [
44946
- join11(currentDirectory, "..", "package.json"),
44947
- join11(currentDirectory, "..", "..", "package.json")
45472
+ join12(currentDirectory, "..", "package.json"),
45473
+ join12(currentDirectory, "..", "..", "package.json")
44948
45474
  ];
44949
45475
  for (const packagePath of packageCandidates) {
44950
45476
  try {
44951
- const value = JSON.parse(readFileSync8(packagePath, "utf-8"));
45477
+ const value = JSON.parse(readFileSync10(packagePath, "utf-8"));
44952
45478
  if (typeof value === "object" && value !== null && "name" in value && value.name === "gdharness" && "version" in value && typeof value.version === "string") {
44953
45479
  return value.version;
44954
45480
  }
@@ -44966,7 +45492,7 @@ class UsageError extends Refusal {
44966
45492
  function projectArgument(at) {
44967
45493
  const named = args.slice(at).find((value) => !value.startsWith("--"));
44968
45494
  const projectPath = resolve6(named ?? ".");
44969
- if (!existsSync16(join23(projectPath, "project.godot"))) {
45495
+ if (!existsSync18(join24(projectPath, "project.godot"))) {
44970
45496
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
44971
45497
  }
44972
45498
  return projectPath;
@@ -45005,7 +45531,7 @@ async function registerRuntime(godot, projectPath) {
45005
45531
  return;
45006
45532
  }
45007
45533
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
45008
- if (!existsSync16(join23(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
45534
+ if (!existsSync18(join24(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
45009
45535
  console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
45010
45536
  }
45011
45537
  }
@@ -45137,7 +45663,7 @@ async function setup() {
45137
45663
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
45138
45664
  }
45139
45665
  if (!args.includes("--no-skill")) {
45140
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync16);
45666
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync18);
45141
45667
  for (const written of writeSkill(directories, getLocalVersion())) {
45142
45668
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
45143
45669
  }
@@ -45231,7 +45757,7 @@ async function upgrade() {
45231
45757
  moved.push(took.from);
45232
45758
  }
45233
45759
  }
45234
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync16), version)) {
45760
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync18), version)) {
45235
45761
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
45236
45762
  }
45237
45763
  console.log(`