gdharness 1.1.21 → 1.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/build/cli.js +1036 -525
  2. package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
  3. package/build/godot/addons/gdharness_editor/reading.gd +17 -0
  4. package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
  5. package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
  6. package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
  7. package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
  8. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
  9. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
  10. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
  11. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
  13. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
  14. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
  15. package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
  16. package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
  17. package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +8 -4
  19. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
  20. package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
  22. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +28 -8
  23. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +7 -299
  24. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
  25. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
  26. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
  27. package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
  28. package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
  29. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +20 -9
  30. package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
  31. package/build/godot/operations/audio_buses.gd +47 -11
  32. package/build/godot/operations/class_cache.gd +18 -2
  33. package/build/godot/operations/dependencies.gd +128 -17
  34. package/build/godot/operations/file_walk.gd +20 -0
  35. package/build/godot/operations/gdscript_analysis.gd +59 -20
  36. package/build/godot/operations/gdscript_authoring.gd +127 -60
  37. package/build/godot/operations/godot_operations.gd +24 -5
  38. package/build/godot/operations/import_pipeline.gd +179 -19
  39. package/build/godot/operations/logger.gd +2 -2
  40. package/build/godot/operations/plugins.gd +88 -70
  41. package/build/godot/operations/project_config.gd +41 -6
  42. package/build/godot/operations/project_diagnostics.gd +25 -10
  43. package/build/godot/operations/reading.gd +17 -0
  44. package/build/godot/operations/resource_files.gd +60 -17
  45. package/build/godot/operations/serialisation.gd +116 -7
  46. package/build/index.js +949 -435
  47. package/package.json +1 -1
package/build/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,51 @@ 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
+
7853
7898
  // src/reimport.ts
7854
7899
  import { createHash } from "node:crypto";
7855
7900
  import { existsSync as existsSync5, readFileSync as readFileSync3, rmSync as rmSync3, statSync as statSync2, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
@@ -7993,10 +8038,8 @@ var init_scratch = __esm(() => {
7993
8038
  });
7994
8039
 
7995
8040
  // src/headless.ts
7996
- import { execFile as execFile3 } from "node:child_process";
7997
- import { writeFileSync as writeFileSync4 } from "node:fs";
7998
- import { join as join7 } from "node:path";
7999
- 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";
8000
8043
  function snakeCased(params) {
8001
8044
  const result = emptyRecord();
8002
8045
  for (const [key, value] of Object.entries(params)) {
@@ -8005,34 +8048,55 @@ function snakeCased(params) {
8005
8048
  }
8006
8049
  return result;
8007
8050
  }
8008
- function lastJsonObject(stdout) {
8009
- const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("{"));
8010
- for (const line of lines.reverse()) {
8011
- try {
8012
- const parsed = JSON.parse(line);
8013
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
8014
- return parsed;
8015
- }
8016
- } catch {}
8017
- }
8018
- 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
+ };
8019
8057
  }
8020
- function problems(stderr) {
8021
- const log = new GameLog;
8022
- log.append("stderr", stderr);
8023
- log.finish();
8024
- 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
+ });
8068
+ }
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 };
8025
8072
  }
8026
- function reason(stdout, stderr) {
8027
- const own = stderr.split(/\r?\n/).filter((line) => line.startsWith("[ERROR] ")).map((line) => line.slice("[ERROR] ".length));
8073
+ function reason(log, problems) {
8074
+ const own = log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
8028
8075
  if (own.length > 0) {
8029
- return own.join("; ");
8076
+ return own.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; ");
8030
8077
  }
8031
- const engine = problems(stderr).filter((entry) => entry.severity === "error").map((entry) => entry.text);
8032
- if (engine.length > 0) {
8033
- return engine.join("; ");
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("; ");
8082
+ }
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;
8034
8099
  }
8035
- return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
8036
8100
  }
8037
8101
  function librariesNotCopied(messages) {
8038
8102
  const found = new Set;
@@ -8072,30 +8136,33 @@ async function runImport(godotPath, projectPath, options = {}) {
8072
8136
  }
8073
8137
  async function importOnce(godotPath, projectPath) {
8074
8138
  const logDir = scratchDirectory("import");
8139
+ let ran;
8075
8140
  try {
8076
- const { stderr } = await run3(godotPath, ["--headless", "--log-file", join7(logDir, "engine.log"), "--path", projectPath, "--import"], { signal: callSignal() });
8077
- return { ok: true, messages: problems(stderr) };
8078
- } catch (error) {
8079
- if (error instanceof Error && "stdout" in error && "stderr" in error) {
8080
- const failed = error;
8081
- return {
8082
- ok: false,
8083
- message: `the import pass failed (exit ${failed.code ?? "unknown"}): ${reason(failed.stdout, failed.stderr)}`,
8084
- messages: problems(failed.stderr)
8085
- };
8086
- }
8087
- return {
8088
- ok: false,
8089
- message: `the import pass could not be run: ${error instanceof Error ? error.message : String(error)}`,
8090
- messages: []
8091
- };
8141
+ ran = await runEngine(godotPath, [
8142
+ "--headless",
8143
+ "--log-file",
8144
+ join7(logDir, "engine.log"),
8145
+ "--path",
8146
+ projectPath,
8147
+ "--import"
8148
+ ]);
8092
8149
  } finally {
8093
8150
  discard(logDir);
8094
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
+ };
8095
8161
  }
8096
8162
  async function runOperation(engine, operation, params, projectPath) {
8097
8163
  const paramsDir = scratchDirectory("params");
8098
8164
  const paramsFile = join7(paramsDir, `${operation}.json`);
8165
+ const answerFile = join7(paramsDir, "answer.json");
8099
8166
  writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
8100
8167
  const args = [
8101
8168
  "--headless",
@@ -8107,50 +8174,73 @@ async function runOperation(engine, operation, params, projectPath) {
8107
8174
  engine.script,
8108
8175
  operation,
8109
8176
  `@file:${paramsFile}`,
8177
+ answerFile,
8110
8178
  ...engine.debug ? ["--debug-godot"] : []
8111
8179
  ];
8112
- let stdout;
8113
- let stderr;
8180
+ let ran;
8181
+ let answer;
8114
8182
  try {
8115
- ({ stdout, stderr } = await run3(engine.godotPath, args, { signal: callSignal() }));
8116
- } catch (error) {
8117
- if (error instanceof Error && "stdout" in error && "stderr" in error) {
8118
- const failed = error;
8119
- return {
8120
- ok: false,
8121
- message: `${operation} failed (exit ${failed.code ?? "unknown"}): ${reason(failed.stdout, failed.stderr)}`,
8122
- messages: problems(failed.stderr)
8123
- };
8124
- }
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") {
8125
8190
  return {
8126
8191
  ok: false,
8127
- message: `${operation} could not be run: ${error instanceof Error ? error.message : String(error)}`,
8128
- messages: []
8192
+ message: `${operation} wrote an answer that could not be read (${answer.why})`,
8193
+ ...reported(problems)
8194
+ };
8195
+ }
8196
+ if (answer.kind === "none") {
8197
+ const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
8198
+ return {
8199
+ ok: false,
8200
+ message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
8201
+ ...reported(problems)
8129
8202
  };
8130
- } finally {
8131
- discard(paramsDir);
8132
8203
  }
8133
- const payload = lastJsonObject(stdout);
8134
- if (payload === null) {
8204
+ const fault = scriptFault(problems, engine.script);
8205
+ if (fault !== undefined) {
8135
8206
  return {
8136
8207
  ok: false,
8137
- message: `${operation} produced no result: ${reason(stdout, stderr)}`,
8138
- messages: problems(stderr)
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)
8139
8210
  };
8140
8211
  }
8141
- return { ok: true, payload, messages: problems(stderr) };
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
+ };
8142
8219
  }
8143
- 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;
8144
8236
  var init_headless = __esm(() => {
8145
- init_call_signal();
8146
- init_game_log();
8237
+ init_engine_run();
8147
8238
  init_reimport();
8148
8239
  init_scratch();
8149
- run3 = promisify3(execFile3);
8150
8240
  });
8151
8241
 
8152
8242
  // src/update-check.ts
8153
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
8243
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
8154
8244
  import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
8155
8245
  import { join as join8 } from "node:path";
8156
8246
  function packageUrl(root) {
@@ -8192,10 +8282,10 @@ function cacheFile(environment = process.env) {
8192
8282
  }
8193
8283
  function readCache(path) {
8194
8284
  try {
8195
- if (!existsSync7(path)) {
8285
+ if (!existsSync8(path)) {
8196
8286
  return null;
8197
8287
  }
8198
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
8288
+ const parsed = JSON.parse(readFileSync5(path, "utf8"));
8199
8289
  if (typeof parsed !== "object" || parsed === null) {
8200
8290
  return null;
8201
8291
  }
@@ -8343,7 +8433,7 @@ var init_update_check = __esm(() => {
8343
8433
  });
8344
8434
 
8345
8435
  // src/server-version.ts
8346
- import { readFileSync as readFileSync5 } from "node:fs";
8436
+ import { readFileSync as readFileSync6 } from "node:fs";
8347
8437
  function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
8348
8438
  if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
8349
8439
  return addonDigest !== shippedDigest;
@@ -8385,7 +8475,7 @@ var init_server_version = __esm(() => {
8385
8475
  GODOT_DEBUG_MODE_DEFAULT = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE;
8386
8476
  SERVER_VERSION = (() => {
8387
8477
  try {
8388
- const pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
8478
+ const pkg = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
8389
8479
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
8390
8480
  } catch {
8391
8481
  return "0.0.0";
@@ -8519,8 +8609,11 @@ var init_issues = __esm(() => {
8519
8609
  });
8520
8610
 
8521
8611
  // src/class-cache.ts
8522
- import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
8612
+ import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync3 } from "node:fs";
8523
8613
  import { join as join9 } from "node:path";
8614
+ function isToolScript(source) {
8615
+ return TOOL.test(codeOf(source));
8616
+ }
8524
8617
  function declaredClasses(projectPath) {
8525
8618
  const declared = new Map;
8526
8619
  eachScript(projectPath, (script, source) => {
@@ -8533,7 +8626,7 @@ function declaredClasses(projectPath) {
8533
8626
  }
8534
8627
  function eachScript(projectPath, each) {
8535
8628
  const visit = (directory, prefix) => {
8536
- if (existsSync8(join9(directory, ".gdignore"))) {
8629
+ if (existsSync9(join9(directory, ".gdignore"))) {
8537
8630
  return;
8538
8631
  }
8539
8632
  for (const entry of readdirSync3(directory, { withFileTypes: true })) {
@@ -8544,7 +8637,7 @@ function eachScript(projectPath, each) {
8544
8637
  if (entry.isDirectory()) {
8545
8638
  visit(path, `${prefix}${entry.name}/`);
8546
8639
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
8547
- each(`res://${prefix}${entry.name}`, readFileSync6(path, "utf8"));
8640
+ each(`res://${prefix}${entry.name}`, readFileSync7(path, "utf8"));
8548
8641
  }
8549
8642
  }
8550
8643
  };
@@ -8560,7 +8653,7 @@ function scriptsNaming(projectPath, classes) {
8560
8653
  }
8561
8654
  const code = codeOf(source);
8562
8655
  if (classesNamedIn(code, named).length > 0) {
8563
- naming.push({ script, tool: /^\s*@tool\b/m.test(code) });
8656
+ naming.push({ script, tool: TOOL.test(code) });
8564
8657
  }
8565
8658
  });
8566
8659
  return naming.sort((a, b) => a.script.localeCompare(b.script));
@@ -8594,11 +8687,11 @@ function codeOf(source) {
8594
8687
  }
8595
8688
  function cachedClasses(projectPath) {
8596
8689
  const cache = join9(projectPath, ".godot", "global_script_class_cache.cfg");
8597
- if (!existsSync8(cache)) {
8690
+ if (!existsSync9(cache)) {
8598
8691
  return null;
8599
8692
  }
8600
8693
  const listed = new Map;
8601
- const text = readFileSync6(cache, "utf8");
8694
+ const text = readFileSync7(cache, "utf8");
8602
8695
  for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
8603
8696
  listed.set(entry[1] ?? "", entry[2] ?? "");
8604
8697
  }
@@ -8767,6 +8860,19 @@ function classRemedy(entries) {
8767
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.";
8768
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.";
8769
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
+ }
8770
8876
  function failedReloadNote(failed) {
8771
8877
  const missing = (before, after) => before.filter((name) => !after.includes(name));
8772
8878
  const lost = [
@@ -8775,7 +8881,17 @@ function failedReloadNote(failed) {
8775
8881
  ];
8776
8882
  const unchanged = lost.length === 0 && missing(failed.heldAfter, failed.heldBefore).length === 0 && missing(failed.constantsAfter, failed.constantsBefore).length === 0;
8777
8883
  const answered = failed.codeName === "" ? `error ${failed.code}` : `error ${failed.code}, ${failed.codeName}`;
8778
- const copy = unchanged ? "Nothing was reloaded: the copy the editor holds is as it was, with the members under heldBeforeReload and heldConstantsBeforeReload." : `The copy the editor holds changed even so${lost.length === 0 ? "" : `, and has lost ${lost.join(", ")}`}: anything diagnosed against it now is answered from what is left, and reloading it once it compiles rebuilds it.`;
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
+ }
8779
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.`;
8780
8896
  return `${failed.script} did not compile: Godot answered ${answered}. ${copy} ` + `script_diagnostics on it gives the errors.${order}`;
8781
8897
  }
@@ -8808,7 +8924,7 @@ function declaredSince(projectPath, paths, names, since) {
8808
8924
  });
8809
8925
  }
8810
8926
  function cachedAtMissingPaths(cached, projectPath) {
8811
- return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync8(join9(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();
8812
8928
  }
8813
8929
  function staleAgainst(cached, projectPath) {
8814
8930
  return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
@@ -8825,9 +8941,10 @@ function unseenByEditor(projectPath, editorHolds) {
8825
8941
  const held = new Set(editorHolds);
8826
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);
8827
8943
  }
8828
- var DECLARATION;
8944
+ var DECLARATION, TOOL;
8829
8945
  var init_class_cache = __esm(() => {
8830
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;
8831
8948
  });
8832
8949
 
8833
8950
  // node_modules/zod/v4/core/util.js
@@ -15934,11 +16051,11 @@ var init_types = __esm(() => {
15934
16051
  });
15935
16052
 
15936
16053
  // src/paths.ts
15937
- import { realpathSync } from "node:fs";
16054
+ import { realpathSync as realpathSync2 } from "node:fs";
15938
16055
  import { isAbsolute, relative, resolve, sep } from "node:path";
15939
16056
  function realPathOr(path) {
15940
16057
  try {
15941
- return realpathSync.native(path);
16058
+ return realpathSync2.native(path);
15942
16059
  } catch {
15943
16060
  return path;
15944
16061
  }
@@ -15959,6 +16076,12 @@ function resolveWithinProject(projectPath, candidatePath) {
15959
16076
  return { ok: false, reason: "Path contains a null byte." };
15960
16077
  }
15961
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
+ }
15962
16085
  const scheme = OTHER_SCHEME.exec(withoutScheme);
15963
16086
  if (scheme) {
15964
16087
  return {
@@ -15995,7 +16118,7 @@ var init_paths = __esm(() => {
15995
16118
  });
15996
16119
 
15997
16120
  // src/resources.ts
15998
- import { readFileSync as readFileSync7 } from "node:fs";
16121
+ import { readFileSync as readFileSync8 } from "node:fs";
15999
16122
  import { extname, resolve as resolve2 } from "node:path";
16000
16123
  function ensureProjectPath(getProjectPath) {
16001
16124
  const projectPath = getProjectPath();
@@ -16161,7 +16284,7 @@ function readResourceText(uri, getProjectPath) {
16161
16284
  const parsedUri = parseGodotUri(uri);
16162
16285
  if (parsedUri.kind === "project-info") {
16163
16286
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
16164
- const rawProject = readFileSync7(projectFilePath, "utf-8");
16287
+ const rawProject = readFileSync8(projectFilePath, "utf-8");
16165
16288
  const parsedProject = parseProjectGodot(rawProject);
16166
16289
  return {
16167
16290
  mimeType: "application/json",
@@ -16170,7 +16293,7 @@ function readResourceText(uri, getProjectPath) {
16170
16293
  }
16171
16294
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
16172
16295
  ensureAllowedExtension(parsedUri.kind, filePath);
16173
- const text = readFileSync7(filePath, "utf-8");
16296
+ const text = readFileSync8(filePath, "utf-8");
16174
16297
  return {
16175
16298
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
16176
16299
  text
@@ -16302,16 +16425,16 @@ import { spawnSync } from "node:child_process";
16302
16425
  import { createHash as createHash3 } from "node:crypto";
16303
16426
  import {
16304
16427
  cpSync,
16305
- existsSync as existsSync9,
16428
+ existsSync as existsSync10,
16306
16429
  mkdirSync as mkdirSync4,
16307
16430
  readdirSync as readdirSync4,
16308
- readFileSync as readFileSync8,
16431
+ readFileSync as readFileSync9,
16309
16432
  rmdirSync,
16310
16433
  rmSync as rmSync5,
16311
16434
  statSync as statSync4,
16312
16435
  writeFileSync as writeFileSync6
16313
16436
  } from "node:fs";
16314
- import { dirname as dirname4, join as join10 } from "node:path";
16437
+ import { dirname as dirname5, join as join10 } from "node:path";
16315
16438
  import { fileURLToPath } from "node:url";
16316
16439
  function editorAddonDigest(from = shippedAddonsDirectory()) {
16317
16440
  const hash = createHash3("sha256");
@@ -16327,7 +16450,7 @@ function editorAddonDigest(from = shippedAddonsDirectory()) {
16327
16450
  } else {
16328
16451
  hash.update(`${inside}
16329
16452
  `);
16330
- hash.update(readFileSync8(path));
16453
+ hash.update(readFileSync9(path));
16331
16454
  hash.update(`
16332
16455
  `);
16333
16456
  }
@@ -16350,29 +16473,29 @@ function shippedEditorDigest() {
16350
16473
  }
16351
16474
  function installedEditorDigest(projectPath) {
16352
16475
  const marker = join10(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
16353
- if (!existsSync9(marker)) {
16476
+ if (!existsSync10(marker)) {
16354
16477
  return null;
16355
16478
  }
16356
- const said = readFileSync8(marker, "utf8").trim();
16479
+ const said = readFileSync9(marker, "utf8").trim();
16357
16480
  return said === "" ? null : said;
16358
16481
  }
16359
16482
  function shippedAddonsDirectory() {
16360
- return join10(dirname4(fileURLToPath(import.meta.url)), "godot", "addons");
16483
+ return join10(dirname5(fileURLToPath(import.meta.url)), "godot", "addons");
16361
16484
  }
16362
16485
  function shippedOperationsScript() {
16363
- return join10(dirname4(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
16486
+ return join10(dirname5(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
16364
16487
  }
16365
16488
  function installAddons(projectPath, from = shippedAddonsDirectory()) {
16366
16489
  const installed = [];
16367
16490
  for (const name of ADDONS) {
16368
16491
  const source = join10(from, name);
16369
- if (!existsSync9(join10(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
16492
+ if (!existsSync10(join10(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
16370
16493
  throw new Error(`The package holds no ${name} addon at ${source}.`);
16371
16494
  }
16372
16495
  const target = join10(projectPath, "addons", name);
16373
- const replaced = existsSync9(target);
16496
+ const replaced = existsSync10(target);
16374
16497
  rmSync5(target, { recursive: true, force: true });
16375
- mkdirSync4(dirname4(target), { recursive: true });
16498
+ mkdirSync4(dirname5(target), { recursive: true });
16376
16499
  cpSync(source, target, { recursive: true });
16377
16500
  writeFileSync6(join10(target, VERSION_MARKER), `${SERVER_VERSION}
16378
16501
  `);
@@ -16384,23 +16507,23 @@ function installAddons(projectPath, from = shippedAddonsDirectory()) {
16384
16507
  }
16385
16508
  function installedAddonVersion(projectPath) {
16386
16509
  const marker = join10(projectPath, "addons", ADDONS[0], VERSION_MARKER);
16387
- if (!existsSync9(marker)) {
16510
+ if (!existsSync10(marker)) {
16388
16511
  return null;
16389
16512
  }
16390
- const said = readFileSync8(marker, "utf8").trim();
16513
+ const said = readFileSync9(marker, "utf8").trim();
16391
16514
  return said === "" ? null : said;
16392
16515
  }
16393
16516
  function removeAddons(projectPath) {
16394
16517
  const removed = [];
16395
16518
  for (const name of ADDONS) {
16396
16519
  const target = join10(projectPath, "addons", name);
16397
- if (existsSync9(target)) {
16520
+ if (existsSync10(target)) {
16398
16521
  rmSync5(target, { recursive: true, force: true });
16399
16522
  removed.push(target);
16400
16523
  }
16401
16524
  }
16402
16525
  const addons = join10(projectPath, "addons");
16403
- if (removed.length > 0 && existsSync9(addons) && readdirSync4(addons).length === 0) {
16526
+ if (removed.length > 0 && existsSync10(addons) && readdirSync4(addons).length === 0) {
16404
16527
  rmdirSync(addons);
16405
16528
  removed.push(addons);
16406
16529
  }
@@ -16424,7 +16547,7 @@ function autoloadIsOurs(named) {
16424
16547
  return named === null || named === `res://${RUNTIME_AUTOLOAD.path}`;
16425
16548
  }
16426
16549
  async function setRuntime(engine, projectPath, on) {
16427
- 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);
16428
16551
  }
16429
16552
  function loaderAmong(projectPath, named) {
16430
16553
  let byName = null;
@@ -16434,7 +16557,7 @@ function loaderAmong(projectPath, named) {
16434
16557
  }
16435
16558
  let source = null;
16436
16559
  try {
16437
- source = readFileSync8(join10(projectPath, path), "utf8");
16560
+ source = readFileSync9(join10(projectPath, path), "utf8");
16438
16561
  } catch {}
16439
16562
  if (source?.includes(RUNTIME_AUTOLOAD.path)) {
16440
16563
  return { name: entry, path, exists: true };
@@ -16478,8 +16601,8 @@ function inspectProject(projectPath) {
16478
16601
  const addons = ADDONS.map((name) => {
16479
16602
  const directory = join10(projectPath, "addons", name);
16480
16603
  const marker = join10(directory, VERSION_MARKER);
16481
- const installed = existsSync9(directory);
16482
- const version = existsSync9(marker) ? readFileSync8(marker, "utf8").trim() : null;
16604
+ const installed = existsSync10(directory);
16605
+ const version = existsSync10(marker) ? readFileSync9(marker, "utf8").trim() : null;
16483
16606
  const current = version === SERVER_VERSION;
16484
16607
  if (!installed) {
16485
16608
  problems.push(`addons/${name} is not installed; run gdharness setup`);
@@ -16488,7 +16611,7 @@ function inspectProject(projectPath) {
16488
16611
  }
16489
16612
  return { name, installed, version, current };
16490
16613
  });
16491
- const settings = parseProjectGodot(readFileSync8(join10(projectPath, "project.godot"), "utf8"));
16614
+ const settings = parseProjectGodot(readFileSync9(join10(projectPath, "project.godot"), "utf8"));
16492
16615
  const pluginsEnabled = enabledPlugins(settings);
16493
16616
  for (const name of EDITOR_PLUGINS) {
16494
16617
  if (!pluginsEnabled.includes(name)) {
@@ -16504,7 +16627,7 @@ function inspectProject(projectPath) {
16504
16627
  const tracked = trackedByGit(projectPath, [...named.values()]);
16505
16628
  const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
16506
16629
  for (const [name, path] of named) {
16507
- if (!existsSync9(join10(projectPath, path))) {
16630
+ if (!existsSync10(join10(projectPath, path))) {
16508
16631
  if (!addonMissing(path)) {
16509
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`);
16510
16633
  }
@@ -16664,7 +16787,7 @@ var init_tool_definitions = __esm(() => {
16664
16787
  };
16665
16788
  PROPERTIES = {
16666
16789
  type: "object",
16667
- 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.',
16668
16791
  additionalProperties: true
16669
16792
  };
16670
16793
  XY = {
@@ -16819,11 +16942,18 @@ var init_tool_definitions = __esm(() => {
16819
16942
  },
16820
16943
  value: {
16821
16944
  blank: true,
16822
- 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.'
16823
16946
  },
16824
16947
  name: { type: "string", description: "Autoload name." },
16825
16948
  path: { type: "string", description: "Autoload script or scene inside the project." },
16826
- 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
+ },
16827
16957
  scenePath: SCENE_PATH,
16828
16958
  actionName: { type: "string", description: 'Input action name, such as "jump".' },
16829
16959
  events: INPUT_EVENTS,
@@ -16835,9 +16965,15 @@ var init_tool_definitions = __esm(() => {
16835
16965
  },
16836
16966
  pluginName: { type: "string", description: "Folder name under addons/." },
16837
16967
  busName: { type: "string", description: "Audio bus name." },
16838
- 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
+ },
16839
16972
  busIndex: { type: "number", description: "Audio bus index." },
16840
- 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
+ },
16841
16977
  effectType: { type: "string", description: 'Effect class, such as "AudioEffectReverb".' },
16842
16978
  volumeDb: { type: "number", description: "Bus volume in decibels." }
16843
16979
  },
@@ -16856,7 +16992,7 @@ var init_tool_definitions = __esm(() => {
16856
16992
  disable_plugin: { summary: "disable an addon", requires: ["pluginName"] },
16857
16993
  add_audio_bus: { summary: "add an audio bus", requires: ["busName"] },
16858
16994
  set_audio_bus_effect: {
16859
- summary: "add or configure an effect on a bus",
16995
+ summary: "put an effect in a slot on a bus",
16860
16996
  requires: ["busIndex", "effectIndex", "effectType"]
16861
16997
  },
16862
16998
  set_audio_bus_volume: { summary: "set a bus volume", requires: ["busIndex", "volumeDb"] }
@@ -16895,7 +17031,11 @@ var init_tool_definitions = __esm(() => {
16895
17031
  type: "boolean",
16896
17032
  description: "forward: include the engine's own res://. resources. Default false."
16897
17033
  },
16898
- 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
+ }
16899
17039
  },
16900
17040
  requires: ["projectPath", "resourcePath"]
16901
17041
  },
@@ -16940,7 +17080,10 @@ var init_tool_definitions = __esm(() => {
16940
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",
16941
17081
  requires: []
16942
17082
  },
16943
- 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
+ },
16944
17087
  refresh_uids: {
16945
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",
16946
17089
  requires: []
@@ -17001,11 +17144,11 @@ var init_tool_definitions = __esm(() => {
17001
17144
  },
17002
17145
  {
17003
17146
  name: "scene_create",
17004
- 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.",
17005
17148
  parameters: {
17006
17149
  projectPath: PROJECT_PATH,
17007
17150
  scenePath: SCENE_PATH,
17008
- rootNodeType: { type: "string", description: "create: the root node class. Default Node2D." },
17151
+ rootNodeType: { type: "string", description: "create: the root node class. Default Node." },
17009
17152
  newPath: { type: "string", description: "save_as: where the copy goes." }
17010
17153
  },
17011
17154
  requires: ["projectPath", "scenePath"],
@@ -17029,7 +17172,7 @@ var init_tool_definitions = __esm(() => {
17029
17172
  },
17030
17173
  {
17031
17174
  name: "scene_node",
17032
- 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.",
17033
17176
  parameters: {
17034
17177
  projectPath: PROJECT_PATH,
17035
17178
  scenePath: SCENE_PATH,
@@ -17047,7 +17190,10 @@ var init_tool_definitions = __esm(() => {
17047
17190
  type: "boolean",
17048
17191
  description: "get: include properties still at their default. Default false."
17049
17192
  },
17050
- 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
+ },
17051
17197
  cells: TILEMAP_CELLS
17052
17198
  },
17053
17199
  requires: ["projectPath", "scenePath"],
@@ -17058,7 +17204,10 @@ var init_tool_definitions = __esm(() => {
17058
17204
  duplicate: { summary: "copy a node and its children", requires: ["nodePath", "newName"] },
17059
17205
  reparent: { summary: "move a node under another parent", requires: ["nodePath", "newParentPath"] },
17060
17206
  delete: { summary: "remove a node and its children", requires: ["nodePath"] },
17061
- 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
+ }
17062
17211
  }
17063
17212
  },
17064
17213
  {
@@ -17146,13 +17295,16 @@ var init_tool_definitions = __esm(() => {
17146
17295
  },
17147
17296
  {
17148
17297
  name: "script_edit",
17149
- 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.",
17150
17299
  parameters: {
17151
17300
  projectPath: PROJECT_PATH,
17152
17301
  scriptPath: SCRIPT_PATH,
17153
- className: { type: "string", description: "create: a class_name for the script." },
17154
- extends: { type: "string", description: "create: the base class. Default Node." },
17155
- 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
+ },
17156
17308
  template: {
17157
17309
  type: "string",
17158
17310
  enum: ["singleton", "state_machine", "component", "resource"],
@@ -17350,7 +17502,7 @@ var init_tool_definitions = __esm(() => {
17350
17502
  },
17351
17503
  {
17352
17504
  name: "editor_output",
17353
- 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.",
17354
17506
  parameters: {
17355
17507
  severity: {
17356
17508
  type: "string",
@@ -17414,7 +17566,7 @@ var init_tool_definitions = __esm(() => {
17414
17566
  },
17415
17567
  reloadScript: {
17416
17568
  type: "string",
17417
- 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."
17418
17570
  }
17419
17571
  },
17420
17572
  requires: ["projectPath"]
@@ -17560,7 +17712,7 @@ var init_tool_definitions = __esm(() => {
17560
17712
  },
17561
17713
  {
17562
17714
  name: "runtime_invoke",
17563
- 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.",
17564
17716
  parameters: {
17565
17717
  projectPath: RUNNING_PROJECT_PATH,
17566
17718
  pid: RUNNING_PID,
@@ -17572,12 +17724,12 @@ var init_tool_definitions = __esm(() => {
17572
17724
  property: {
17573
17725
  type: "string",
17574
17726
  ops: ["set"],
17575
- 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.'
17576
17728
  },
17577
17729
  value: {
17578
17730
  ops: ["set"],
17579
17731
  blank: true,
17580
- 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.`
17581
17733
  },
17582
17734
  method: {
17583
17735
  type: "string",
@@ -17587,7 +17739,7 @@ var init_tool_definitions = __esm(() => {
17587
17739
  args: {
17588
17740
  type: "array",
17589
17741
  ops: ["call"],
17590
- 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.`
17591
17743
  },
17592
17744
  timeoutMs: {
17593
17745
  type: "integer",
@@ -17665,7 +17817,7 @@ var init_tool_definitions = __esm(() => {
17665
17817
  says: {
17666
17818
  type: "string",
17667
17819
  ops: ["click"],
17668
- 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."
17669
17821
  },
17670
17822
  action: {
17671
17823
  type: "string",
@@ -17698,7 +17850,7 @@ var init_tool_definitions = __esm(() => {
17698
17850
  replace: {
17699
17851
  type: "boolean",
17700
17852
  ops: ["text"],
17701
- 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.'
17702
17854
  },
17703
17855
  index: {
17704
17856
  type: "number",
@@ -17742,11 +17894,11 @@ var init_tool_definitions = __esm(() => {
17742
17894
  requires: [],
17743
17895
  operations: {
17744
17896
  click: {
17745
- 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",
17746
17898
  requires: []
17747
17899
  },
17748
17900
  choose: {
17749
- 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",
17750
17902
  requires: ["nodePath"]
17751
17903
  },
17752
17904
  action: { summary: "press an action, or hold it", requires: ["action"] },
@@ -17755,7 +17907,7 @@ var init_tool_definitions = __esm(() => {
17755
17907
  requires: ["keycode"]
17756
17908
  },
17757
17909
  text: {
17758
- 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",
17759
17911
  requires: ["text"]
17760
17912
  },
17761
17913
  mouse_click: {
@@ -17843,7 +17995,7 @@ var init_tool_definitions = __esm(() => {
17843
17995
  },
17844
17996
  {
17845
17997
  name: "debug_control",
17846
- 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.",
17847
17999
  parameters: {},
17848
18000
  requires: [],
17849
18001
  operations: {
@@ -17854,12 +18006,12 @@ var init_tool_definitions = __esm(() => {
17854
18006
  },
17855
18007
  {
17856
18008
  name: "debug_state",
17857
- 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.",
17858
18010
  parameters: {
17859
18011
  frameId: {
17860
18012
  type: "number",
17861
18013
  ops: ["variables"],
17862
- 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."
17863
18015
  }
17864
18016
  },
17865
18017
  requires: [],
@@ -32626,10 +32778,10 @@ var init_stdio2 = __esm(() => {
32626
32778
  });
32627
32779
 
32628
32780
  // src/runtime-client.ts
32629
- import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync5, unlinkSync } from "node:fs";
32781
+ import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync11, statSync as statSync5, unlinkSync } from "node:fs";
32630
32782
  import { createConnection } from "node:net";
32631
32783
  import { tmpdir as tmpdir3 } from "node:os";
32632
- import { dirname as dirname7, join as join13, resolve as resolve3 } from "node:path";
32784
+ import { dirname as dirname8, join as join13, resolve as resolve3 } from "node:path";
32633
32785
  import { setTimeout as delay } from "node:timers/promises";
32634
32786
  function runtimeDirectory(variables = process.env) {
32635
32787
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
@@ -32650,7 +32802,7 @@ function runtimeDirectories(variables = process.env) {
32650
32802
  function errorReportOf(pid, directories = runtimeDirectories()) {
32651
32803
  for (const directory of directories) {
32652
32804
  const path = join13(directory, `runtime-${pid}.log`);
32653
- if (existsSync11(path)) {
32805
+ if (existsSync12(path)) {
32654
32806
  return path;
32655
32807
  }
32656
32808
  }
@@ -32681,7 +32833,7 @@ function reportAlreadyInTranscript(transcript, report) {
32681
32833
  function parseAnnouncement(file, pid) {
32682
32834
  let fields;
32683
32835
  try {
32684
- fields = asParams(JSON.parse(readFileSync10(file, "utf8")));
32836
+ fields = asParams(JSON.parse(readFileSync11(file, "utf8")));
32685
32837
  } catch {
32686
32838
  return { kind: "rubbish" };
32687
32839
  }
@@ -32739,7 +32891,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
32739
32891
  function wentAway(file, pid) {
32740
32892
  let project = "";
32741
32893
  try {
32742
- const fields = asParams(JSON.parse(readFileSync10(file, "utf8")));
32894
+ const fields = asParams(JSON.parse(readFileSync11(file, "utf8")));
32743
32895
  project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
32744
32896
  } catch {
32745
32897
  return;
@@ -32782,7 +32934,7 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
32782
32934
  }).map((one) => one.file));
32783
32935
  }
32784
32936
  function announcedIn(directory) {
32785
- if (!existsSync11(directory)) {
32937
+ if (!existsSync12(directory)) {
32786
32938
  return [];
32787
32939
  }
32788
32940
  const found = [];
@@ -32828,14 +32980,14 @@ function announcedIn(directory) {
32828
32980
  const held = new Set(entries.map((one) => one.file));
32829
32981
  const here = join13(directory, ".");
32830
32982
  for (const file of verdicts.keys()) {
32831
- if (dirname7(file) === here && !held.has(file)) {
32983
+ if (dirname8(file) === here && !held.has(file)) {
32832
32984
  verdicts.delete(file);
32833
32985
  }
32834
32986
  }
32835
32987
  return found;
32836
32988
  }
32837
32989
  async function announcementEnded(pid, directories = runtimeDirectories()) {
32838
- const files = directories.map((directory) => join13(directory, `runtime-${pid}.json`)).filter((file) => existsSync11(file));
32990
+ const files = directories.map((directory) => join13(directory, `runtime-${pid}.json`)).filter((file) => existsSync12(file));
32839
32991
  const first = files[0];
32840
32992
  if (first === undefined) {
32841
32993
  return;
@@ -32870,13 +33022,13 @@ async function announcedSince(projectPath, before, waiting = {}) {
32870
33022
  const directories = waiting.directories ?? runtimeDirectories();
32871
33023
  const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
32872
33024
  for (;; ) {
32873
- const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && isSameDirectory(endpoint.project.path, projectPath) && (waiting.accept?.(endpoint) ?? true));
32874
- 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) {
32875
33027
  return fresh;
32876
33028
  }
32877
33029
  const left = until - Date.now();
32878
33030
  if (left <= 0 || waiting.giveUp?.() === true) {
32879
- return null;
33031
+ return [];
32880
33032
  }
32881
33033
  await delay(Math.min(LOOK_EVERY_MS, left));
32882
33034
  }
@@ -33135,14 +33287,14 @@ var init_runtime_client = __esm(() => {
33135
33287
  });
33136
33288
 
33137
33289
  // src/boot-note.ts
33138
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "node:fs";
33139
- import { dirname as dirname8, join as join14 } 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";
33140
33292
  function bootNotePath(projectPath) {
33141
33293
  return join14(projectPath, ".godot", "gdharness-boot.json");
33142
33294
  }
33143
33295
  function readBootNote(projectPath) {
33144
33296
  try {
33145
- const noted = readNumber(asParams(JSON.parse(readFileSync11(bootNotePath(projectPath), "utf8"))), "announcedAfterMs");
33297
+ const noted = readNumber(asParams(JSON.parse(readFileSync12(bootNotePath(projectPath), "utf8"))), "announcedAfterMs");
33146
33298
  return noted !== undefined && Number.isFinite(noted) && noted > 0 ? noted : null;
33147
33299
  } catch {
33148
33300
  return null;
@@ -33151,7 +33303,7 @@ function readBootNote(projectPath) {
33151
33303
  function writeBootNote(projectPath, announcedAfterMs) {
33152
33304
  try {
33153
33305
  const path = bootNotePath(projectPath);
33154
- mkdirSync6(dirname8(path), { recursive: true });
33306
+ mkdirSync6(dirname9(path), { recursive: true });
33155
33307
  writeFileSync8(path, JSON.stringify({ announcedAfterMs: Math.round(announcedAfterMs), at: new Date().toISOString() }, null, 2), "utf8");
33156
33308
  } catch {}
33157
33309
  }
@@ -33170,8 +33322,8 @@ var init_boot_note = __esm(() => {
33170
33322
  });
33171
33323
 
33172
33324
  // src/breakpoint-note.ts
33173
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
33174
- import { dirname as dirname9, isAbsolute as isAbsolute2, join as join15, 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";
33175
33327
  function breakpointNotePath(projectPath) {
33176
33328
  return join15(projectPath, ".godot", "gdharness-breakpoints.json");
33177
33329
  }
@@ -33190,7 +33342,7 @@ function writeBreakpointNote(projectPath, held) {
33190
33342
  lines: [...one.lines]
33191
33343
  }))
33192
33344
  };
33193
- mkdirSync7(dirname9(path), { recursive: true });
33345
+ mkdirSync7(dirname10(path), { recursive: true });
33194
33346
  writeFileSync9(path, `${JSON.stringify(note, null, 2)}
33195
33347
  `, "utf8");
33196
33348
  } catch (error) {
@@ -33199,7 +33351,7 @@ function writeBreakpointNote(projectPath, held) {
33199
33351
  }
33200
33352
  function readBreakpointNote(projectPath) {
33201
33353
  try {
33202
- const parsed = JSON.parse(readFileSync12(breakpointNotePath(projectPath), "utf8"));
33354
+ const parsed = JSON.parse(readFileSync13(breakpointNotePath(projectPath), "utf8"));
33203
33355
  if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
33204
33356
  return [];
33205
33357
  }
@@ -33232,14 +33384,14 @@ var init_breakpoint_note = __esm(() => {
33232
33384
  });
33233
33385
 
33234
33386
  // src/class-note.ts
33235
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "node:fs";
33236
- import { dirname as dirname10, join as join16 } 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";
33237
33389
  function classNotePath(projectPath) {
33238
33390
  return join16(projectPath, ".godot", "gdharness-classes.json");
33239
33391
  }
33240
33392
  function readClassNote(projectPath) {
33241
33393
  try {
33242
- const fields = asParams(JSON.parse(readFileSync13(classNotePath(projectPath), "utf8")));
33394
+ const fields = asParams(JSON.parse(readFileSync14(classNotePath(projectPath), "utf8")));
33243
33395
  const rebuiltWith = readArray(fields, "rebuiltWith");
33244
33396
  if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
33245
33397
  return null;
@@ -33256,7 +33408,7 @@ function readClassNote(projectPath) {
33256
33408
  function writeClassNote(projectPath, note) {
33257
33409
  try {
33258
33410
  const path = classNotePath(projectPath);
33259
- mkdirSync8(dirname10(path), { recursive: true });
33411
+ mkdirSync8(dirname11(path), { recursive: true });
33260
33412
  writeFileSync10(path, `${JSON.stringify(note, null, 2)}
33261
33413
  `, "utf8");
33262
33414
  } catch {}
@@ -33264,16 +33416,16 @@ function writeClassNote(projectPath, note) {
33264
33416
  var init_class_note = () => {};
33265
33417
 
33266
33418
  // src/config-pin.ts
33267
- import { existsSync as existsSync12, readFileSync as readFileSync14 } from "node:fs";
33419
+ import { existsSync as existsSync13, readFileSync as readFileSync15 } from "node:fs";
33268
33420
  import { join as join17 } from "node:path";
33269
33421
  function versionsNamedInConfig(projectPath) {
33270
33422
  const config = join17(projectPath, ".mcp.json");
33271
- if (!existsSync12(config)) {
33423
+ if (!existsSync13(config)) {
33272
33424
  return [];
33273
33425
  }
33274
33426
  let text;
33275
33427
  try {
33276
- text = readFileSync14(config, "utf8");
33428
+ text = readFileSync15(config, "utf8");
33277
33429
  JSON.parse(text);
33278
33430
  } catch {
33279
33431
  return [];
@@ -33422,6 +33574,9 @@ var init_ports = __esm(() => {
33422
33574
  // src/dap_client.ts
33423
33575
  import { createConnection as createConnection2 } from "node:net";
33424
33576
  import { setTimeout as delay2 } from "node:timers/promises";
33577
+ function fileKey(path) {
33578
+ return process.platform === "win32" ? path.replaceAll("\\", "/").toLowerCase() : path;
33579
+ }
33425
33580
  function said(body, field) {
33426
33581
  const value = body?.[field];
33427
33582
  return typeof value === "string" ? value : "";
@@ -33438,17 +33593,25 @@ class GodotDAPClient {
33438
33593
  outputBuffer = [];
33439
33594
  onOutput = null;
33440
33595
  maxOutputLines = 1000;
33596
+ outputDropped = 0;
33597
+ outputTaken = 0;
33441
33598
  initialized = false;
33442
33599
  attached = false;
33443
33600
  lastThreadId = 1;
33444
33601
  halt = null;
33445
33602
  holdKnown = false;
33603
+ stops = 0;
33446
33604
  breakpoints = new Map;
33605
+ spelled = new Map;
33447
33606
  inEditor = new Map;
33448
33607
  onBreakpointsChanged = null;
33449
- 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 = {}) {
33450
33611
  this.port = port;
33451
33612
  this.host = host;
33613
+ this.scopesPatienceMs = patience.scopesMs ?? SCOPES_TIMEOUT_MS;
33614
+ this.stepPatienceMs = patience.stepMs ?? STEP_LANDS_WITHIN_MS;
33452
33615
  this.pendingRequests = new Map;
33453
33616
  }
33454
33617
  async connect() {
@@ -33609,6 +33772,7 @@ class GodotDAPClient {
33609
33772
  }
33610
33773
  }
33611
33774
  if (this.outputBuffer.length > this.maxOutputLines) {
33775
+ this.outputDropped += this.outputBuffer.length - this.maxOutputLines;
33612
33776
  this.outputBuffer = this.outputBuffer.slice(this.outputBuffer.length - this.maxOutputLines);
33613
33777
  }
33614
33778
  return;
@@ -33625,6 +33789,7 @@ class GodotDAPClient {
33625
33789
  text: said(body, "text")
33626
33790
  };
33627
33791
  this.holdKnown = true;
33792
+ this.stops += 1;
33628
33793
  return;
33629
33794
  }
33630
33795
  if (eventName === "continued") {
@@ -33641,20 +33806,24 @@ class GodotDAPClient {
33641
33806
  if (typeof path !== "string" || typeof line !== "number") {
33642
33807
  return;
33643
33808
  }
33809
+ const key = fileKey(path);
33810
+ if (!this.spelled.has(key)) {
33811
+ this.spelled.set(key, path);
33812
+ }
33644
33813
  if (said(body, "reason") === "removed") {
33645
- this.inEditor.get(path)?.delete(line);
33646
- const held = this.breakpoints.get(path);
33814
+ this.inEditor.get(key)?.delete(line);
33815
+ const held = this.breakpoints.get(key);
33647
33816
  if (held?.delete(line) === true) {
33648
33817
  if (held.size === 0) {
33649
- this.breakpoints.delete(path);
33818
+ this.breakpoints.delete(key);
33650
33819
  }
33651
33820
  this.onBreakpointsChanged?.(this.breakpointsHeld());
33652
33821
  }
33653
33822
  return;
33654
33823
  }
33655
- const known = this.inEditor.get(path) ?? new Set;
33824
+ const known = this.inEditor.get(key) ?? new Set;
33656
33825
  known.add(line);
33657
- this.inEditor.set(path, known);
33826
+ this.inEditor.set(key, known);
33658
33827
  return;
33659
33828
  }
33660
33829
  if (eventName === "terminated" || eventName === "exited") {
@@ -33737,24 +33906,44 @@ class GodotDAPClient {
33737
33906
  getOutput(clear = false) {
33738
33907
  const lines = [...this.outputBuffer];
33739
33908
  if (clear) {
33909
+ this.outputTaken += lines.length;
33740
33910
  this.outputBuffer = [];
33741
33911
  }
33742
33912
  return lines;
33743
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
+ }
33744
33931
  async setBreakpoint(filePath, line) {
33745
- const wanted = new Set(this.breakpoints.get(filePath) ?? []);
33932
+ const wanted = new Set(this.breakpoints.get(fileKey(filePath)) ?? []);
33746
33933
  wanted.add(line);
33747
33934
  return await this.sendBreakpoints(filePath, wanted);
33748
33935
  }
33749
33936
  async removeBreakpoint(filePath, line) {
33750
- const wanted = new Set(this.breakpoints.get(filePath) ?? []);
33937
+ const wanted = new Set(this.breakpoints.get(fileKey(filePath)) ?? []);
33751
33938
  wanted.delete(line);
33752
33939
  return await this.sendBreakpoints(filePath, wanted, line);
33753
33940
  }
33754
33941
  async sendBreakpoints(filePath, mine, clearing) {
33755
33942
  await this.ensureConnected();
33756
33943
  await this.initialize();
33757
- 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) ?? []]);
33758
33947
  if (clearing !== undefined) {
33759
33948
  lines.delete(clearing);
33760
33949
  }
@@ -33764,14 +33953,14 @@ class GodotDAPClient {
33764
33953
  breakpoints: sorted.map((breakpointLine) => ({ line: breakpointLine }))
33765
33954
  });
33766
33955
  if (mine.size === 0) {
33767
- this.breakpoints.delete(filePath);
33956
+ this.breakpoints.delete(key);
33768
33957
  } else {
33769
- this.breakpoints.set(filePath, new Set(mine));
33958
+ this.breakpoints.set(key, new Set(mine));
33770
33959
  }
33771
33960
  if (lines.size === 0) {
33772
- this.inEditor.delete(filePath);
33961
+ this.inEditor.delete(key);
33773
33962
  } else {
33774
- this.inEditor.set(filePath, lines);
33963
+ this.inEditor.set(key, lines);
33775
33964
  }
33776
33965
  return answer;
33777
33966
  }
@@ -33780,11 +33969,11 @@ class GodotDAPClient {
33780
33969
  }
33781
33970
  breakpointsInEditor() {
33782
33971
  const theirs = [];
33783
- for (const [scriptPath, lines] of this.inEditor) {
33784
- const mine = this.breakpoints.get(scriptPath);
33972
+ for (const [key, lines] of this.inEditor) {
33973
+ const mine = this.breakpoints.get(key);
33785
33974
  const rest = Array.from(lines).filter((line) => !mine?.has(line)).sort((a, b) => a - b);
33786
33975
  if (rest.length > 0) {
33787
- theirs.push({ scriptPath, lines: rest });
33976
+ theirs.push({ scriptPath: this.spelled.get(key) ?? key, lines: rest });
33788
33977
  }
33789
33978
  }
33790
33979
  return theirs;
@@ -33792,12 +33981,13 @@ class GodotDAPClient {
33792
33981
  async reapplyBreakpoints() {
33793
33982
  const applied = [];
33794
33983
  const refused = [];
33795
- 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;
33796
33986
  try {
33797
33987
  await this.sendBreakpoints(filePath, lines);
33798
33988
  applied.push({ scriptPath: filePath, lines: Array.from(lines).sort((a, b) => a - b) });
33799
33989
  } catch (error) {
33800
- this.breakpoints.delete(filePath);
33990
+ this.breakpoints.delete(key);
33801
33991
  refused.push({
33802
33992
  scriptPath: filePath,
33803
33993
  reason: error instanceof Error ? error.message : String(error)
@@ -33810,19 +34000,21 @@ class GodotDAPClient {
33810
34000
  return { applied, refused };
33811
34001
  }
33812
34002
  breakpointsHeld() {
33813
- return Array.from(this.breakpoints, ([scriptPath, lines]) => ({
33814
- scriptPath,
34003
+ return Array.from(this.breakpoints, ([key, lines]) => ({
34004
+ scriptPath: this.spelled.get(key) ?? key,
33815
34005
  lines: Array.from(lines).sort((a, b) => a - b)
33816
34006
  }));
33817
34007
  }
33818
34008
  holdBreakpoints(held) {
33819
34009
  for (const { scriptPath, lines } of held) {
33820
- const fileBreakpoints = this.breakpoints.get(scriptPath) ?? new Set;
34010
+ const key = fileKey(scriptPath);
34011
+ const fileBreakpoints = this.breakpoints.get(key) ?? new Set;
33821
34012
  for (const line of lines) {
33822
34013
  fileBreakpoints.add(line);
33823
34014
  }
33824
34015
  if (fileBreakpoints.size > 0) {
33825
- this.breakpoints.set(scriptPath, fileBreakpoints);
34016
+ this.breakpoints.set(key, fileBreakpoints);
34017
+ this.spelled.set(key, scriptPath);
33826
34018
  }
33827
34019
  }
33828
34020
  }
@@ -33833,15 +34025,16 @@ class GodotDAPClient {
33833
34025
  this.halt = null;
33834
34026
  this.holdKnown = true;
33835
34027
  }
33836
- async stepOver(threadId) {
33837
- await this.attach();
33838
- const resolvedThreadId = await this.resolveThreadId(threadId);
33839
- await this.sendRequest("next", { threadId: resolvedThreadId });
33840
- }
33841
- async stepInto(threadId) {
34028
+ async step(command) {
33842
34029
  await this.attach();
33843
- const resolvedThreadId = await this.resolveThreadId(threadId);
33844
- 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;
33845
34038
  }
33846
34039
  async getStackTrace(threadId) {
33847
34040
  await this.attach();
@@ -33867,16 +34060,16 @@ class GodotDAPClient {
33867
34060
  } catch {}
33868
34061
  await delay2(200);
33869
34062
  }
33870
- return [];
34063
+ return null;
33871
34064
  }
33872
34065
  async getScopes(frameId) {
33873
34066
  await this.attach();
33874
34067
  const frames = await this.getStackTrace();
33875
34068
  const frame = frameId === undefined ? frames[0] : frames.find((each) => each["id"] === frameId);
33876
34069
  if (frame === undefined) {
33877
- 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`);
33878
34071
  }
33879
- const deadline = Date.now() + SCOPES_TIMEOUT_MS;
34072
+ const deadline = Date.now() + this.scopesPatienceMs;
33880
34073
  let scopes = [];
33881
34074
  while (scopes.length === 0 && Date.now() < deadline) {
33882
34075
  try {
@@ -33888,6 +34081,9 @@ class GodotDAPClient {
33888
34081
  } catch {}
33889
34082
  await delay2(200);
33890
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
+ }
33891
34087
  const named = [];
33892
34088
  for (const scope of scopes) {
33893
34089
  const reference = scope["variablesReference"];
@@ -33951,9 +34147,8 @@ async function handleDAPTool(client, toolName, args) {
33951
34147
  try {
33952
34148
  switch (toolName) {
33953
34149
  case "dap_get_output": {
33954
- const output = client.getOutput(false);
33955
34150
  return {
33956
- content: [{ type: "text", text: JSON.stringify({ lines: output.length, output }, null, 2) }]
34151
+ content: [{ type: "text", text: JSON.stringify(client.outputSoFar(), null, 2) }]
33957
34152
  };
33958
34153
  }
33959
34154
  case "dap_set_breakpoint": {
@@ -33980,19 +34175,13 @@ async function handleDAPTool(client, toolName, args) {
33980
34175
  }
33981
34176
  case "dap_step_over":
33982
34177
  case "dap_step_into": {
33983
- if (toolName === "dap_step_into") {
33984
- await client.stepInto();
33985
- } else {
33986
- await client.stepOver();
33987
- }
33988
- return {
33989
- content: [
33990
- {
33991
- type: "text",
33992
- text: JSON.stringify({ stepped: true, stack: await client.getStackTrace() }, null, 2)
33993
- }
33994
- ]
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.`
33995
34183
  };
34184
+ return { content: [{ type: "text", text: JSON.stringify(answer, null, 2) }] };
33996
34185
  }
33997
34186
  case "dap_get_stack_trace": {
33998
34187
  const stack = await client.getStackTrace();
@@ -34003,8 +34192,14 @@ async function handleDAPTool(client, toolName, args) {
34003
34192
  case "dap_get_variables": {
34004
34193
  const frameId = typeof safeArgs.frameId === "number" ? safeArgs.frameId : undefined;
34005
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
+ };
34006
34201
  return {
34007
- content: [{ type: "text", text: JSON.stringify({ scopes }, null, 2) }]
34202
+ content: [{ type: "text", text: JSON.stringify(answer, null, 2) }]
34008
34203
  };
34009
34204
  }
34010
34205
  default:
@@ -34012,18 +34207,22 @@ async function handleDAPTool(client, toolName, args) {
34012
34207
  }
34013
34208
  } catch (error) {
34014
34209
  const message = error instanceof Error ? error.message : String(error);
34015
- 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.";
34016
34211
  return {
34017
34212
  isError: true,
34018
- content: [{ type: "text", text: `DAP tool '${toolName}' failed: ${message}. ${cure}` }]
34213
+ content: [
34214
+ { type: "text", text: `DAP tool '${toolName}' failed: ${message}.${cure === "" ? "" : ` ${cure}`}` }
34215
+ ]
34019
34216
  };
34020
34217
  }
34021
34218
  }
34022
- 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;
34023
34220
  var init_dap_client = __esm(() => {
34024
34221
  init_errors();
34025
34222
  init_framing();
34026
34223
  init_ports();
34224
+ ReadRefusal = class ReadRefusal extends Refusal {
34225
+ };
34027
34226
  });
34028
34227
 
34029
34228
  // src/desktop.ts
@@ -34033,16 +34232,16 @@ var init_desktop = () => {};
34033
34232
  // src/editor-log.ts
34034
34233
  import {
34035
34234
  closeSync,
34036
- existsSync as existsSync13,
34235
+ existsSync as existsSync14,
34037
34236
  mkdirSync as mkdirSync9,
34038
34237
  openSync,
34039
- readFileSync as readFileSync15,
34238
+ readFileSync as readFileSync16,
34040
34239
  readSync,
34041
34240
  rmSync as rmSync8,
34042
34241
  statSync as statSync6,
34043
34242
  writeFileSync as writeFileSync11
34044
34243
  } from "node:fs";
34045
- import { dirname as dirname11, join as join18 } from "node:path";
34244
+ import { dirname as dirname12, join as join18 } from "node:path";
34046
34245
  function editorLogPath(projectPath) {
34047
34246
  return join18(projectPath, ".godot", "gdharness-editor.log");
34048
34247
  }
@@ -34051,7 +34250,7 @@ function editorLogNotePath(projectPath) {
34051
34250
  }
34052
34251
  function readEditorLogNote(projectPath) {
34053
34252
  try {
34054
- const fields = asParams(JSON.parse(readFileSync15(editorLogNotePath(projectPath), "utf8")));
34253
+ const fields = asParams(JSON.parse(readFileSync16(editorLogNotePath(projectPath), "utf8")));
34055
34254
  const pid = readNumber(fields, "pid");
34056
34255
  const startedAt = readString(fields, "startedAt");
34057
34256
  return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
@@ -34062,7 +34261,7 @@ function readEditorLogNote(projectPath) {
34062
34261
  function clearEditorLog(projectPath) {
34063
34262
  try {
34064
34263
  const path = editorLogPath(projectPath);
34065
- mkdirSync9(dirname11(path), { recursive: true });
34264
+ mkdirSync9(dirname12(path), { recursive: true });
34066
34265
  rmSync8(path, { force: true });
34067
34266
  rmSync8(editorLogNotePath(projectPath), { force: true });
34068
34267
  } catch {}
@@ -34070,7 +34269,7 @@ function clearEditorLog(projectPath) {
34070
34269
  function writeEditorLogNote(projectPath, pid) {
34071
34270
  try {
34072
34271
  const path = editorLogNotePath(projectPath);
34073
- mkdirSync9(dirname11(path), { recursive: true });
34272
+ mkdirSync9(dirname12(path), { recursive: true });
34074
34273
  writeFileSync11(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
34075
34274
  } catch {}
34076
34275
  }
@@ -34092,7 +34291,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
34092
34291
  return { kind: "older server", notedPid: noted?.pid ?? null };
34093
34292
  }
34094
34293
  const path = editorLogPath(projectPath);
34095
- if (!existsSync13(path)) {
34294
+ if (!existsSync14(path)) {
34096
34295
  return { kind: "not written" };
34097
34296
  }
34098
34297
  const log = new GameLog;
@@ -34104,7 +34303,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
34104
34303
  function theFirstOf(path) {
34105
34304
  const whole = statSync6(path).size;
34106
34305
  if (whole <= MOST_CONSOLE_BYTES) {
34107
- return { text: readFileSync15(path, "utf8") };
34306
+ return { text: readFileSync16(path, "utf8") };
34108
34307
  }
34109
34308
  const handle = openSync(path, "r");
34110
34309
  try {
@@ -37798,13 +37997,17 @@ function parseJUnit(xml) {
37798
37997
  const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
37799
37998
  const suites = suiteElements.map((suite) => {
37800
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;
37801
38002
  return {
37802
38003
  name: suite.attributes["name"] ?? "",
37803
38004
  path: pathOf(suite),
37804
38005
  tests: cases.length,
37805
38006
  discovered: count(suite.attributes, "tests"),
37806
- failures: cases.filter((entry) => entry.status === "failed").length,
37807
- 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),
37808
38011
  skipped: cases.filter((entry) => entry.status === "skipped").length,
37809
38012
  time: count(suite.attributes, "time"),
37810
38013
  cases
@@ -37815,11 +38018,19 @@ function parseJUnit(xml) {
37815
38018
  tests: all.length,
37816
38019
  failures: all.filter((entry) => entry.status === "failed").length,
37817
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),
37818
38023
  skipped: all.filter((entry) => entry.status === "skipped").length,
37819
38024
  time: suites.reduce((sum, suite) => sum + suite.time, 0),
37820
38025
  suites
37821
38026
  };
37822
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
+ }
37823
38034
  function whyNoReport(printed, asked) {
37824
38035
  const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
37825
38036
  if (missing !== undefined) {
@@ -37884,13 +38095,17 @@ function withActualsPrinted(failed, printed) {
37884
38095
  at: match[5] ?? "",
37885
38096
  ...readDiff(match[1] ?? "")
37886
38097
  }));
38098
+ const used = new Set;
37887
38099
  return failed.map((entry) => {
37888
38100
  if (entry.detail === null) {
37889
38101
  return entry;
37890
38102
  }
37891
38103
  let detail = entry.detail;
37892
38104
  let recovered = false;
37893
- for (const diff of diffs) {
38105
+ for (const [index, diff] of diffs.entries()) {
38106
+ if (used.has(index)) {
38107
+ continue;
38108
+ }
37894
38109
  const wrong = ` but was
37895
38110
  '${diff.merged.replaceAll("\r", "")}'${diff.suffix}
37896
38111
  at ${diff.at}`;
@@ -37899,6 +38114,7 @@ function withActualsPrinted(failed, printed) {
37899
38114
  '${diff.actual}'${diff.suffix}
37900
38115
  at ${diff.at}`);
37901
38116
  recovered = true;
38117
+ used.add(index);
37902
38118
  }
37903
38119
  }
37904
38120
  const quoted = QUOTED_EQUALITY.exec(detail);
@@ -37950,7 +38166,7 @@ function orphansPrinted(printed) {
37950
38166
  suites
37951
38167
  };
37952
38168
  }
37953
- 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";
37954
38170
  var init_junit = __esm(() => {
37955
38171
  MalformedReportError = class MalformedReportError extends Error {
37956
38172
  constructor(message) {
@@ -37960,8 +38176,9 @@ var init_junit = __esm(() => {
37960
38176
  };
37961
38177
  ENTITIES = { lt: "<", gt: ">", amp: "&", quot: '"', apos: "'" };
37962
38178
  ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
38179
+ HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
37963
38180
  NOTHING_RAN = /no test cases found/i;
37964
- 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;
37965
38182
  ESCAPE = String.fromCharCode(27);
37966
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");
37967
38184
  QUOTED_EQUALITY = new RegExp(`^Expecting(?: error message)?:\\n '([\\s\\S]*)'\\n but was\\n '([\\s\\S]*)'(?:${IGNORING_CASE})?\\n\\tat `);
@@ -37979,15 +38196,15 @@ var init_junit = __esm(() => {
37979
38196
  "<DEL>": ""
37980
38197
  };
37981
38198
  PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
37982
- RUNNING_SUITE = /Run Test Suite:\s*(\S+)/;
38199
+ RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
37983
38200
  ORPHANS = /(\d+)\s+orphans/;
37984
38201
  });
37985
38202
 
37986
38203
  // src/lsp_client.ts
37987
- import { realpathSync as realpathSync2 } from "node:fs";
38204
+ import { existsSync as existsSync15, realpathSync as realpathSync3 } from "node:fs";
37988
38205
  import { readFile, realpath } from "node:fs/promises";
37989
38206
  import { createConnection as createConnection3 } from "node:net";
37990
- import { dirname as dirname12, resolve as resolve4 } from "node:path";
38207
+ import { dirname as dirname13, join as join19, resolve as resolve4 } from "node:path";
37991
38208
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
37992
38209
  function diagnosticsKey(uri) {
37993
38210
  let decoded;
@@ -38000,7 +38217,7 @@ function diagnosticsKey(uri) {
38000
38217
  return decoded;
38001
38218
  }
38002
38219
  try {
38003
- return realpathSync2.native(fileURLToPath3(decoded)).toLowerCase();
38220
+ return realpathSync3.native(fileURLToPath3(decoded)).toLowerCase();
38004
38221
  } catch {
38005
38222
  return decoded.toLowerCase();
38006
38223
  }
@@ -38017,8 +38234,10 @@ class GodotLSPClient {
38017
38234
  connectPromise = null;
38018
38235
  initialized = false;
38019
38236
  rootPath = null;
38237
+ servedWorkspace = null;
38020
38238
  diagnosticsWaiters = new Map;
38021
38239
  documentVersions = new Map;
38240
+ documentTurns = new Map;
38022
38241
  constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1") {
38023
38242
  this.port = port;
38024
38243
  this.host = host;
@@ -38115,7 +38334,7 @@ class GodotLSPClient {
38115
38334
  if (this.initialized) {
38116
38335
  return;
38117
38336
  }
38118
- const rootPath = this.rootPath ?? dirname12(resolve4(filePath));
38337
+ const rootPath = this.rootPath ?? dirname13(resolve4(filePath));
38119
38338
  await this.initialize(rootPath);
38120
38339
  }
38121
38340
  async sendRequest(method, params) {
@@ -38190,6 +38409,10 @@ class GodotLSPClient {
38190
38409
  }
38191
38410
  }
38192
38411
  }
38412
+ if (message["method"] === "gdscript_client/changeWorkspace") {
38413
+ const path = message["params"]?.["path"];
38414
+ this.servedWorkspace = typeof path === "string" ? path : null;
38415
+ }
38193
38416
  if (message["method"] === "textDocument/publishDiagnostics") {
38194
38417
  const params = message["params"];
38195
38418
  const paramsObject = params && typeof params === "object" ? params : null;
@@ -38292,10 +38515,14 @@ class GodotLSPClient {
38292
38515
  this.sendNotification("textDocument/didClose", { textDocument: { uri } });
38293
38516
  } catch {}
38294
38517
  }
38518
+ forgetServedWorkspace() {
38519
+ this.servedWorkspace = null;
38520
+ }
38295
38521
  async initialize(rootPath) {
38296
38522
  await this.ensureConnected();
38297
38523
  const resolvedRootPath = resolve4(rootPath);
38298
38524
  const rootUri = pathToFileURL(resolvedRootPath).href;
38525
+ this.forgetServedWorkspace();
38299
38526
  const result = await this.sendRequest("initialize", {
38300
38527
  processId: process.pid,
38301
38528
  rootPath: resolvedRootPath,
@@ -38321,60 +38548,82 @@ class GodotLSPClient {
38321
38548
  }
38322
38549
  ]
38323
38550
  });
38551
+ const served = this.servedWorkspace;
38552
+ if (served !== null && !isSameDirectory(served, resolvedRootPath)) {
38553
+ throw new AnotherProjectsServer(this.port, served, resolvedRootPath);
38554
+ }
38324
38555
  this.sendNotification("initialized", {});
38325
38556
  this.initialized = true;
38326
38557
  this.rootPath = resolvedRootPath;
38327
38558
  return result;
38328
38559
  }
38329
- async getDiagnostics(filePath, content) {
38330
- await this.ensureConnected();
38331
- await this.ensureInitializedForFile(filePath);
38560
+ async withDocument(filePath, work) {
38332
38561
  const key = diagnosticsKey(this.toFileUri(filePath));
38333
- const diagnosticsPromise = new Promise((resolveDiagnostics, rejectDiagnostics) => {
38334
- const existing = this.diagnosticsWaiters.get(key);
38335
- if (existing) {
38336
- clearTimeout(existing.timer);
38337
- }
38338
- const timer = setTimeout(() => {
38339
- this.diagnosticsWaiters.delete(key);
38340
- 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."));
38341
- }, DIAGNOSTICS_TIMEOUT_MS);
38342
- this.diagnosticsWaiters.set(key, {
38343
- resolve: resolveDiagnostics,
38344
- reject: rejectDiagnostics,
38345
- timer
38346
- });
38562
+ const before = this.documentTurns.get(key) ?? Promise.resolve();
38563
+ let done = () => {};
38564
+ const mine = new Promise((finished) => {
38565
+ done = finished;
38347
38566
  });
38348
- let opened = null;
38567
+ const turn = before.then(() => mine);
38568
+ this.documentTurns.set(key, turn);
38569
+ await before;
38349
38570
  try {
38350
- opened = this.syncDocument(filePath, content);
38351
- return await diagnosticsPromise;
38352
- } catch (error) {
38353
- const waiter = this.diagnosticsWaiters.get(key);
38354
- if (waiter) {
38355
- clearTimeout(waiter.timer);
38356
- this.diagnosticsWaiters.delete(key);
38357
- }
38358
- throw error;
38571
+ return await work();
38359
38572
  } finally {
38360
- if (opened !== null) {
38361
- this.closeDocument(opened);
38573
+ done();
38574
+ if (this.documentTurns.get(key) === turn) {
38575
+ this.documentTurns.delete(key);
38362
38576
  }
38363
38577
  }
38364
38578
  }
38365
- async getCompletions(filePath, content, line, character) {
38579
+ async getDiagnostics(filePath, content) {
38366
38580
  await this.ensureConnected();
38367
38581
  await this.ensureInitializedForFile(filePath);
38368
- const uri = this.syncDocument(filePath, content);
38369
- let result;
38370
- try {
38371
- result = await this.sendRequest("textDocument/completion", {
38372
- textDocument: { uri },
38373
- 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
+ });
38374
38594
  });
38375
- } finally {
38376
- this.closeDocument(uri);
38377
- }
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
+ });
38378
38627
  if (Array.isArray(result)) {
38379
38628
  return result;
38380
38629
  }
@@ -38389,28 +38638,31 @@ class GodotLSPClient {
38389
38638
  async getHover(filePath, content, line, character) {
38390
38639
  await this.ensureConnected();
38391
38640
  await this.ensureInitializedForFile(filePath);
38392
- const uri = this.syncDocument(filePath, content);
38393
- try {
38394
- return await this.sendRequest("textDocument/hover", {
38395
- textDocument: { uri },
38396
- position: { line, character }
38397
- });
38398
- } finally {
38399
- this.closeDocument(uri);
38400
- }
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
+ });
38401
38652
  }
38402
38653
  async getDocumentSymbols(filePath, content) {
38403
38654
  await this.ensureConnected();
38404
38655
  await this.ensureInitializedForFile(filePath);
38405
- const uri = this.syncDocument(filePath, content);
38406
- let result;
38407
- try {
38408
- result = await this.sendRequest("textDocument/documentSymbol", {
38409
- textDocument: { uri }
38410
- });
38411
- } finally {
38412
- this.closeDocument(uri);
38413
- }
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
+ });
38414
38666
  return Array.isArray(result) ? result : [];
38415
38667
  }
38416
38668
  isConnected() {
@@ -38442,36 +38694,39 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
38442
38694
  try {
38443
38695
  projectPath = await realpath(requestedProjectPath);
38444
38696
  } catch {
38445
- 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.`);
38446
38701
  }
38447
38702
  const contained = resolveWithinProject(projectPath, scriptPathValue);
38448
38703
  if (!contained.ok) {
38449
- throw new Refusal(contained.reason);
38704
+ throw new ArgumentRefusal(contained.reason);
38450
38705
  }
38451
38706
  let scriptPath;
38452
38707
  try {
38453
38708
  scriptPath = await realpath(contained.absolutePath);
38454
38709
  } catch {
38455
- throw new Refusal(`Script file does not exist: ${contained.absolutePath}`);
38710
+ throw new ArgumentRefusal(`Script file does not exist: ${contained.absolutePath}`);
38456
38711
  }
38457
38712
  if (!isWithinRoot(projectPath, scriptPath)) {
38458
- throw new Refusal("scriptPath resolves outside the project root boundary.");
38713
+ throw new ArgumentRefusal("scriptPath resolves outside the project root boundary.");
38459
38714
  }
38460
38715
  return { projectPath, scriptPath };
38461
38716
  }
38462
38717
  async function handleLSPTool(client, toolName, args) {
38463
38718
  try {
38464
38719
  if (!args || typeof args !== "object") {
38465
- throw new Refusal("Tool arguments must be an object.");
38720
+ throw new ArgumentRefusal("Tool arguments must be an object.");
38466
38721
  }
38467
38722
  const parsedArgs = args;
38468
38723
  const projectPathValue = parsedArgs["projectPath"];
38469
38724
  const scriptPathValue = parsedArgs["scriptPath"];
38470
38725
  if (typeof projectPathValue !== "string" || projectPathValue.length === 0) {
38471
- throw new Refusal("Missing required argument: projectPath");
38726
+ throw new ArgumentRefusal("Missing required argument: projectPath");
38472
38727
  }
38473
38728
  if (typeof scriptPathValue !== "string" || scriptPathValue.length === 0) {
38474
- throw new Refusal("Missing required argument: scriptPath");
38729
+ throw new ArgumentRefusal("Missing required argument: scriptPath");
38475
38730
  }
38476
38731
  const { projectPath, scriptPath } = await resolveLSPPaths(projectPathValue, scriptPathValue);
38477
38732
  const content = await readFile(scriptPath, "utf8");
@@ -38485,7 +38740,7 @@ async function handleLSPTool(client, toolName, args) {
38485
38740
  const line = Number(parsedArgs["line"]);
38486
38741
  const character = Number(parsedArgs["character"]);
38487
38742
  if (!Number.isFinite(line) || !Number.isFinite(character)) {
38488
- throw new Refusal("Arguments line and character must be numbers.");
38743
+ throw new ArgumentRefusal("Arguments line and character must be numbers.");
38489
38744
  }
38490
38745
  const completions = await client.getCompletions(scriptPath, content, line, character);
38491
38746
  return asToolResponse({ completions });
@@ -38494,7 +38749,7 @@ async function handleLSPTool(client, toolName, args) {
38494
38749
  const line = Number(parsedArgs["line"]);
38495
38750
  const character = Number(parsedArgs["character"]);
38496
38751
  if (!Number.isFinite(line) || !Number.isFinite(character)) {
38497
- throw new Refusal("Arguments line and character must be numbers.");
38752
+ throw new ArgumentRefusal("Arguments line and character must be numbers.");
38498
38753
  }
38499
38754
  const hover = await client.getHover(scriptPath, content, line, character);
38500
38755
  return asToolResponse({ hover });
@@ -38508,20 +38763,31 @@ async function handleLSPTool(client, toolName, args) {
38508
38763
  }
38509
38764
  } catch (error) {
38510
38765
  return asToolResponse({
38511
- error: normalizeLSPError(error, client.port)
38766
+ error: normalizeLSPError(error, client.port),
38767
+ ...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
38768
+ ...error instanceof ArgumentRefusal ? { refusedArguments: true } : {}
38512
38769
  });
38513
38770
  }
38514
38771
  }
38515
- var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000;
38772
+ var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, ArgumentRefusal, AnotherProjectsServer;
38516
38773
  var init_lsp_client = __esm(() => {
38517
38774
  init_errors();
38518
38775
  init_framing();
38519
38776
  init_paths();
38520
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
+ };
38521
38787
  });
38522
38788
 
38523
38789
  // src/outside.ts
38524
- import { spawn as spawn2 } from "node:child_process";
38790
+ import { spawn as spawn3 } from "node:child_process";
38525
38791
  import { fileURLToPath as fileURLToPath4 } from "node:url";
38526
38792
  function changesFrom(env, base) {
38527
38793
  const changes = {};
@@ -38554,7 +38820,7 @@ function readLaunched(printed) {
38554
38820
  }
38555
38821
  async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
38556
38822
  return await new Promise((resolve) => {
38557
- const launcher = spawn2(runtime, [keeper, "launch"], {
38823
+ const launcher = spawn3(runtime, [keeper, "launch"], {
38558
38824
  stdio: ["pipe", "pipe", "pipe"],
38559
38825
  windowsHide: true
38560
38826
  });
@@ -38589,8 +38855,8 @@ var init_outside = __esm(() => {
38589
38855
  });
38590
38856
 
38591
38857
  // src/process-time.ts
38592
- import { execFile as execFile4 } from "node:child_process";
38593
- import { promisify as promisify4 } from "node:util";
38858
+ import { execFile as execFile3 } from "node:child_process";
38859
+ import { promisify as promisify3 } from "node:util";
38594
38860
  async function cpuSecondsOf(pid) {
38595
38861
  try {
38596
38862
  return process.platform === "win32" ? await secondsOnWindows(pid) : await askPosix2(pid);
@@ -38604,7 +38870,7 @@ async function secondsOnWindows(pid) {
38604
38870
  return Number.isFinite(seconds) ? seconds : undefined;
38605
38871
  }
38606
38872
  async function askPosix2(pid) {
38607
- 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 });
38608
38874
  return secondsFromClock(stdout.trim());
38609
38875
  }
38610
38876
  function secondsFromClock(printed) {
@@ -38622,20 +38888,20 @@ function secondsFromClock(printed) {
38622
38888
  const extra = days === null ? 0 : Number(days) * 86400;
38623
38889
  return Number.isFinite(extra) ? seconds + extra : undefined;
38624
38890
  }
38625
- var run4, ASK_TIMEOUT_MS2 = 6000;
38891
+ var run3, ASK_TIMEOUT_MS2 = 6000;
38626
38892
  var init_process_time = __esm(() => {
38627
38893
  init_windows_ask();
38628
- run4 = promisify4(execFile4);
38894
+ run3 = promisify3(execFile3);
38629
38895
  });
38630
38896
 
38631
38897
  // src/project-scan.ts
38632
- import { existsSync as existsSync14, readdirSync as readdirSync7, readFileSync as readFileSync16 } from "node:fs";
38633
- import { join as join19 } 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";
38634
38900
  function skipped(name) {
38635
38901
  return name.startsWith(".") || SKIPPED.has(name);
38636
38902
  }
38637
38903
  function steppedOver(directory) {
38638
- return existsSync14(join19(directory, ".gdignore"));
38904
+ return existsSync16(join20(directory, ".gdignore"));
38639
38905
  }
38640
38906
  function projectStructure(projectPath) {
38641
38907
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
@@ -38648,7 +38914,7 @@ function projectStructure(projectPath) {
38648
38914
  continue;
38649
38915
  }
38650
38916
  if (entry.isDirectory()) {
38651
- visit(join19(directory, entry.name));
38917
+ visit(join20(directory, entry.name));
38652
38918
  } else if (entry.isFile()) {
38653
38919
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
38654
38920
  if (extension === "tscn") {
@@ -38678,14 +38944,14 @@ function scriptsWithoutUid(projectPath) {
38678
38944
  }
38679
38945
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
38680
38946
  if (entry.isDirectory()) {
38681
- visit(join19(directory, entry.name), spelled);
38947
+ visit(join20(directory, entry.name), spelled);
38682
38948
  continue;
38683
38949
  }
38684
38950
  if (!entry.isFile()) {
38685
38951
  continue;
38686
38952
  }
38687
38953
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
38688
- if (CARRIES_A_UID.has(extension) && !existsSync14(join19(directory, `${entry.name}.uid`))) {
38954
+ if (CARRIES_A_UID.has(extension) && !existsSync16(join20(directory, `${entry.name}.uid`))) {
38689
38955
  found.push(spelled);
38690
38956
  }
38691
38957
  }
@@ -38720,7 +38986,7 @@ function searchProject(projectPath, options) {
38720
38986
  if (skipped(entry.name)) {
38721
38987
  continue;
38722
38988
  }
38723
- const entryPath = join19(directory, entry.name);
38989
+ const entryPath = join20(directory, entry.name);
38724
38990
  if (entry.isDirectory()) {
38725
38991
  visit(entryPath);
38726
38992
  continue;
@@ -38731,7 +38997,7 @@ function searchProject(projectPath, options) {
38731
38997
  }
38732
38998
  result.summary.files_searched += 1;
38733
38999
  const matches = [];
38734
- for (const [index, line] of readFileSync16(entryPath, "utf8").split(`
39000
+ for (const [index, line] of readFileSync17(entryPath, "utf8").split(`
38735
39001
  `).entries()) {
38736
39002
  if (full()) {
38737
39003
  break;
@@ -38760,15 +39026,15 @@ var init_project_scan = __esm(() => {
38760
39026
  });
38761
39027
 
38762
39028
  // src/restart-note.ts
38763
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync17, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "node:fs";
38764
- 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";
38765
39031
  function restartNotePath(projectPath) {
38766
- return join20(projectPath, ".godot", "gdharness-restart.json");
39032
+ return join21(projectPath, ".godot", "gdharness-restart.json");
38767
39033
  }
38768
39034
  function noteRestartBegun(note) {
38769
39035
  const path = restartNotePath(note.projectPath);
38770
39036
  try {
38771
- mkdirSync10(dirname13(path), { recursive: true });
39037
+ mkdirSync10(dirname14(path), { recursive: true });
38772
39038
  writeFileSync12(path, `${JSON.stringify(note, null, 2)}
38773
39039
  `, "utf8");
38774
39040
  } catch (error) {
@@ -38777,7 +39043,7 @@ function noteRestartBegun(note) {
38777
39043
  }
38778
39044
  function restartOwed(projectPath) {
38779
39045
  try {
38780
- const parsed = JSON.parse(readFileSync17(restartNotePath(projectPath), "utf8"));
39046
+ const parsed = JSON.parse(readFileSync18(restartNotePath(projectPath), "utf8"));
38781
39047
  if (typeof parsed !== "object" || parsed === null) {
38782
39048
  return null;
38783
39049
  }
@@ -38802,31 +39068,31 @@ var init_restart_note = __esm(() => {
38802
39068
  });
38803
39069
 
38804
39070
  // src/run-record.ts
38805
- import { execFile as execFile5 } from "node:child_process";
39071
+ import { execFile as execFile4 } from "node:child_process";
38806
39072
  import { createHash as createHash4 } from "node:crypto";
38807
39073
  import {
38808
39074
  mkdirSync as mkdirSync11,
38809
39075
  openSync as openSync2,
38810
39076
  readdirSync as readdirSync8,
38811
- readFileSync as readFileSync18,
38812
- realpathSync as realpathSync3,
39077
+ readFileSync as readFileSync19,
39078
+ realpathSync as realpathSync4,
38813
39079
  rmSync as rmSync10,
38814
39080
  statSync as statSync7,
38815
39081
  writeFileSync as writeFileSync13
38816
39082
  } from "node:fs";
38817
- import { basename as basename4, dirname as dirname14, join as join21, resolve as resolve5 } from "node:path";
38818
- 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";
38819
39085
  function runsDirectory() {
38820
- return join21(runtimeDirectory(), "runs");
39086
+ return join22(runtimeDirectory(), "runs");
38821
39087
  }
38822
39088
  function realAsFarAsItExists(path) {
38823
39089
  const missing = [];
38824
39090
  let at = path;
38825
39091
  for (;; ) {
38826
39092
  try {
38827
- return join21(realpathSync3.native(at), ...missing);
39093
+ return join22(realpathSync4.native(at), ...missing);
38828
39094
  } catch {
38829
- const parent = dirname14(at);
39095
+ const parent = dirname15(at);
38830
39096
  if (parent === at) {
38831
39097
  return path;
38832
39098
  }
@@ -38841,15 +39107,18 @@ function projectKey(projectPath) {
38841
39107
  return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
38842
39108
  }
38843
39109
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
38844
- return join21(directory, "runs", `note-${projectKey(projectPath)}.json`);
39110
+ return join22(directory, "runs", `note-${projectKey(projectPath)}.json`);
38845
39111
  }
38846
39112
  function openTranscript(startedAt) {
38847
39113
  mkdirSync11(runsDirectory(), { recursive: true });
38848
- const path = join21(runsDirectory(), `run-${startedAt}.log`);
39114
+ const path = join22(runsDirectory(), `run-${startedAt}.log`);
38849
39115
  return { path, fd: openSync2(path, "a") };
38850
39116
  }
39117
+ function noteIsAbout(note, playing) {
39118
+ return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
39119
+ }
38851
39120
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
38852
- return join21(directory, "runs", `editor-${projectKey(projectPath)}.json`);
39121
+ return join22(directory, "runs", `editor-${projectKey(projectPath)}.json`);
38853
39122
  }
38854
39123
  function writeEditorRunNote(note) {
38855
39124
  mkdirSync11(runsDirectory(), { recursive: true });
@@ -38857,7 +39126,7 @@ function writeEditorRunNote(note) {
38857
39126
  }
38858
39127
  function readEditorRunNote(projectPath) {
38859
39128
  for (const directory of runtimeDirectories()) {
38860
- 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"));
38861
39130
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
38862
39131
  return note;
38863
39132
  }
@@ -38867,7 +39136,7 @@ function readEditorRunNote(projectPath) {
38867
39136
  function editorNoteAt(path) {
38868
39137
  let parsed;
38869
39138
  try {
38870
- parsed = JSON.parse(readFileSync18(path, "utf8"));
39139
+ parsed = JSON.parse(readFileSync19(path, "utf8"));
38871
39140
  } catch {
38872
39141
  return null;
38873
39142
  }
@@ -38881,7 +39150,9 @@ function editorNoteAt(path) {
38881
39150
  if (typeof projectPath !== "string" || typeof transcript !== "string" || typeof startedAt !== "number") {
38882
39151
  return null;
38883
39152
  }
38884
- 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 } };
38885
39156
  }
38886
39157
  function readRunRecord(projectPath) {
38887
39158
  for (const directory of runtimeDirectories()) {
@@ -38889,7 +39160,7 @@ function readRunRecord(projectPath) {
38889
39160
  if (own !== null) {
38890
39161
  return own;
38891
39162
  }
38892
- const shared = recordAt(join21(directory, "runs", SHARED_NOTE));
39163
+ const shared = recordAt(join22(directory, "runs", SHARED_NOTE));
38893
39164
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
38894
39165
  return shared;
38895
39166
  }
@@ -38901,13 +39172,13 @@ function everyRunRecord() {
38901
39172
  for (const directory of runtimeDirectories()) {
38902
39173
  let entries;
38903
39174
  try {
38904
- entries = readdirSync8(join21(directory, "runs"));
39175
+ entries = readdirSync8(join22(directory, "runs"));
38905
39176
  } catch {
38906
39177
  continue;
38907
39178
  }
38908
39179
  for (const entry of entries) {
38909
39180
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
38910
- const record = recordAt(join21(directory, "runs", entry));
39181
+ const record = recordAt(join22(directory, "runs", entry));
38911
39182
  if (record !== null) {
38912
39183
  found.push(record);
38913
39184
  }
@@ -38919,7 +39190,7 @@ function everyRunRecord() {
38919
39190
  function recordAt(path) {
38920
39191
  let parsed;
38921
39192
  try {
38922
- parsed = JSON.parse(readFileSync18(path, "utf8"));
39193
+ parsed = JSON.parse(readFileSync19(path, "utf8"));
38923
39194
  } catch {
38924
39195
  return null;
38925
39196
  }
@@ -38965,7 +39236,7 @@ async function runningAs(pid) {
38965
39236
  const began = await startedAt(pid, null);
38966
39237
  const when = began === null ? {} : { startedAt: began };
38967
39238
  if (process.platform === "linux") {
38968
- const raw = readFileSync18(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
39239
+ const raw = readFileSync19(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
38969
39240
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
38970
39241
  }
38971
39242
  const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
@@ -39093,7 +39364,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
39093
39364
  }
39094
39365
  function clearRunRecord(projectPath) {
39095
39366
  for (const directory of runtimeDirectories()) {
39096
- const shared = join21(directory, "runs", SHARED_NOTE);
39367
+ const shared = join22(directory, "runs", SHARED_NOTE);
39097
39368
  const sharedRecord = recordAt(shared);
39098
39369
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
39099
39370
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
@@ -39123,7 +39394,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
39123
39394
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
39124
39395
  continue;
39125
39396
  }
39126
- const path = join21(directory, entry);
39397
+ const path = join22(directory, entry);
39127
39398
  if (kept.has(path) || ageMs(path, now) < keepMs) {
39128
39399
  continue;
39129
39400
  }
@@ -39138,31 +39409,31 @@ var init_run_record = __esm(() => {
39138
39409
  init_process_children();
39139
39410
  init_runtime_client();
39140
39411
  init_windows_ask();
39141
- execFileAsync = promisify5(execFile5);
39412
+ execFileAsync = promisify4(execFile4);
39142
39413
  });
39143
39414
 
39144
39415
  // src/server.ts
39145
- import { execFile as execFile6, spawn as spawn3 } from "node:child_process";
39416
+ import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
39146
39417
  import { randomUUID as randomUUID2 } from "node:crypto";
39147
39418
  import {
39148
39419
  appendFileSync,
39149
39420
  closeSync as closeSync2,
39150
39421
  constants,
39151
39422
  copyFileSync,
39152
- existsSync as existsSync15,
39423
+ existsSync as existsSync17,
39153
39424
  fstatSync,
39154
39425
  mkdirSync as mkdirSync12,
39155
39426
  openSync as openSync3,
39156
39427
  readdirSync as readdirSync9,
39157
- readFileSync as readFileSync19,
39428
+ readFileSync as readFileSync20,
39158
39429
  readSync as readSync2,
39159
39430
  rmdirSync as rmdirSync2,
39160
39431
  statSync as statSync8
39161
39432
  } from "node:fs";
39162
- 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";
39163
39434
  import { setTimeout as delay3 } from "node:timers/promises";
39164
39435
  import { fileURLToPath as fileURLToPath5 } from "node:url";
39165
- import { promisify as promisify6 } from "node:util";
39436
+ import { promisify as promisify5 } from "node:util";
39166
39437
  function captureDestinationRefusal(outputPath, projects) {
39167
39438
  if (/^(res|user):\/\//.test(outputPath)) {
39168
39439
  return `outputPath ${outputPath} is a Godot path; a capture is saved outside the project, at an absolute path on this machine.`;
@@ -39173,16 +39444,16 @@ function captureDestinationRefusal(outputPath, projects) {
39173
39444
  if (extname2(outputPath).toLowerCase() !== ".png") {
39174
39445
  return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
39175
39446
  }
39176
- const directory = realPathOr(dirname15(outputPath));
39177
- if (!existsSync15(directory)) {
39178
- 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.`;
39179
39450
  }
39180
- const where = join22(directory, basename5(outputPath));
39451
+ const where = join23(directory, basename5(outputPath));
39181
39452
  const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
39182
39453
  if (inside !== undefined) {
39183
39454
  return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
39184
39455
  }
39185
- if (existsSync15(where)) {
39456
+ if (existsSync17(where)) {
39186
39457
  return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
39187
39458
  }
39188
39459
  return null;
@@ -39213,16 +39484,18 @@ function describeHalt(halt) {
39213
39484
  function exited(run) {
39214
39485
  return run.exitCode !== null || run.exitSignal !== null;
39215
39486
  }
39216
- function endingOf(failed) {
39217
- return {
39218
- exitCode: typeof failed.code === "number" ? failed.code : null,
39219
- exitSignal: typeof failed.signal === "string" ? failed.signal : null,
39220
- failure: typeof failed.code === "string" ? failed.code : null
39221
- };
39222
- }
39223
39487
  function howItExited(run) {
39224
39488
  return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}, with no exit code`;
39225
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
+ }
39226
39499
  function stillRunning(run) {
39227
39500
  if (run === null || exited(run) || run.endedUnwatched === true) {
39228
39501
  return false;
@@ -39238,6 +39511,11 @@ function stillRunning(run) {
39238
39511
  }
39239
39512
  return true;
39240
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
+ }
39241
39519
  function runIsUp(run, editorSays, now = Date.now()) {
39242
39520
  if (run?.throughEditor !== true) {
39243
39521
  return stillRunning(run);
@@ -39285,11 +39563,19 @@ function stopVerdict(stop) {
39285
39563
  return {
39286
39564
  stopped: true,
39287
39565
  endedPid: stop.endedPid,
39288
- clean: stop.errors === 0,
39566
+ clean: stop.clean,
39289
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.",
39290
39568
  withChildren: false
39291
39569
  };
39292
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
+ }
39293
39579
  if (stop.ending === "refused") {
39294
39580
  return {
39295
39581
  stopped: false,
@@ -39301,13 +39587,13 @@ function stopVerdict(stop) {
39301
39587
  return {
39302
39588
  stopped: true,
39303
39589
  endedPid: stop.endedPid,
39304
- clean: stop.errors === 0,
39590
+ clean: stop.clean,
39305
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.`}`,
39306
39592
  withChildren: true
39307
39593
  };
39308
39594
  }
39309
39595
  function endedPreviousRun(ended) {
39310
- if (ended === null || ended.ending === "refused") {
39596
+ if (ended === null || ended.ending === "refused" || ended.ending === "unreached") {
39311
39597
  return;
39312
39598
  }
39313
39599
  return ended.pid ?? true;
@@ -39389,6 +39675,9 @@ function endedToStartThis(ended) {
39389
39675
  if (ended.ending === "refused") {
39390
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.`;
39391
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
+ }
39392
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.` : "";
39393
39682
  return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.${after}`;
39394
39683
  }
@@ -39514,19 +39803,58 @@ function withScanWait(outcome, scanned) {
39514
39803
  if (scanned.waitedMs === 0) {
39515
39804
  return outcome;
39516
39805
  }
39517
- const said = scanWaitAnswer(scanned);
39806
+ const said = scanWaitAnswer(scanned, "engine");
39518
39807
  if (outcome.ok) {
39519
39808
  return { ...outcome, payload: { ...outcome.payload, ...said } };
39520
39809
  }
39521
39810
  return said.scanNote === undefined ? outcome : { ...outcome, message: `${outcome.message} ${said.scanNote}` };
39522
39811
  }
39523
- 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") {
39524
39851
  if (scanned.waitedMs === 0) {
39525
39852
  return {};
39526
39853
  }
39854
+ const still = `The editor was still scanning the project after ${SCAN_WAIT_MS / 1000} seconds, so`;
39527
39855
  return {
39528
39856
  waitedForEditorScanMs: scanned.waitedMs,
39529
- 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."
39530
39858
  };
39531
39859
  }
39532
39860
  function changedSettings(before, after) {
@@ -39581,16 +39909,17 @@ function importStatuses(payload) {
39581
39909
  };
39582
39910
  });
39583
39911
  }
39584
- function escapingPropertyValue(properties) {
39585
- 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) {
39586
39917
  return;
39587
39918
  }
39588
- for (const value of Object.values(properties)) {
39589
- if (typeof value !== "string" || !value.includes("/")) {
39590
- continue;
39591
- }
39592
- if (value.split(/[/\\]/).includes("..")) {
39593
- return JSON.stringify(value);
39919
+ for (const item of Object.values(value)) {
39920
+ const found = escapingPropertyValue(item);
39921
+ if (found !== undefined) {
39922
+ return found;
39594
39923
  }
39595
39924
  }
39596
39925
  return;
@@ -39611,7 +39940,7 @@ function aboveTheRunner(entry) {
39611
39940
  }
39612
39941
  function elsewhereIn(projectPath, asked) {
39613
39942
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
39614
- if (wanted === "" || existsSync15(join22(projectPath, wanted))) {
39943
+ if (wanted === "" || existsSync17(join23(projectPath, wanted))) {
39615
39944
  return "";
39616
39945
  }
39617
39946
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -39625,7 +39954,7 @@ function elsewhereIn(projectPath, asked) {
39625
39954
  return near.length === 0 ? "" : ` The project does have ${there}.`;
39626
39955
  }
39627
39956
  function hasMainScene(projectFile) {
39628
- const scene = parseProjectGodot(readFileSync19(projectFile, "utf8"))["application"]?.["run/main_scene"];
39957
+ const scene = parseProjectGodot(readFileSync20(projectFile, "utf8"))["application"]?.["run/main_scene"];
39629
39958
  return typeof scene === "string" && scene !== "";
39630
39959
  }
39631
39960
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -39633,13 +39962,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
39633
39962
  return false;
39634
39963
  }
39635
39964
  try {
39636
- return readFileSync19(join22(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
39965
+ return readFileSync20(join23(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
39637
39966
  } catch {
39638
39967
  return false;
39639
39968
  }
39640
39969
  }
39641
39970
  function editorPlaysHeadless(projectFile) {
39642
- const runArgs = parseProjectGodot(readFileSync19(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
39971
+ const runArgs = parseProjectGodot(readFileSync20(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
39643
39972
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
39644
39973
  }
39645
39974
  function camelCased(params) {
@@ -39658,7 +39987,7 @@ function needsItsOwnProcess(asked) {
39658
39987
  class GodotServer {
39659
39988
  mcp;
39660
39989
  locator = new GodotLocator;
39661
- operationsScript = join22(__dirname2, "godot", "operations", "godot_operations.gd");
39990
+ operationsScript = join23(__dirname2, "godot", "operations", "godot_operations.gd");
39662
39991
  godotBridge;
39663
39992
  tools = buildToolDefinitions();
39664
39993
  activeProcess = null;
@@ -40158,6 +40487,9 @@ class GodotServer {
40158
40487
  const contained = this.containProjectFiles(asks);
40159
40488
  return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
40160
40489
  }
40490
+ if (headless === "create_script") {
40491
+ return await this.handleCreateScript(answerable);
40492
+ }
40161
40493
  if (headless !== "refresh_class_cache") {
40162
40494
  return await this.headless(headless, answerable);
40163
40495
  }
@@ -40328,7 +40660,7 @@ class GodotServer {
40328
40660
  return await this.handleRuntimeCommand("click", {
40329
40661
  ...whichGame(args),
40330
40662
  path: readNonEmptyString(args, "nodePath") ?? "",
40331
- button: readString(args, "button") ?? "left",
40663
+ button: args["button"] ?? "left",
40332
40664
  double: readBoolean(args, "doubleClick") ?? false,
40333
40665
  ...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
40334
40666
  ...args["index"] === undefined ? {} : { index: args["index"] }
@@ -40400,8 +40732,8 @@ class GodotServer {
40400
40732
  if (path === undefined) {
40401
40733
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
40402
40734
  }
40403
- const file = join22(path, "project.godot");
40404
- if (!existsSync15(file)) {
40735
+ const file = join23(path, "project.godot");
40736
+ if (!existsSync17(file)) {
40405
40737
  return {
40406
40738
  ok: false,
40407
40739
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -40611,13 +40943,30 @@ class GodotServer {
40611
40943
  }
40612
40944
  contained[key] = `res://${location.relativePath}`;
40613
40945
  }
40614
- const escaping = escapingPropertyValue(args["properties"]);
40946
+ const escaping = escapingPropertyValue([args["properties"], args["track"]]);
40615
40947
  if (escaping !== undefined) {
40616
40948
  return {
40617
40949
  ok: false,
40618
40950
  response: this.createErrorResponse(`The property value ${escaping} resolves outside the project directory.`, PATH_SOLUTIONS)
40619
40951
  };
40620
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
+ }
40621
40970
  const pluginName = readNonEmptyString(args, "pluginName");
40622
40971
  if (pluginName !== undefined) {
40623
40972
  const plugin = resolveWithinProject(projectPath, `addons/${pluginName}/plugin.cfg`);
@@ -40665,6 +41014,7 @@ class GodotServer {
40665
41014
  return this.answer(withScanWait({
40666
41015
  ok: true,
40667
41016
  messages: imported.messages,
41017
+ ...imported.messagesOmitted === undefined ? {} : { messagesOmitted: imported.messagesOmitted },
40668
41018
  payload: {
40669
41019
  uidsCreated: given,
40670
41020
  stillWithoutUid: after,
@@ -40715,16 +41065,25 @@ class GodotServer {
40715
41065
  if (changed.length === 0) {
40716
41066
  return written;
40717
41067
  }
40718
- 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));
40719
41075
  return this.jsonTextResponse({
40720
41076
  ...answer,
40721
- ...taken,
40722
- ...taken["editorNote"] === undefined ? { editorAdopted: changed } : {}
41077
+ editorAdopted: adopted,
41078
+ ...passedOver.length > 0 ? { editorPassedOver: passedOver } : {}
40723
41079
  });
40724
41080
  }
40725
- async editorTakes(command, args) {
41081
+ async editorTakes(command, args, answered = {}) {
40726
41082
  try {
40727
- 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
+ }
40728
41087
  return {};
40729
41088
  } catch (error) {
40730
41089
  const message = errorMessage(error);
@@ -40762,7 +41121,19 @@ class GodotServer {
40762
41121
  }
40763
41122
  const applied = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
40764
41123
  if (!applied.ok) {
40765
- 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
+ };
40766
41137
  }
40767
41138
  return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
40768
41139
  }
@@ -40780,7 +41151,14 @@ class GodotServer {
40780
41151
  response: this.createErrorResponse(`${missingSource[0]} is not on disk, so there is nothing to import.`)
40781
41152
  };
40782
41153
  }
40783
- 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);
40784
41162
  if (targets.length === 0) {
40785
41163
  return {
40786
41164
  ok: true,
@@ -40819,9 +41197,7 @@ class GodotServer {
40819
41197
  if (!imported.ok) {
40820
41198
  return { ok: false, response: this.answer(withScanWait(imported, scanned)) };
40821
41199
  }
40822
- if (imported.messages.length > 0) {
40823
- extra["engine_messages"] = imported.messages;
40824
- }
41200
+ Object.assign(extra, engineExtras(imported));
40825
41201
  if (imported.extensionNote !== undefined) {
40826
41202
  extra["extensionNote"] = imported.extensionNote;
40827
41203
  }
@@ -40904,17 +41280,15 @@ class GodotServer {
40904
41280
  return withScanWait(outcome, scanned);
40905
41281
  }
40906
41282
  answer(outcome) {
41283
+ const extras = engineExtras(outcome);
40907
41284
  if (!outcome.ok) {
40908
41285
  const response = this.createErrorResponse(outcome.message);
40909
- if (outcome.messages.length > 0) {
40910
- response.content.push({
40911
- type: "text",
40912
- text: JSON.stringify({ engine_messages: outcome.messages }, null, 2)
40913
- });
41286
+ if (Object.keys(extras).length > 0) {
41287
+ response.content.push({ type: "text", text: JSON.stringify(extras, null, 2) });
40914
41288
  }
40915
41289
  return response;
40916
41290
  }
40917
- return this.jsonTextResponse(outcome.messages.length > 0 ? { ...outcome.payload, engine_messages: outcome.messages } : outcome.payload);
41291
+ return this.jsonTextResponse({ ...outcome.payload, ...extras });
40918
41292
  }
40919
41293
  async handleProjectInfo(args) {
40920
41294
  const include = readStringArray(args, "include") ?? [];
@@ -40930,7 +41304,7 @@ class GodotServer {
40930
41304
  if (!engine.ok) {
40931
41305
  return engine.response;
40932
41306
  }
40933
- const application = parseProjectGodot(readFileSync19(project.value.file, "utf8"))["application"] ?? {};
41307
+ const application = parseProjectGodot(readFileSync20(project.value.file, "utf8"))["application"] ?? {};
40934
41308
  const name = application["config/name"];
40935
41309
  const mainScene = application["run/main_scene"];
40936
41310
  const info = {
@@ -40947,7 +41321,7 @@ class GodotServer {
40947
41321
  continue;
40948
41322
  }
40949
41323
  const outcome = await this.operation(spec.operation, spec.params(args, detailed), project.value.path);
40950
- 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) };
40951
41325
  }
40952
41326
  return this.jsonTextResponse(info);
40953
41327
  }
@@ -40991,18 +41365,18 @@ class GodotServer {
40991
41365
  if (!engine.ok) {
40992
41366
  return engine.response;
40993
41367
  }
40994
- const folder = dirname15(output.absolutePath);
41368
+ const folder = dirname16(output.absolutePath);
40995
41369
  try {
40996
41370
  mkdirSync12(folder, { recursive: true });
40997
41371
  } catch (error) {
40998
- 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)}`);
40999
41373
  }
41000
41374
  const debug = readBoolean(args, "debug") ?? false;
41001
41375
  const exportLogs = scratchDirectory("export");
41002
41376
  const exportArgs = [
41003
41377
  "--headless",
41004
41378
  "--log-file",
41005
- join22(exportLogs, "engine.log"),
41379
+ join23(exportLogs, "engine.log"),
41006
41380
  "--path",
41007
41381
  project.value.path,
41008
41382
  debug ? "--export-debug" : "--export-release",
@@ -41010,55 +41384,18 @@ class GodotServer {
41010
41384
  output.absolutePath
41011
41385
  ];
41012
41386
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
41013
- const log = new GameLog;
41014
- 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;
41015
41390
  try {
41016
- const { stdout, stderr } = await run5(engine.value, exportArgs, {
41017
- timeout: 300000,
41018
- maxBuffer: 64 * 1024 * 1024,
41019
- signal: callSignal()
41020
- });
41021
- log.append("stdout", stdout);
41022
- log.append("stderr", stderr);
41023
- } catch (error) {
41024
- if (!(error instanceof Error && ("stdout" in error) && ("stderr" in error))) {
41025
- return this.createErrorResponse(`Export could not be run: ${errorMessage(error)}`);
41026
- }
41027
- const failed = error;
41028
- log.append("stdout", failed.stdout);
41029
- log.append("stderr", failed.stderr);
41030
- ending = endingOf(failed);
41391
+ ending = await runEngine(engine.value, exportArgs, { timeout: { ms: 300000, said: "five minutes" } });
41031
41392
  } finally {
41032
41393
  discard(exportLogs);
41033
41394
  }
41034
- log.finish();
41035
- const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
41036
- const verdict = {
41037
- exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync15(output.absolutePath),
41038
- preset,
41039
- outputPath: output.relativePath,
41040
- debug,
41041
- exitCode: ending.exitCode,
41042
- exitSignal: ending.exitSignal ?? undefined,
41043
- failure: ending.failure ?? undefined,
41044
- errors: log.count("error"),
41045
- warnings: log.count("warning"),
41046
- entries: forAnswer(problems.entries)
41047
- };
41048
- if (!verdict.exported) {
41049
- 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.`;
41050
- return {
41051
- content: [
41052
- {
41053
- type: "text",
41054
- text: `Export with preset '${preset}' did not produce ${output.relativePath}.${why}`
41055
- },
41056
- { type: "text", text: JSON.stringify(verdict, null, 2) }
41057
- ],
41058
- isError: true
41059
- };
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"}`);
41060
41397
  }
41061
- return this.jsonTextResponse(verdict);
41398
+ return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt());
41062
41399
  }
41063
41400
  async handleRunTests(args) {
41064
41401
  const project = this.project(args);
@@ -41070,7 +41407,7 @@ class GodotServer {
41070
41407
  return contained.response;
41071
41408
  }
41072
41409
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
41073
- if (!existsSync15(join22(project.value.path, runner))) {
41410
+ if (!existsSync17(join23(project.value.path, runner))) {
41074
41411
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
41075
41412
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
41076
41413
  ]);
@@ -41136,21 +41473,22 @@ class GodotServer {
41136
41473
  if (timedOut && run.process.pid !== undefined) {
41137
41474
  await untilGone([run.process.pid]);
41138
41475
  }
41139
- const reportsDir = join22(project.value.path, ".godot", ...ours.split("/"));
41476
+ await run.settled;
41477
+ const reportsDir = join23(project.value.path, ".godot", ...ours.split("/"));
41140
41478
  let report = null;
41141
41479
  let reportProblem = null;
41142
41480
  try {
41143
- 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))) : [];
41144
41482
  const newest = written.at(-1);
41145
41483
  if (newest !== undefined) {
41146
- report = parseJUnit(readFileSync19(join22(reportsDir, newest, "results.xml"), "utf8"));
41484
+ report = parseJUnit(readFileSync20(join23(reportsDir, newest, "results.xml"), "utf8"));
41147
41485
  }
41148
41486
  } catch (error) {
41149
41487
  reportProblem = errorMessage(error);
41150
41488
  } finally {
41151
41489
  discard(reportsDir, userData);
41152
41490
  try {
41153
- rmdirSync2(dirname15(reportsDir));
41491
+ rmdirSync2(dirname16(reportsDir));
41154
41492
  } catch {}
41155
41493
  }
41156
41494
  const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
@@ -41163,7 +41501,11 @@ class GodotServer {
41163
41501
  105: "script errors"
41164
41502
  };
41165
41503
  const exitCode = run.exitCode;
41166
- 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;
41167
41509
  const said = printed.map((entry) => entry.text);
41168
41510
  const nothingRan = timedOut ? null : whyNoReport(said, asked);
41169
41511
  const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
@@ -41179,7 +41521,7 @@ class GodotServer {
41179
41521
  ...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
41180
41522
  };
41181
41523
  if (report === null) {
41182
- 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}).`;
41183
41525
  return {
41184
41526
  content: [
41185
41527
  { type: "text", text: note },
@@ -41216,10 +41558,21 @@ class GodotServer {
41216
41558
  orphans: suite.orphans,
41217
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().`
41218
41560
  }));
41219
- 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
+ });
41220
41573
  const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
41221
41574
  return this.jsonTextResponse({
41222
- 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,
41223
41576
  verdict,
41224
41577
  ...timedOut ? { timedOut, hung, silentForMs } : {},
41225
41578
  exitCode,
@@ -41230,13 +41583,14 @@ class GodotServer {
41230
41583
  skipped: report.skipped,
41231
41584
  time: report.time,
41232
41585
  failed,
41586
+ hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
41233
41587
  warnings: warnings.length > 0 ? warnings : undefined,
41234
41588
  orphans: orphans.total > 0 ? orphans.total : undefined,
41235
41589
  notRun: notRun > 0 ? notRun : undefined,
41236
41590
  ...printedLines,
41237
41591
  savesNote,
41238
41592
  ...scanned,
41239
- 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),
41240
41594
  suites: unclean.map((suite) => ({
41241
41595
  name: suite.name,
41242
41596
  path: suite.path,
@@ -41244,6 +41598,7 @@ class GodotServer {
41244
41598
  notRun: suite.discovered > suite.tests ? suite.discovered - suite.tests : undefined,
41245
41599
  failures: suite.failures,
41246
41600
  errors: suite.errors,
41601
+ hookFailures: suite.hookFailures + suite.hookErrors > 0 ? suite.hookFailures + suite.hookErrors : undefined,
41247
41602
  skipped: suite.skipped,
41248
41603
  orphans: orphansIn(suite.path) > 0 ? orphansIn(suite.path) : undefined,
41249
41604
  time: suite.time
@@ -41257,7 +41612,7 @@ class GodotServer {
41257
41612
  }
41258
41613
  async godotVersion(godotPath) {
41259
41614
  try {
41260
- const { stdout } = await run5(godotPath, ["--version"], { timeout: 1e4 });
41615
+ const { stdout } = await run4(godotPath, ["--version"], { timeout: 1e4 });
41261
41616
  this.lastGodotVersion = stdout.trim();
41262
41617
  return this.lastGodotVersion;
41263
41618
  } catch (error) {
@@ -41269,8 +41624,18 @@ class GodotServer {
41269
41624
  const answer = await this.handleLSP("lsp_get_diagnostics", args);
41270
41625
  const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
41271
41626
  if (payload["error"] !== undefined) {
41272
- const reason = payload["error"];
41273
- 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}`, [
41274
41639
  `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
41275
41640
  "GDHARNESS_LSP_PORT points this server at another one"
41276
41641
  ]);
@@ -41301,11 +41666,11 @@ class GodotServer {
41301
41666
  const cached = cachedClasses(projectPath);
41302
41667
  const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
41303
41668
  const contained = resolveWithinProject(projectPath, resourcePath);
41304
- if (!contained.ok || !existsSync15(contained.absolutePath)) {
41669
+ if (!contained.ok || !existsSync17(contained.absolutePath)) {
41305
41670
  return null;
41306
41671
  }
41307
41672
  try {
41308
- return readFileSync19(contained.absolutePath, "utf8");
41673
+ return readFileSync20(contained.absolutePath, "utf8");
41309
41674
  } catch {
41310
41675
  return null;
41311
41676
  }
@@ -41361,11 +41726,11 @@ class GodotServer {
41361
41726
  }
41362
41727
  sourceOfClass(projectPath, resourcePath) {
41363
41728
  const contained = resolveWithinProject(projectPath, resourcePath);
41364
- if (!contained.ok || !existsSync15(contained.absolutePath)) {
41729
+ if (!contained.ok || !existsSync17(contained.absolutePath)) {
41365
41730
  return null;
41366
41731
  }
41367
41732
  try {
41368
- return readFileSync19(contained.absolutePath, "utf8");
41733
+ return readFileSync20(contained.absolutePath, "utf8");
41369
41734
  } catch {
41370
41735
  return null;
41371
41736
  }
@@ -41553,7 +41918,8 @@ class GodotServer {
41553
41918
  return {
41554
41919
  playing: readBoolean(asParams(answer), "playing") ?? false,
41555
41920
  scene: readString(asParams(answer), "scenePath") ?? "",
41556
- debugPort: readNumber(asParams(answer), "debugPort")
41921
+ debugPort: readNumber(asParams(answer), "debugPort"),
41922
+ play: playOf(asParams(answer))
41557
41923
  };
41558
41924
  } catch {
41559
41925
  return null;
@@ -41649,7 +42015,7 @@ class GodotServer {
41649
42015
  return null;
41650
42016
  }
41651
42017
  try {
41652
- return settingKeys(readFileSync19(join22(projectPath, "project.godot"), "utf8"));
42018
+ return settingKeys(readFileSync20(join23(projectPath, "project.godot"), "utf8"));
41653
42019
  } catch {
41654
42020
  return null;
41655
42021
  }
@@ -41812,6 +42178,13 @@ class GodotServer {
41812
42178
  if (!project.ok) {
41813
42179
  return project.response;
41814
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
+ }
41815
42188
  const scene = readNonEmptyString(args, "scene");
41816
42189
  const sceneToRun = scene !== undefined ? resolveWithinProject(project.value.path, scene) : null;
41817
42190
  if (sceneToRun && !sceneToRun.ok) {
@@ -41906,7 +42279,7 @@ class GodotServer {
41906
42279
  }
41907
42280
  async runtimeUp(projectPath, before, budget, withArgs) {
41908
42281
  const budgetMs = budget.ms;
41909
- if (!existsSync15(join22(projectPath, RUNTIME_AUTOLOAD.path))) {
42282
+ if (!existsSync17(join23(projectPath, RUNTIME_AUTOLOAD.path))) {
41910
42283
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
41911
42284
  }
41912
42285
  let asking = null;
@@ -41915,7 +42288,7 @@ class GodotServer {
41915
42288
  let editorAsks = 0;
41916
42289
  const waitBegan = Date.now();
41917
42290
  const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
41918
- const endpoint = await announcedSince(projectPath, before, {
42291
+ const fresh = await announcedSince(projectPath, before, {
41919
42292
  budgetMs,
41920
42293
  accept: (one) => this.isTheGameOf(this.activeProcess, one, projectPath),
41921
42294
  giveUp: () => {
@@ -41939,11 +42312,19 @@ class GodotServer {
41939
42312
  }
41940
42313
  });
41941
42314
  const asked = editorAsks === 0 ? "editor not asked" : `editor asked ${editorAsks === 1 ? "once" : `${editorAsks} times`}, last said ${lastSaid()}`;
41942
- 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}`);
41943
42316
  const going = await this.currentRun();
42317
+ const endpoint = await this.theRunsAmong(going, fresh);
41944
42318
  if (endpoint !== null && going !== null && going.announcedPid === undefined) {
41945
42319
  this.tie(going, endpoint);
41946
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
+ }
41947
42328
  return runtimeVerdict(endpoint, {
41948
42329
  addon: true,
41949
42330
  budgetMs,
@@ -42027,7 +42408,13 @@ class GodotServer {
42027
42408
  };
42028
42409
  this.activeProcess = played;
42029
42410
  this.watchThePlayedConsole();
42030
- 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
+ });
42031
42418
  sweepTranscripts();
42032
42419
  return this.jsonTextResponse({
42033
42420
  started: true,
@@ -42142,7 +42529,7 @@ class GodotServer {
42142
42529
  return 0;
42143
42530
  }
42144
42531
  try {
42145
- return reportAlreadyInTranscript(readFileSync19(game.transcript), readFileSync19(report));
42532
+ return reportAlreadyInTranscript(readFileSync20(game.transcript), readFileSync20(report));
42146
42533
  } catch (error) {
42147
42534
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
42148
42535
  return 0;
@@ -42156,6 +42543,18 @@ class GodotServer {
42156
42543
  game.brokeOn = halt.text;
42157
42544
  game.log.record("error", halt.text);
42158
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
+ }
42159
42558
  async endActiveGame(reason, going) {
42160
42559
  const running = this.activeProcess;
42161
42560
  const project = running?.projectPath ?? this.ourProject();
@@ -42174,8 +42573,16 @@ class GodotServer {
42174
42573
  running.endedHere = reason;
42175
42574
  const announced = this.announcedPidOf(running);
42176
42575
  if (running.throughEditor) {
42177
- await this.handleViaBridge("stop_playing", {});
42178
- 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";
42179
42586
  }
42180
42587
  if (running.pid === null) {
42181
42588
  return "gone";
@@ -42215,7 +42622,7 @@ class GodotServer {
42215
42622
  return gone ? "gone" : "lingering";
42216
42623
  }
42217
42624
  spawnGame(godotPath, cmdArgs, env) {
42218
- const child = spawn3(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
42625
+ const child = spawn4(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
42219
42626
  const cancelled = callSignal();
42220
42627
  if (cancelled !== undefined) {
42221
42628
  const end = () => {
@@ -42255,7 +42662,6 @@ class GodotServer {
42255
42662
  });
42256
42663
  child.on("exit", (code, signal) => {
42257
42664
  this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
42258
- log.finish();
42259
42665
  started.exitCode = code;
42260
42666
  started.exitSignal = code === null ? signal : null;
42261
42667
  });
@@ -42265,7 +42671,28 @@ class GodotServer {
42265
42671
  `);
42266
42672
  started.exitCode = -1;
42267
42673
  });
42268
- 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 });
42269
42696
  }
42270
42697
  async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
42271
42698
  const startedAt = Date.now();
@@ -42347,7 +42774,8 @@ class GodotServer {
42347
42774
  this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
42348
42775
  }
42349
42776
  const project = this.godotBridge.getStatus().projectPath ?? null;
42350
- 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;
42351
42779
  const picked = {
42352
42780
  pid: null,
42353
42781
  log: new GameLog,
@@ -42404,6 +42832,31 @@ class GodotServer {
42404
42832
  const its = await this.tiedThroughTheTree(run, running);
42405
42833
  return its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
42406
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
+ }
42407
42860
  async tiedThroughTheTree(run, announced) {
42408
42861
  if (run.announcedPid !== undefined) {
42409
42862
  return run.announcedPid;
@@ -42604,6 +43057,7 @@ class GodotServer {
42604
43057
  resolve(false);
42605
43058
  });
42606
43059
  });
43060
+ await boot.settled;
42607
43061
  const errors = boot.log.count("error");
42608
43062
  const warnings = boot.log.count("warning");
42609
43063
  return this.jsonTextResponse({
@@ -42745,6 +43199,7 @@ class GodotServer {
42745
43199
  if (!run) {
42746
43200
  return this.createErrorResponse(this.nothingOfOursIsRunning());
42747
43201
  }
43202
+ await this.tieThePlayedGame(run);
42748
43203
  this.drainEditorOutput(run);
42749
43204
  this.drainTranscript(run);
42750
43205
  const going = await this.runStillGoing(run);
@@ -42780,7 +43235,7 @@ class GodotServer {
42780
43235
  notes.push(endedWithoutACode({
42781
43236
  throughEditor: run.throughEditor,
42782
43237
  announced: announced !== undefined || run.announcedPid !== undefined,
42783
- couldAnnounce: run.projectPath !== null && existsSync15(join22(run.projectPath, RUNTIME_AUTOLOAD.path)),
43238
+ couldAnnounce: run.projectPath !== null && existsSync17(join23(run.projectPath, RUNTIME_AUTOLOAD.path)),
42784
43239
  endedHere: typeof run.endedHere === "string",
42785
43240
  pickedUp: run.pickedUp === true
42786
43241
  }));
@@ -42788,6 +43243,9 @@ class GodotServer {
42788
43243
  if (run.pickedUpPlaying === true) {
42789
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.");
42790
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
+ }
42791
43249
  if (run.consoleLost === true) {
42792
43250
  const back = this.dapClient?.isConnected() === true;
42793
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.`);
@@ -42803,8 +43261,9 @@ class GodotServer {
42803
43261
  pid: run.pid ?? announced ?? run.announcedPid ?? null,
42804
43262
  errors: run.log.count("error"),
42805
43263
  warnings: run.log.count("warning"),
42806
- clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
43264
+ clean: cleanVerdict(run),
42807
43265
  consoleLost: run.consoleLost === true ? true : undefined,
43266
+ errorsUnread: errorsUnread(run) ? true : undefined,
42808
43267
  ...hold,
42809
43268
  endedUnwatched: run.endedUnwatched === true ? true : undefined,
42810
43269
  endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
@@ -42839,6 +43298,7 @@ class GodotServer {
42839
43298
  if (elsewhere !== null) {
42840
43299
  return elsewhere;
42841
43300
  }
43301
+ await this.tieThePlayedGame(stopped);
42842
43302
  this.drainEditorOutput(stopped);
42843
43303
  this.drainTranscript(stopped);
42844
43304
  const wasRunning = await this.runStillGoing(stopped);
@@ -42847,7 +43307,7 @@ class GodotServer {
42847
43307
  const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
42848
43308
  this.logDebug("Stopping the running game");
42849
43309
  const ending = await this.endActiveGame("editor_run stop", wasRunning);
42850
- const refused = ending === "refused";
43310
+ const refused = ending === "refused" || ending === "unreached";
42851
43311
  const ended = children === null || refused ? null : endChildrenAmong(children);
42852
43312
  if (wasRunning && !refused) {
42853
43313
  for (const pid of new Set([endedPid, announcedPid])) {
@@ -42862,7 +43322,7 @@ class GodotServer {
42862
43322
  endedPid,
42863
43323
  throughEditor: stopped.throughEditor,
42864
43324
  exitSignal: stopped.exitSignal,
42865
- errors: stopped.log.count("error")
43325
+ clean: cleanVerdict(stopped)
42866
43326
  });
42867
43327
  return this.jsonTextResponse({
42868
43328
  stopped: verdict.stopped,
@@ -42876,6 +43336,7 @@ class GodotServer {
42876
43336
  errors: stopped.log.count("error"),
42877
43337
  warnings: stopped.log.count("warning"),
42878
43338
  clean: verdict.clean,
43339
+ errorsUnread: errorsUnread(stopped) ? true : undefined,
42879
43340
  note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
42880
43341
  entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
42881
43342
  });
@@ -43088,7 +43549,12 @@ class GodotServer {
43088
43549
  const stillAtMissingPaths = atMissingPaths.filter((name) => !dropped.includes(name));
43089
43550
  const reloading = readNonEmptyString(args, "reloadScript");
43090
43551
  let reloaded = {};
43091
- 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) {
43092
43558
  try {
43093
43559
  const answer = asParams(await this.godotBridge.invokeTool("reload_script", { ...args, scriptPath: reloading }));
43094
43560
  const methods = readArray(answer, "methods");
@@ -43096,29 +43562,36 @@ class GodotServer {
43096
43562
  const found = readArray(answer, field);
43097
43563
  return found ? { [field]: found.map(String) } : {};
43098
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 };
43099
43568
  const failed = answer["failed"];
43100
43569
  if (typeof failed === "number" && failed !== 0) {
43101
43570
  reloaded = {
43571
+ ...spelling,
43102
43572
  ...listed("heldBefore"),
43103
43573
  ...listed("heldConstantsBefore"),
43104
43574
  problem: failedReloadNote({
43105
- script: reloading,
43575
+ script,
43106
43576
  code: failed,
43107
43577
  codeName: readString(answer, "failedAs") ?? "",
43108
43578
  heldBefore: listed("heldBefore")["heldBefore"] ?? [],
43109
43579
  heldAfter: methods?.map(String) ?? [],
43110
43580
  constantsBefore: listed("heldConstantsBefore")["heldConstantsBefore"] ?? [],
43111
43581
  constantsAfter: listed("constants")["constants"] ?? [],
43112
- 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") ?? ""
43113
43585
  })
43114
43586
  };
43115
43587
  } else {
43116
43588
  reloaded = methods ? {
43589
+ ...spelling,
43117
43590
  methods: methods.map(String),
43118
43591
  ...listed("heldBefore"),
43119
43592
  ...listed("constants"),
43120
43593
  ...listed("heldConstantsBefore")
43121
- } : { 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` };
43122
43595
  }
43123
43596
  } catch (error) {
43124
43597
  reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
@@ -43140,7 +43613,7 @@ class GodotServer {
43140
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.");
43141
43614
  }
43142
43615
  if (dependents !== null && dependents.notReloaded.length > 0) {
43143
- 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));
43144
43617
  }
43145
43618
  for (const group of uidsDuplicatedOnDisk) {
43146
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.`);
@@ -43155,6 +43628,7 @@ class GodotServer {
43155
43628
  stillWorking: busy,
43156
43629
  waitedMs: Date.now() - started,
43157
43630
  waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
43631
+ reloadedAs: reloaded.as,
43158
43632
  heldBeforeReload: reloaded.heldBefore,
43159
43633
  reloadedMethods: reloaded.methods,
43160
43634
  heldConstantsBeforeReload: reloaded.heldConstantsBefore,
@@ -43181,7 +43655,8 @@ class GodotServer {
43181
43655
  if (tool) {
43182
43656
  notReloaded.push({
43183
43657
  scriptPath,
43184
- 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
43185
43660
  });
43186
43661
  continue;
43187
43662
  }
@@ -43189,9 +43664,10 @@ class GodotServer {
43189
43664
  const answer = asParams(await this.godotBridge.invokeTool("reload_script", { scriptPath }));
43190
43665
  const failed = answer["failed"];
43191
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";
43192
43668
  notReloaded.push({
43193
43669
  scriptPath,
43194
- 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}`
43195
43671
  });
43196
43672
  } else if (readArray(answer, "methods") === undefined) {
43197
43673
  notReloaded.push({
@@ -43263,6 +43739,35 @@ class GodotServer {
43263
43739
  }
43264
43740
  };
43265
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
+ }
43266
43771
  async alsoSayWhatTheEditorCannotSee(answered, args) {
43267
43772
  const first = answered.content[0];
43268
43773
  if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
@@ -43391,7 +43896,7 @@ class GodotServer {
43391
43896
  async ownRunOnItsWay(projectPath, budgetMs) {
43392
43897
  const run = await this.currentRun();
43393
43898
  const project = projectPath ?? run?.projectPath ?? null;
43394
- 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)) {
43395
43900
  return null;
43396
43901
  }
43397
43902
  const already = runtimesAnnounced().running;
@@ -43415,11 +43920,15 @@ class GodotServer {
43415
43920
  const seconds = Math.round((Date.now() - run.startedAt) / 1000);
43416
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.`;
43417
43922
  }
43418
- 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)), {
43419
43924
  budgetMs: Math.min(budgetMs, remaining),
43420
43925
  accept: (one) => this.isTheGameOf(this.activeProcess, one, project),
43421
43926
  giveUp: () => this.activeProcess !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
43422
43927
  });
43928
+ const endpoint = await this.theRunsAmong(run, fresh);
43929
+ if (fresh.length > 0 && endpoint === null) {
43930
+ return null;
43931
+ }
43423
43932
  if (endpoint !== null) {
43424
43933
  const tiedMeanwhile = () => run.announcedPid !== undefined;
43425
43934
  if (!tiedMeanwhile()) {
@@ -43471,7 +43980,7 @@ class GodotServer {
43471
43980
  }
43472
43981
  }
43473
43982
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
43474
- const screenshotPath = screenshotDir ? join22(screenshotDir, "capture.png") : null;
43983
+ const screenshotPath = screenshotDir ? join23(screenshotDir, "capture.png") : null;
43475
43984
  try {
43476
43985
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
43477
43986
  if (!reply.ok && reply.requestId !== undefined) {
@@ -43512,7 +44021,7 @@ class GodotServer {
43512
44021
  type: "text",
43513
44022
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
43514
44023
  },
43515
- { type: "image", data: readFileSync19(screenshotPath).toString("base64"), mimeType: "image/png" }
44024
+ { type: "image", data: readFileSync20(screenshotPath).toString("base64"), mimeType: "image/png" }
43516
44025
  ]
43517
44026
  };
43518
44027
  } finally {
@@ -43612,7 +44121,7 @@ async function runGodotServer() {
43612
44121
  const server = new GodotServer;
43613
44122
  await server.run();
43614
44123
  }
43615
- 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/";
43616
44125
  var init_server3 = __esm(() => {
43617
44126
  init_mcp();
43618
44127
  init_stdio2();
@@ -43628,6 +44137,7 @@ var init_server3 = __esm(() => {
43628
44137
  init_dap_client();
43629
44138
  init_desktop();
43630
44139
  init_editor_log();
44140
+ init_engine_run();
43631
44141
  init_errors();
43632
44142
  init_game_log();
43633
44143
  init_godot_bridge();
@@ -43652,8 +44162,8 @@ var init_server3 = __esm(() => {
43652
44162
  init_setup();
43653
44163
  init_tool_definitions();
43654
44164
  init_update_check();
43655
- run5 = promisify6(execFile6);
43656
- __dirname2 = dirname15(fileURLToPath5(import.meta.url));
44165
+ run4 = promisify5(execFile5);
44166
+ __dirname2 = dirname16(fileURLToPath5(import.meta.url));
43657
44167
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
43658
44168
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
43659
44169
  VALUE_NAMES = {
@@ -43702,8 +44212,8 @@ var init_server3 = __esm(() => {
43702
44212
  init_bridge_announce();
43703
44213
  init_errors();
43704
44214
  init_godot_path();
43705
- import { existsSync as existsSync16 } from "node:fs";
43706
- 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";
43707
44217
 
43708
44218
  // src/harnesses.ts
43709
44219
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
@@ -44602,8 +45112,8 @@ init_setup();
44602
45112
  // src/skill.ts
44603
45113
  init_setup();
44604
45114
  init_tool_definitions();
44605
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
44606
- import { basename as basename3, dirname as dirname5, join as join11 } 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";
44607
45117
 
44608
45118
  // src/tool-reference.ts
44609
45119
  init_headless_operations();
@@ -44736,9 +45246,10 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
44736
45246
  - Read \`editor_output\` after every run. It returns the engine's errors and warnings as entries
44737
45247
  with their backtraces, and a \`clean\` verdict, so a run that printed an error is one call away
44738
45248
  from being known. For a run the editor plays, the errors come from the game through the runtime
44739
- addon, so a played game without the addon is judged on its prints alone and \`clean\` there says
44740
- less. For a long run it also names a transcript file: read that for output and this
44741
- 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
44742
45253
  list, or a timeout; a bench between prints looks exactly like a bench that died to everything else.
44743
45254
  \`endedBy\` says whether gdharness ended it, and \`running\` is asked of the operating system
44744
45255
  whenever there is a process to ask about: every run started through \`editor_run\`, and every
@@ -44918,9 +45429,9 @@ function everySkillDirectory(projectPath, harnesses) {
44918
45429
  function removeSkill(directories, projectPath) {
44919
45430
  const removed = [];
44920
45431
  for (const directory of directories) {
44921
- if (existsSync10(directory)) {
45432
+ if (existsSync11(directory)) {
44922
45433
  rmSync6(directory, { recursive: true, force: true });
44923
- pruneEmpty(dirname5(directory), projectPath);
45434
+ pruneEmpty(dirname6(directory), projectPath);
44924
45435
  removed.push(directory);
44925
45436
  }
44926
45437
  }
@@ -44934,17 +45445,17 @@ function pruneEmpty(directory, projectPath) {
44934
45445
  return;
44935
45446
  }
44936
45447
  rmSync6(path, { recursive: true, force: true });
44937
- path = dirname5(path);
45448
+ path = dirname6(path);
44938
45449
  }
44939
45450
  }
44940
45451
  function writeSkill(directories, version) {
44941
45452
  const files = skillFiles(version);
44942
45453
  return directories.map((directory) => {
44943
- const replaced = existsSync10(directory);
45454
+ const replaced = existsSync11(directory);
44944
45455
  rmSync6(directory, { recursive: true, force: true });
44945
45456
  for (const [name, contents] of files) {
44946
45457
  const path = join11(directory, name);
44947
- mkdirSync5(dirname5(path), { recursive: true });
45458
+ mkdirSync5(dirname6(path), { recursive: true });
44948
45459
  writeFileSync7(path, contents, "utf8");
44949
45460
  }
44950
45461
  return { path: directory, replaced };
@@ -44952,18 +45463,18 @@ function writeSkill(directories, version) {
44952
45463
  }
44953
45464
 
44954
45465
  // src/version.ts
44955
- import { readFileSync as readFileSync9 } from "node:fs";
44956
- import { dirname as dirname6, join as join12 } from "node:path";
45466
+ import { readFileSync as readFileSync10 } from "node:fs";
45467
+ import { dirname as dirname7, join as join12 } from "node:path";
44957
45468
  import { fileURLToPath as fileURLToPath2 } from "node:url";
44958
45469
  function getLocalVersion() {
44959
- const currentDirectory = dirname6(fileURLToPath2(import.meta.url));
45470
+ const currentDirectory = dirname7(fileURLToPath2(import.meta.url));
44960
45471
  const packageCandidates = [
44961
45472
  join12(currentDirectory, "..", "package.json"),
44962
45473
  join12(currentDirectory, "..", "..", "package.json")
44963
45474
  ];
44964
45475
  for (const packagePath of packageCandidates) {
44965
45476
  try {
44966
- const value = JSON.parse(readFileSync9(packagePath, "utf-8"));
45477
+ const value = JSON.parse(readFileSync10(packagePath, "utf-8"));
44967
45478
  if (typeof value === "object" && value !== null && "name" in value && value.name === "gdharness" && "version" in value && typeof value.version === "string") {
44968
45479
  return value.version;
44969
45480
  }
@@ -44981,7 +45492,7 @@ class UsageError extends Refusal {
44981
45492
  function projectArgument(at) {
44982
45493
  const named = args.slice(at).find((value) => !value.startsWith("--"));
44983
45494
  const projectPath = resolve6(named ?? ".");
44984
- if (!existsSync16(join23(projectPath, "project.godot"))) {
45495
+ if (!existsSync18(join24(projectPath, "project.godot"))) {
44985
45496
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
44986
45497
  }
44987
45498
  return projectPath;
@@ -45020,7 +45531,7 @@ async function registerRuntime(godot, projectPath) {
45020
45531
  return;
45021
45532
  }
45022
45533
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
45023
- if (!existsSync16(join23(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
45534
+ if (!existsSync18(join24(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
45024
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}`);
45025
45536
  }
45026
45537
  }
@@ -45152,7 +45663,7 @@ async function setup() {
45152
45663
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
45153
45664
  }
45154
45665
  if (!args.includes("--no-skill")) {
45155
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync16);
45666
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync18);
45156
45667
  for (const written of writeSkill(directories, getLocalVersion())) {
45157
45668
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
45158
45669
  }
@@ -45246,7 +45757,7 @@ async function upgrade() {
45246
45757
  moved.push(took.from);
45247
45758
  }
45248
45759
  }
45249
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync16), version)) {
45760
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync18), version)) {
45250
45761
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
45251
45762
  }
45252
45763
  console.log(`