gdharness 1.2.26 → 1.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -3363,17 +3363,17 @@ import {
3363
3363
  closeSync as closeSync2,
3364
3364
  constants,
3365
3365
  copyFileSync,
3366
- existsSync as existsSync17,
3366
+ existsSync as existsSync18,
3367
3367
  fstatSync,
3368
3368
  mkdirSync as mkdirSync14,
3369
3369
  openSync as openSync3,
3370
- readdirSync as readdirSync9,
3371
- readFileSync as readFileSync21,
3370
+ readdirSync as readdirSync10,
3371
+ readFileSync as readFileSync22,
3372
3372
  readSync as readSync2,
3373
3373
  rmdirSync as rmdirSync2,
3374
3374
  statSync as statSync9
3375
3375
  } from "node:fs";
3376
- import { basename as basename5, dirname as dirname16, extname as extname3, isAbsolute as isAbsolute3, join as join23, normalize as normalize2, relative as relative4 } from "node:path";
3376
+ import { basename as basename6, dirname as dirname17, extname as extname3, isAbsolute as isAbsolute4, join as join24, normalize as normalize2, relative as relative4 } from "node:path";
3377
3377
  import { setTimeout as delay4 } from "node:timers/promises";
3378
3378
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3379
3379
  import { promisify as promisify5 } from "node:util";
@@ -27929,6 +27929,18 @@ class GodotDAPClient {
27929
27929
  lines: Array.from(lines).sort((a, b) => a - b)
27930
27930
  }));
27931
27931
  }
27932
+ forgetBreakpoint(scriptPath, line) {
27933
+ const key = fileKey(scriptPath);
27934
+ const lines = this.breakpoints.get(key);
27935
+ if (lines === undefined) {
27936
+ return;
27937
+ }
27938
+ lines.delete(line);
27939
+ if (lines.size === 0) {
27940
+ this.breakpoints.delete(key);
27941
+ this.spelled.delete(key);
27942
+ }
27943
+ }
27932
27944
  holdBreakpoints(held) {
27933
27945
  for (const { scriptPath, lines } of held) {
27934
27946
  const key = fileKey(scriptPath);
@@ -28707,6 +28719,22 @@ function howItEnded(run) {
28707
28719
  return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}`;
28708
28720
  }
28709
28721
 
28722
+ // src/godot-variant.ts
28723
+ var FLAG_64 = 1 << 16;
28724
+ var MOST_MESSAGE_BYTES = (8 << 20) + 4;
28725
+
28726
+ // src/game-debugger.ts
28727
+ var PAUSE_MS = 30000;
28728
+ var STEP_MS = 1e4;
28729
+ var READ_MS = 5000;
28730
+ var REPLIES = new Set([
28731
+ "debug_enter",
28732
+ "debug_exit",
28733
+ "stack_dump",
28734
+ "stack_frame_vars",
28735
+ "stack_frame_var"
28736
+ ]);
28737
+
28710
28738
  // src/godot-bridge.ts
28711
28739
  import { randomUUID } from "node:crypto";
28712
28740
  import { EventEmitter } from "node:events";
@@ -29489,33 +29517,114 @@ class GodotLocator {
29489
29517
  }
29490
29518
 
29491
29519
  // src/headless.ts
29492
- import { existsSync as existsSync11, readFileSync as readFileSync13, realpathSync as realpathSync2, writeFileSync as writeFileSync9 } from "node:fs";
29493
- import { dirname as dirname8, join as join14 } from "node:path";
29520
+ import { existsSync as existsSync12, readFileSync as readFileSync14, realpathSync as realpathSync2, writeFileSync as writeFileSync9 } from "node:fs";
29521
+ import { dirname as dirname9, join as join15 } from "node:path";
29522
+
29523
+ // src/library-copies.ts
29524
+ import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync12, renameSync, unlinkSync as unlinkSync2 } from "node:fs";
29525
+ import { basename as basename2, dirname as dirname8, isAbsolute as isAbsolute3, join as join12 } from "node:path";
29526
+ function freeLibraryCopies(projectPath, platform = process.platform) {
29527
+ const setAside = [];
29528
+ const swept = [];
29529
+ if (platform !== "win32") {
29530
+ return { setAside, swept };
29531
+ }
29532
+ for (const library of windowsLibraries(projectPath)) {
29533
+ const folder = dirname8(library);
29534
+ const copy = `~${basename2(library)}`;
29535
+ if (existsSync9(join12(folder, copy))) {
29536
+ const aside = join12(folder, `${copy}.aside-${process.pid}-${Date.now()}`);
29537
+ try {
29538
+ renameSync(join12(folder, copy), aside);
29539
+ setAside.push(aside);
29540
+ } catch {}
29541
+ }
29542
+ let names;
29543
+ try {
29544
+ names = readdirSync4(folder);
29545
+ } catch {
29546
+ continue;
29547
+ }
29548
+ for (const name of names) {
29549
+ if (name.length > copy.length && name.toLowerCase().startsWith(copy.toLowerCase())) {
29550
+ try {
29551
+ unlinkSync2(join12(folder, name));
29552
+ swept.push(join12(folder, name));
29553
+ } catch {}
29554
+ }
29555
+ }
29556
+ }
29557
+ return { setAside, swept };
29558
+ }
29559
+ function windowsLibraries(projectPath) {
29560
+ const listed = readOrNull(join12(projectPath, ".godot", "extension_list.cfg"));
29561
+ if (listed === null) {
29562
+ return [];
29563
+ }
29564
+ const libraries = new Set;
29565
+ for (const line of listed.split(/\r?\n/)) {
29566
+ const manifest = resolveResource(projectPath, projectPath, line.trim());
29567
+ const text = manifest === null ? null : readOrNull(manifest);
29568
+ if (manifest === null || text === null) {
29569
+ continue;
29570
+ }
29571
+ let inLibraries = false;
29572
+ for (const row of text.split(/\r?\n/)) {
29573
+ const section = /^\s*\[([^\]]+)\]/.exec(row)?.[1];
29574
+ if (section !== undefined) {
29575
+ inLibraries = section.trim() === "libraries";
29576
+ continue;
29577
+ }
29578
+ const named = inLibraries ? /^\s*windows[\w.]*\s*=\s*"([^"]+)"/.exec(row)?.[1] : undefined;
29579
+ const library = named === undefined ? null : resolveResource(projectPath, dirname8(manifest), named);
29580
+ if (library !== null) {
29581
+ libraries.add(library);
29582
+ }
29583
+ }
29584
+ }
29585
+ return [...libraries];
29586
+ }
29587
+ function resolveResource(projectPath, relativeTo, path) {
29588
+ if (path === "") {
29589
+ return null;
29590
+ }
29591
+ if (path.startsWith("res://")) {
29592
+ return join12(projectPath, path.slice("res://".length));
29593
+ }
29594
+ return isAbsolute3(path) ? path : join12(relativeTo, path);
29595
+ }
29596
+ function readOrNull(path) {
29597
+ try {
29598
+ return readFileSync12(path, "utf8");
29599
+ } catch {
29600
+ return null;
29601
+ }
29602
+ }
29494
29603
 
29495
29604
  // src/reimport.ts
29496
29605
  import { createHash as createHash2 } from "node:crypto";
29497
- import { existsSync as existsSync9, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync5, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
29498
- import { basename as basename2, join as join12 } from "node:path";
29606
+ import { existsSync as existsSync10, readFileSync as readFileSync13, rmSync as rmSync5, statSync as statSync5, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
29607
+ import { basename as basename3, join as join13 } from "node:path";
29499
29608
  function forceNextImport(projectPath, resourcePath) {
29500
29609
  const relative = resourcePath.replace(/^res:\/\//, "");
29501
- const sidecar = join12(projectPath, `${relative}.import`);
29502
- if (!existsSync9(sidecar)) {
29610
+ const sidecar = join13(projectPath, `${relative}.import`);
29611
+ if (!existsSync10(sidecar)) {
29503
29612
  return;
29504
29613
  }
29505
29614
  const { atime, mtimeMs } = statSync5(sidecar);
29506
- writeFileSync8(sidecar, readFileSync12(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
29615
+ writeFileSync8(sidecar, readFileSync13(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
29507
29616
  utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
29508
29617
  rmSync5(importRecord(projectPath, resourcePath), { force: true });
29509
29618
  }
29510
29619
  function importRecord(projectPath, resourcePath) {
29511
29620
  const digest = createHash2("md5").update(resourcePath).digest("hex");
29512
- return join12(projectPath, ".godot", "imported", `${basename2(resourcePath)}-${digest}.md5`);
29621
+ return join13(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
29513
29622
  }
29514
29623
 
29515
29624
  // src/scratch.ts
29516
- import { existsSync as existsSync10, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync6 } from "node:fs";
29625
+ import { existsSync as existsSync11, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync6 } from "node:fs";
29517
29626
  import { tmpdir as tmpdir4 } from "node:os";
29518
- import { join as join13 } from "node:path";
29627
+ import { join as join14 } from "node:path";
29519
29628
  import process7 from "node:process";
29520
29629
  var SWEPT = { recursive: true, force: true };
29521
29630
  var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
@@ -29530,12 +29639,12 @@ var KINDS = [
29530
29639
  "tests"
29531
29640
  ];
29532
29641
  function scratchDirectory(kind) {
29533
- return mkdtempSync(join13(tmpdir4(), `gdharness-${kind}-${process7.pid}-`));
29642
+ return mkdtempSync(join14(tmpdir4(), `gdharness-${kind}-${process7.pid}-`));
29534
29643
  }
29535
29644
  function sweepAbandonedScratch(directory = tmpdir4()) {
29536
29645
  let entries;
29537
29646
  try {
29538
- entries = readdirSync4(directory);
29647
+ entries = readdirSync5(directory);
29539
29648
  } catch {
29540
29649
  return;
29541
29650
  }
@@ -29546,7 +29655,7 @@ function sweepAbandonedScratch(directory = tmpdir4()) {
29546
29655
  continue;
29547
29656
  }
29548
29657
  try {
29549
- remove(join13(directory, entry));
29658
+ remove(join14(directory, entry));
29550
29659
  } catch {}
29551
29660
  }
29552
29661
  }
@@ -29584,7 +29693,7 @@ function tryAgain(path, attempt) {
29584
29693
  lastTryOnExit();
29585
29694
  }
29586
29695
  const after = AGAIN_AFTER_MS[attempt];
29587
- if (after === undefined || !existsSync10(path)) {
29696
+ if (after === undefined || !existsSync11(path)) {
29588
29697
  waiting.delete(path);
29589
29698
  if (after === undefined) {
29590
29699
  atExit.add(path);
@@ -29675,7 +29784,7 @@ function refusalsIn(log) {
29675
29784
  }
29676
29785
  function scriptFault(problems, script) {
29677
29786
  const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
29678
- const directories = [...new Set([spelled(dirname8(script)), spelled(resolvedOr(dirname8(script)))])];
29787
+ const directories = [...new Set([spelled(dirname9(script)), spelled(resolvedOr(dirname9(script)))])];
29679
29788
  return problems.find((entry) => {
29680
29789
  const at = entry.detail.find((line) => line.startsWith("at: "))?.replaceAll("\\", "/").toLowerCase();
29681
29790
  return entry.severity === "error" && at !== undefined && directories.some((directory) => at.includes(directory));
@@ -29700,7 +29809,7 @@ function librariesNotCopied(messages) {
29700
29809
  }
29701
29810
  function extensionRetryNote(libraries, stillNotLoaded = []) {
29702
29811
  const second = stillNotLoaded.length === 0 ? "so the import was run again and loaded the extension, and the messages are from that second run." : `so the import was run again, and it still could not load ${stillNotLoaded.join(" and ")}: this answer is from an import without ${stillNotLoaded.length === 1 ? "it" : "them"}, and its messages say what the engine hit.`;
29703
- return `The first import could not load ${libraries.join(" and ")}: on Windows an engine running as the editor ` + "loads a GDExtension from a ~ copy beside the library, and the editor open on this project holds its own " + "copy there, so the first engine started beside it cannot make one. That failed attempt moves the " + `editor's copy aside, ${second} The editor's copy is left beside the library as a ~RF….TMP file, which ` + "can be deleted once the editor has exited.";
29812
+ return `The first import could not load ${libraries.join(" and ")}: on Windows an engine running as the editor ` + "loads a GDExtension from a ~ copy beside the library, and the editor open on this project holds its own " + "copy there, so the first engine started beside it cannot make one. That failed attempt moves the " + `editor's copy aside, ${second} The editor's copy is left beside the library as a ~RF….TMP file, which ` + "the next import, export or editor this server starts on the project deletes once the editor has exited.";
29704
29813
  }
29705
29814
  async function runImport(godotPath, projectPath, options = {}) {
29706
29815
  const once = options.once ?? importOnce;
@@ -29708,6 +29817,7 @@ async function runImport(godotPath, projectPath, options = {}) {
29708
29817
  for (const target of options.forcing ?? []) {
29709
29818
  forceNextImport(projectPath, target);
29710
29819
  }
29820
+ freeLibraryCopies(projectPath);
29711
29821
  return once(godotPath, projectPath);
29712
29822
  };
29713
29823
  const first = await pass();
@@ -29731,7 +29841,7 @@ async function importOnce(godotPath, projectPath) {
29731
29841
  ran = await runEngine(godotPath, [
29732
29842
  "--headless",
29733
29843
  "--log-file",
29734
- join14(logDir, "engine.log"),
29844
+ join15(logDir, "engine.log"),
29735
29845
  "--path",
29736
29846
  projectPath,
29737
29847
  "--import"
@@ -29751,13 +29861,13 @@ async function importOnce(godotPath, projectPath) {
29751
29861
  }
29752
29862
  async function runOperation(engine, operation, params, projectPath) {
29753
29863
  const paramsDir = scratchDirectory("params");
29754
- const paramsFile = join14(paramsDir, `${operation}.json`);
29755
- const answerFile = join14(paramsDir, "answer.json");
29864
+ const paramsFile = join15(paramsDir, `${operation}.json`);
29865
+ const answerFile = join15(paramsDir, "answer.json");
29756
29866
  writeFileSync9(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
29757
29867
  const args = [
29758
29868
  "--headless",
29759
29869
  "--log-file",
29760
- join14(paramsDir, "engine.log"),
29870
+ join15(paramsDir, "engine.log"),
29761
29871
  "--path",
29762
29872
  projectPath,
29763
29873
  "--script",
@@ -29827,10 +29937,10 @@ async function runOperation(engine, operation, params, projectPath) {
29827
29937
  };
29828
29938
  }
29829
29939
  function answerIn(file) {
29830
- if (!existsSync11(file)) {
29940
+ if (!existsSync12(file)) {
29831
29941
  return { kind: "none" };
29832
29942
  }
29833
- const text = readFileSync13(file, "utf8");
29943
+ const text = readFileSync14(file, "utf8");
29834
29944
  try {
29835
29945
  const parsed = JSON.parse(text);
29836
29946
  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)}` };
@@ -30340,7 +30450,31 @@ function testProgressMessage(printed, elapsedMs) {
30340
30450
 
30341
30451
  // src/keeper-channel.ts
30342
30452
  import { connect, createServer as createServer2 } from "node:net";
30343
- function askTheKeeperToStop(address, withinMs) {
30453
+ var DEBUG_PREFIX = "debug ";
30454
+ async function askTheKeeperToStop(address, withinMs) {
30455
+ const answered = await askTheKeeper(address, "stop", withinMs);
30456
+ if (typeof answered !== "string") {
30457
+ return answered;
30458
+ }
30459
+ return answered === "signalled" || answered === "gone" ? answered : { unreached: `it answered ${JSON.stringify(answered)}` };
30460
+ }
30461
+ async function askTheKeepersDebugger(address, request, withinMs) {
30462
+ const answered = await askTheKeeper(address, `${DEBUG_PREFIX}${JSON.stringify(request)}`, withinMs);
30463
+ if (typeof answered !== "string") {
30464
+ return answered;
30465
+ }
30466
+ if (answered === "unknown") {
30467
+ return { unreached: "its keeper was started by a gdharness from before game debuggers, and has none" };
30468
+ }
30469
+ try {
30470
+ const parsed = JSON.parse(answered);
30471
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
30472
+ return parsed;
30473
+ }
30474
+ } catch {}
30475
+ return { unreached: `it answered ${JSON.stringify(answered.slice(0, 200))}` };
30476
+ }
30477
+ function askTheKeeper(address, line, withinMs) {
30344
30478
  return new Promise((resolve) => {
30345
30479
  let said = "";
30346
30480
  let settled = false;
@@ -30358,16 +30492,15 @@ function askTheKeeperToStop(address, withinMs) {
30358
30492
  }, withinMs);
30359
30493
  socket.setEncoding("utf8");
30360
30494
  socket.on("connect", () => {
30361
- socket.write(`stop
30495
+ socket.write(`${line}
30362
30496
  `);
30363
30497
  });
30364
30498
  socket.on("data", (chunk) => {
30365
30499
  said += chunk;
30366
- const line = said.split(`
30367
- `)[0] ?? "";
30368
30500
  if (said.includes(`
30369
30501
  `)) {
30370
- settle(line === "signalled" || line === "gone" ? line : { unreached: `it answered ${JSON.stringify(line)}` });
30502
+ settle(said.split(`
30503
+ `)[0] ?? "");
30371
30504
  }
30372
30505
  });
30373
30506
  socket.on("error", (error) => {
@@ -30380,20 +30513,20 @@ function askTheKeeperToStop(address, withinMs) {
30380
30513
  }
30381
30514
 
30382
30515
  // src/lifecycle-log.ts
30383
- import { appendFileSync, mkdirSync as mkdirSync8, renameSync, statSync as statSync6 } from "node:fs";
30384
- import { dirname as dirname9, join as join15 } from "node:path";
30516
+ import { appendFileSync, mkdirSync as mkdirSync8, renameSync as renameSync2, statSync as statSync6 } from "node:fs";
30517
+ import { dirname as dirname10, join as join16 } from "node:path";
30385
30518
  import process8 from "node:process";
30386
30519
  function lifecycleLogPath() {
30387
- return join15(runtimeDirectory(), "servers.log");
30520
+ return join16(runtimeDirectory(), "servers.log");
30388
30521
  }
30389
30522
  var LIFECYCLE_LOG_LIMIT = 1e6;
30390
30523
  function writeLifecycle(event, fields = {}) {
30391
30524
  const path = lifecycleLogPath();
30392
30525
  try {
30393
- mkdirSync8(dirname9(path), { recursive: true });
30526
+ mkdirSync8(dirname10(path), { recursive: true });
30394
30527
  if ((statSync6(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
30395
30528
  try {
30396
- renameSync(path, `${path}.1`);
30529
+ renameSync2(path, `${path}.1`);
30397
30530
  } catch {}
30398
30531
  }
30399
30532
  const line = {
@@ -30423,10 +30556,10 @@ function callsInFlight(calls, now = Date.now()) {
30423
30556
  }
30424
30557
 
30425
30558
  // src/lsp_client.ts
30426
- import { existsSync as existsSync12, realpathSync as realpathSync3 } from "node:fs";
30559
+ import { existsSync as existsSync13, realpathSync as realpathSync3 } from "node:fs";
30427
30560
  import { readFile, realpath } from "node:fs/promises";
30428
30561
  import { createConnection as createConnection3 } from "node:net";
30429
- import { dirname as dirname10, join as join16, resolve as resolve3 } from "node:path";
30562
+ import { dirname as dirname11, join as join17, resolve as resolve3 } from "node:path";
30430
30563
  import { setTimeout as delay3 } from "node:timers/promises";
30431
30564
  import { fileURLToPath, pathToFileURL } from "node:url";
30432
30565
  var DEFAULT_LSP_PORT = 6005;
@@ -30624,7 +30757,7 @@ class GodotLSPClient {
30624
30757
  if (this.initialized) {
30625
30758
  return;
30626
30759
  }
30627
- const rootPath = this.rootPath ?? dirname10(resolve3(filePath));
30760
+ const rootPath = this.rootPath ?? dirname11(resolve3(filePath));
30628
30761
  await this.initialize(rootPath);
30629
30762
  }
30630
30763
  async sendRequest(method, params, timeoutMs = this.timeouts.requestMs) {
@@ -31182,7 +31315,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
31182
31315
  } catch {
31183
31316
  throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
31184
31317
  }
31185
- if (!existsSync12(join16(projectPath, "project.godot"))) {
31318
+ if (!existsSync13(join17(projectPath, "project.godot"))) {
31186
31319
  throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
31187
31320
  }
31188
31321
  const contained = resolveWithinProject(projectPath, scriptPathValue);
@@ -31389,8 +31522,8 @@ function secondsFromClock(printed) {
31389
31522
  }
31390
31523
 
31391
31524
  // src/project-scan.ts
31392
- import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync14 } from "node:fs";
31393
- import { extname, join as join17 } from "node:path";
31525
+ import { existsSync as existsSync14, readdirSync as readdirSync6, readFileSync as readFileSync15 } from "node:fs";
31526
+ import { extname, join as join18 } from "node:path";
31394
31527
  var SKIPPED = new Set(["node_modules"]);
31395
31528
  function skipped(name) {
31396
31529
  return name.startsWith(".") || SKIPPED.has(name);
@@ -31400,10 +31533,10 @@ function steppedOver(directory) {
31400
31533
  return gdignored(directory) || installedByUs(directory);
31401
31534
  }
31402
31535
  function gdignored(directory) {
31403
- return existsSync13(join17(directory, ".gdignore"));
31536
+ return existsSync14(join18(directory, ".gdignore"));
31404
31537
  }
31405
31538
  function installedByUs(directory) {
31406
- return existsSync13(join17(directory, VERSION_MARKER));
31539
+ return existsSync14(join18(directory, VERSION_MARKER));
31407
31540
  }
31408
31541
  var SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
31409
31542
  var ASSET_EXTENSIONS = new Set([
@@ -31442,12 +31575,12 @@ function projectStructure(projectPath) {
31442
31575
  if (steppedOver(directory)) {
31443
31576
  return;
31444
31577
  }
31445
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
31578
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
31446
31579
  if (skipped(entry.name)) {
31447
31580
  continue;
31448
31581
  }
31449
31582
  if (entry.isDirectory()) {
31450
- visit(join17(directory, entry.name));
31583
+ visit(join18(directory, entry.name));
31451
31584
  } else if (entry.isFile()) {
31452
31585
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
31453
31586
  if (SCENE_EXTENSIONS.has(extension)) {
@@ -31471,13 +31604,13 @@ function projectFiles(projectPath, extensions) {
31471
31604
  if (steppedOver(directory)) {
31472
31605
  return;
31473
31606
  }
31474
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
31607
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
31475
31608
  if (skipped(entry.name)) {
31476
31609
  continue;
31477
31610
  }
31478
31611
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
31479
31612
  if (entry.isDirectory()) {
31480
- visit(join17(directory, entry.name), spelled);
31613
+ visit(join18(directory, entry.name), spelled);
31481
31614
  } else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
31482
31615
  found.push(spelled);
31483
31616
  }
@@ -31493,20 +31626,20 @@ function scriptsWithoutUid(projectPath) {
31493
31626
  if (steppedOver(directory)) {
31494
31627
  return;
31495
31628
  }
31496
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
31629
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
31497
31630
  if (skipped(entry.name)) {
31498
31631
  continue;
31499
31632
  }
31500
31633
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
31501
31634
  if (entry.isDirectory()) {
31502
- visit(join17(directory, entry.name), spelled);
31635
+ visit(join18(directory, entry.name), spelled);
31503
31636
  continue;
31504
31637
  }
31505
31638
  if (!entry.isFile()) {
31506
31639
  continue;
31507
31640
  }
31508
31641
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
31509
- if (CARRIES_A_UID.has(extension) && !existsSync13(join17(directory, `${entry.name}.uid`))) {
31642
+ if (CARRIES_A_UID.has(extension) && !existsSync14(join18(directory, `${entry.name}.uid`))) {
31510
31643
  found.push(spelled);
31511
31644
  }
31512
31645
  }
@@ -31534,11 +31667,11 @@ function projectTexts(projectPath) {
31534
31667
  if (steppedOver(directory)) {
31535
31668
  return;
31536
31669
  }
31537
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
31670
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
31538
31671
  if (skipped(entry.name)) {
31539
31672
  continue;
31540
31673
  }
31541
- const absolute = join17(directory, entry.name);
31674
+ const absolute = join18(directory, entry.name);
31542
31675
  const spelled = `${prefix}${entry.name}`;
31543
31676
  if (entry.isDirectory()) {
31544
31677
  visit(absolute, `${spelled}/`);
@@ -31548,7 +31681,7 @@ function projectTexts(projectPath) {
31548
31681
  if (!entry.isFile() || BINARY_EXTENSIONS.has(extension)) {
31549
31682
  continue;
31550
31683
  }
31551
- const bytes = readFileSync14(absolute);
31684
+ const bytes = readFileSync15(absolute);
31552
31685
  if (!isBinary(bytes)) {
31553
31686
  found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
31554
31687
  }
@@ -31564,11 +31697,11 @@ function ignoredTexts(projectPath) {
31564
31697
  return;
31565
31698
  }
31566
31699
  const under = ignored || gdignored(directory);
31567
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
31700
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
31568
31701
  if (skipped(entry.name)) {
31569
31702
  continue;
31570
31703
  }
31571
- const absolute = join17(directory, entry.name);
31704
+ const absolute = join18(directory, entry.name);
31572
31705
  const spelled = `${prefix}${entry.name}`;
31573
31706
  if (entry.isDirectory()) {
31574
31707
  visit(absolute, `${spelled}/`, under);
@@ -31578,7 +31711,7 @@ function ignoredTexts(projectPath) {
31578
31711
  if (!under || !entry.isFile() || BINARY_EXTENSIONS.has(extension) || entry.name === ".gdignore") {
31579
31712
  continue;
31580
31713
  }
31581
- const bytes = readFileSync14(absolute);
31714
+ const bytes = readFileSync15(absolute);
31582
31715
  if (!isBinary(bytes)) {
31583
31716
  found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
31584
31717
  }
@@ -31601,14 +31734,14 @@ function searchProject(projectPath, options) {
31601
31734
  if (steppedOver(directory)) {
31602
31735
  return;
31603
31736
  }
31604
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
31737
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
31605
31738
  if (full()) {
31606
31739
  return;
31607
31740
  }
31608
31741
  if (skipped(entry.name)) {
31609
31742
  continue;
31610
31743
  }
31611
- const entryPath = join17(directory, entry.name);
31744
+ const entryPath = join18(directory, entry.name);
31612
31745
  if (entry.isDirectory()) {
31613
31746
  visit(entryPath);
31614
31747
  continue;
@@ -31617,7 +31750,7 @@ function searchProject(projectPath, options) {
31617
31750
  if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
31618
31751
  continue;
31619
31752
  }
31620
- const bytes = readFileSync14(entryPath);
31753
+ const bytes = readFileSync15(entryPath);
31621
31754
  if (extensions === null && isBinary(bytes)) {
31622
31755
  continue;
31623
31756
  }
@@ -31779,8 +31912,8 @@ function setupPromptHandlers(server) {
31779
31912
  }
31780
31913
 
31781
31914
  // src/rename.ts
31782
- import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync15, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
31783
- import { dirname as dirname11, posix as posix2 } from "node:path";
31915
+ import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync16, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync10 } from "node:fs";
31916
+ import { dirname as dirname12, posix as posix2 } from "node:path";
31784
31917
  class Script {
31785
31918
  path;
31786
31919
  text;
@@ -32502,26 +32635,26 @@ function rewritesOf(plan, texts, move) {
32502
32635
  }
32503
32636
  function applyRewrites(rewrites) {
32504
32637
  for (const rewrite of rewrites) {
32505
- const now = readFileSync15(rewrite.absolute, "utf8");
32638
+ const now = readFileSync16(rewrite.absolute, "utf8");
32506
32639
  if (now !== rewrite.before) {
32507
32640
  throw new Error(`${rewrite.path} changed on disk while the rename was being worked out; nothing was written`);
32508
32641
  }
32509
- if (rewrite.writeTo !== rewrite.absolute && existsSync14(rewrite.writeTo)) {
32642
+ if (rewrite.writeTo !== rewrite.absolute && existsSync15(rewrite.writeTo)) {
32510
32643
  throw new Error(`${rewrite.writeTo} already exists; nothing was written`);
32511
32644
  }
32512
32645
  }
32513
32646
  const done = [];
32514
32647
  try {
32515
32648
  for (const rewrite of rewrites) {
32516
- mkdirSync9(dirname11(rewrite.writeTo), { recursive: true });
32649
+ mkdirSync9(dirname12(rewrite.writeTo), { recursive: true });
32517
32650
  const staging = `${rewrite.writeTo}.gdharness-rename`;
32518
32651
  writeFileSync10(staging, rewrite.after, "utf8");
32519
- renameSync2(staging, rewrite.writeTo);
32652
+ renameSync3(staging, rewrite.writeTo);
32520
32653
  let movedUid = false;
32521
32654
  if (rewrite.writeTo !== rewrite.absolute) {
32522
- unlinkSync2(rewrite.absolute);
32523
- if (existsSync14(`${rewrite.absolute}.uid`)) {
32524
- renameSync2(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
32655
+ unlinkSync3(rewrite.absolute);
32656
+ if (existsSync15(`${rewrite.absolute}.uid`)) {
32657
+ renameSync3(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
32525
32658
  movedUid = true;
32526
32659
  }
32527
32660
  }
@@ -32532,9 +32665,9 @@ function applyRewrites(rewrites) {
32532
32665
  try {
32533
32666
  writeFileSync10(rewrite.absolute, rewrite.before, "utf8");
32534
32667
  if (rewrite.writeTo !== rewrite.absolute) {
32535
- unlinkSync2(rewrite.writeTo);
32668
+ unlinkSync3(rewrite.writeTo);
32536
32669
  if (movedUid) {
32537
- renameSync2(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
32670
+ renameSync3(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
32538
32671
  }
32539
32672
  }
32540
32673
  } catch {}
@@ -32544,11 +32677,11 @@ function applyRewrites(rewrites) {
32544
32677
  }
32545
32678
 
32546
32679
  // src/rename-symbol.ts
32547
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, readdirSync as readdirSync6, realpathSync as realpathSync5, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
32548
- import { basename as basename3, dirname as dirname12, join as join18, relative as relative3, resolve as resolve5 } from "node:path";
32680
+ import { existsSync as existsSync16, mkdirSync as mkdirSync10, readdirSync as readdirSync7, realpathSync as realpathSync5, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
32681
+ import { basename as basename4, dirname as dirname13, join as join19, relative as relative3, resolve as resolve5 } from "node:path";
32549
32682
 
32550
32683
  // src/resources.ts
32551
- import { readFileSync as readFileSync16, realpathSync as realpathSync4 } from "node:fs";
32684
+ import { readFileSync as readFileSync17, realpathSync as realpathSync4 } from "node:fs";
32552
32685
  import { extname as extname2, resolve as resolve4 } from "node:path";
32553
32686
  var STATIC_RESOURCES = [
32554
32687
  {
@@ -32779,7 +32912,7 @@ function readResourceText(uri, getProjectPath) {
32779
32912
  const parsedUri = parseGodotUri(uri);
32780
32913
  if (parsedUri.kind === "project-info") {
32781
32914
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
32782
- const rawProject = readFileSync16(projectFilePath, "utf-8");
32915
+ const rawProject = readFileSync17(projectFilePath, "utf-8");
32783
32916
  const parsedProject = parseProjectGodot(rawProject);
32784
32917
  return {
32785
32918
  mimeType: "application/json",
@@ -32788,7 +32921,7 @@ function readResourceText(uri, getProjectPath) {
32788
32921
  }
32789
32922
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
32790
32923
  ensureAllowedExtension(parsedUri.kind, filePath);
32791
- const text = readFileSync16(filePath, "utf-8");
32924
+ const text = readFileSync17(filePath, "utf-8");
32792
32925
  return {
32793
32926
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
32794
32927
  text
@@ -32853,13 +32986,13 @@ function onDiskSpelling(path) {
32853
32986
  let at = resolve5(path);
32854
32987
  for (;; ) {
32855
32988
  try {
32856
- return join18(realpathSync5.native(at), ...missing.reverse());
32989
+ return join19(realpathSync5.native(at), ...missing.reverse());
32857
32990
  } catch {
32858
- const parent = dirname12(at);
32991
+ const parent = dirname13(at);
32859
32992
  if (parent === at) {
32860
32993
  return resolve5(path);
32861
32994
  }
32862
- missing.push(basename3(at));
32995
+ missing.push(basename4(at));
32863
32996
  at = parent;
32864
32997
  }
32865
32998
  }
@@ -32906,7 +33039,7 @@ async function renameSymbol(request, services) {
32906
33039
  reason: `newScriptPath must be a .gd path inside the project, and ${request.newScriptPath} is not.`
32907
33040
  };
32908
33041
  }
32909
- if (existsSync15(request.newScriptPath)) {
33042
+ if (existsSync16(request.newScriptPath)) {
32910
33043
  return { ok: false, reason: `${moveTo} already exists, so the script cannot be moved there.` };
32911
33044
  }
32912
33045
  }
@@ -33082,7 +33215,7 @@ async function renameSymbol(request, services) {
33082
33215
  scripts,
33083
33216
  from: declaringPath,
33084
33217
  to: moveTo,
33085
- exists: (path) => existsSync15(`${projectPath}/${path.slice("res://".length)}`)
33218
+ exists: (path) => existsSync16(`${projectPath}/${path.slice("res://".length)}`)
33086
33219
  }, plan);
33087
33220
  }
33088
33221
  plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? null : declaringPath);
@@ -33283,15 +33416,15 @@ ${mention.kind}`;
33283
33416
  var LEFT_ALONE_ANSWER_LINES = 300;
33284
33417
  var detailWrites = 0;
33285
33418
  function writeLeftAloneDetail(projectPath, symbol, detail) {
33286
- const directory = join18(projectPath, ".godot", "gdharness-renames");
33419
+ const directory = join19(projectPath, ".godot", "gdharness-renames");
33287
33420
  mkdirSync10(directory, { recursive: true });
33288
33421
  detailWrites += 1;
33289
- const path = join18(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
33422
+ const path = join19(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
33290
33423
  writeFileSync11(path, `${JSON.stringify(detail, null, 2)}
33291
33424
  `, "utf8");
33292
- const kept = readdirSync6(directory).filter((name) => name.endsWith(".json")).sort().reverse();
33425
+ const kept = readdirSync7(directory).filter((name) => name.endsWith(".json")).sort().reverse();
33293
33426
  for (const old of kept.slice(10)) {
33294
- rmSync7(join18(directory, old), { force: true });
33427
+ rmSync7(join19(directory, old), { force: true });
33295
33428
  }
33296
33429
  return path;
33297
33430
  }
@@ -33314,15 +33447,15 @@ function summaryOf(edits, mentions, rewrites, left) {
33314
33447
  }
33315
33448
 
33316
33449
  // src/restart-note.ts
33317
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync17, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "node:fs";
33318
- import { dirname as dirname13, join as join19 } from "node:path";
33450
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync18, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "node:fs";
33451
+ import { dirname as dirname14, join as join20 } from "node:path";
33319
33452
  function restartNotePath(projectPath) {
33320
- return join19(projectPath, ".godot", "gdharness-restart.json");
33453
+ return join20(projectPath, ".godot", "gdharness-restart.json");
33321
33454
  }
33322
33455
  function noteRestartBegun(note) {
33323
33456
  const path = restartNotePath(note.projectPath);
33324
33457
  try {
33325
- mkdirSync11(dirname13(path), { recursive: true });
33458
+ mkdirSync11(dirname14(path), { recursive: true });
33326
33459
  writeFileSync12(path, `${JSON.stringify(note, null, 2)}
33327
33460
  `, "utf8");
33328
33461
  } catch (error) {
@@ -33331,7 +33464,7 @@ function noteRestartBegun(note) {
33331
33464
  }
33332
33465
  function restartOwed(projectPath) {
33333
33466
  try {
33334
- const parsed = JSON.parse(readFileSync17(restartNotePath(projectPath), "utf8"));
33467
+ const parsed = JSON.parse(readFileSync18(restartNotePath(projectPath), "utf8"));
33335
33468
  if (typeof parsed !== "object" || parsed === null) {
33336
33469
  return null;
33337
33470
  }
@@ -33355,12 +33488,12 @@ function restartSettled(projectPath) {
33355
33488
  // src/run-record.ts
33356
33489
  import { execFile as execFile4 } from "node:child_process";
33357
33490
  import { createHash as createHash3 } from "node:crypto";
33358
- import { mkdirSync as mkdirSync12, openSync as openSync2, readdirSync as readdirSync7, readFileSync as readFileSync18, realpathSync as realpathSync6, rmSync as rmSync10, statSync as statSync7 } from "node:fs";
33359
- import { basename as basename4, dirname as dirname14, join as join20, resolve as resolve6 } from "node:path";
33491
+ import { mkdirSync as mkdirSync12, openSync as openSync2, readdirSync as readdirSync8, readFileSync as readFileSync19, realpathSync as realpathSync6, rmSync as rmSync10, statSync as statSync7 } from "node:fs";
33492
+ import { basename as basename5, dirname as dirname15, join as join21, resolve as resolve6 } from "node:path";
33360
33493
  import { promisify as promisify4 } from "node:util";
33361
33494
 
33362
33495
  // src/whole-file.ts
33363
- import { renameSync as renameSync3, rmSync as rmSync9, writeFileSync as writeFileSync13 } from "node:fs";
33496
+ import { renameSync as renameSync4, rmSync as rmSync9, writeFileSync as writeFileSync13 } from "node:fs";
33364
33497
  var HELD_FILE_MS = 1000;
33365
33498
  var HELD_FILE_RETRY_MS = 10;
33366
33499
  var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
@@ -33374,7 +33507,7 @@ function writeWhole(path, text) {
33374
33507
  const until = Date.now() + HELD_FILE_MS;
33375
33508
  for (;; ) {
33376
33509
  try {
33377
- renameSync3(partial, path);
33510
+ renameSync4(partial, path);
33378
33511
  return true;
33379
33512
  } catch (error) {
33380
33513
  const code = error.code ?? "";
@@ -33392,20 +33525,20 @@ function writeWhole(path, text) {
33392
33525
  // src/run-record.ts
33393
33526
  var execFileAsync = promisify4(execFile4);
33394
33527
  function runsDirectory() {
33395
- return join20(runtimeDirectory(), "runs");
33528
+ return join21(runtimeDirectory(), "runs");
33396
33529
  }
33397
33530
  function realAsFarAsItExists(path) {
33398
33531
  const missing = [];
33399
33532
  let at = path;
33400
33533
  for (;; ) {
33401
33534
  try {
33402
- return join20(realpathSync6.native(at), ...missing);
33535
+ return join21(realpathSync6.native(at), ...missing);
33403
33536
  } catch {
33404
- const parent = dirname14(at);
33537
+ const parent = dirname15(at);
33405
33538
  if (parent === at) {
33406
33539
  return path;
33407
33540
  }
33408
- missing.unshift(basename4(at));
33541
+ missing.unshift(basename5(at));
33409
33542
  at = parent;
33410
33543
  }
33411
33544
  }
@@ -33416,10 +33549,10 @@ function projectKey(projectPath) {
33416
33549
  return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
33417
33550
  }
33418
33551
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
33419
- return join20(directory, "runs", `note-${projectKey(projectPath)}.json`);
33552
+ return join21(directory, "runs", `note-${projectKey(projectPath)}.json`);
33420
33553
  }
33421
33554
  function latestRunPath(servedBy, directory = runtimeDirectory()) {
33422
- return join20(directory, "runs", `latest-${projectKey(servedBy)}.json`);
33555
+ return join21(directory, "runs", `latest-${projectKey(servedBy)}.json`);
33423
33556
  }
33424
33557
  function writeLatestRun(servedBy, latest) {
33425
33558
  try {
@@ -33429,7 +33562,7 @@ function writeLatestRun(servedBy, latest) {
33429
33562
  }
33430
33563
  function readLatestRun(servedBy, directory = runtimeDirectory()) {
33431
33564
  try {
33432
- const fields = JSON.parse(readFileSync18(latestRunPath(servedBy, directory), "utf8"));
33565
+ const fields = JSON.parse(readFileSync19(latestRunPath(servedBy, directory), "utf8"));
33433
33566
  if (typeof fields !== "object" || fields === null) {
33434
33567
  return null;
33435
33568
  }
@@ -33445,17 +33578,17 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
33445
33578
  var SHARED_NOTE = "run.json";
33446
33579
  function openTranscript(startedAt) {
33447
33580
  mkdirSync12(runsDirectory(), { recursive: true });
33448
- const path = join20(runsDirectory(), `run-${startedAt}.log`);
33581
+ const path = join21(runsDirectory(), `run-${startedAt}.log`);
33449
33582
  return { path, fd: openSync2(path, "a") };
33450
33583
  }
33451
33584
  function profilePath(startedAt) {
33452
- return join20(runsDirectory(), `run-${startedAt}.profile.json`);
33585
+ return join21(runsDirectory(), `run-${startedAt}.profile.json`);
33453
33586
  }
33454
33587
  function noteIsAbout(note, playing) {
33455
33588
  return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
33456
33589
  }
33457
33590
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
33458
- return join20(directory, "runs", `editor-${projectKey(projectPath)}.json`);
33591
+ return join21(directory, "runs", `editor-${projectKey(projectPath)}.json`);
33459
33592
  }
33460
33593
  function writeEditorRunNote(note) {
33461
33594
  mkdirSync12(runsDirectory(), { recursive: true });
@@ -33466,7 +33599,7 @@ function writeEditorRunNote(note) {
33466
33599
  }
33467
33600
  function readEditorRunNote(projectPath) {
33468
33601
  for (const directory of runtimeDirectories()) {
33469
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join20(directory, "runs", "editor-run.json"));
33602
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join21(directory, "runs", "editor-run.json"));
33470
33603
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
33471
33604
  return note;
33472
33605
  }
@@ -33476,7 +33609,7 @@ function readEditorRunNote(projectPath) {
33476
33609
  function editorNoteAt(path) {
33477
33610
  let parsed;
33478
33611
  try {
33479
- parsed = JSON.parse(readFileSync18(path, "utf8"));
33612
+ parsed = JSON.parse(readFileSync19(path, "utf8"));
33480
33613
  } catch {
33481
33614
  return null;
33482
33615
  }
@@ -33500,7 +33633,7 @@ function readRunRecord(projectPath) {
33500
33633
  if (own !== null) {
33501
33634
  return own;
33502
33635
  }
33503
- const shared = recordAt(join20(directory, "runs", SHARED_NOTE));
33636
+ const shared = recordAt(join21(directory, "runs", SHARED_NOTE));
33504
33637
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
33505
33638
  return shared;
33506
33639
  }
@@ -33512,13 +33645,13 @@ function everyRunRecord() {
33512
33645
  for (const directory of runtimeDirectories()) {
33513
33646
  let entries;
33514
33647
  try {
33515
- entries = readdirSync7(join20(directory, "runs"));
33648
+ entries = readdirSync8(join21(directory, "runs"));
33516
33649
  } catch {
33517
33650
  continue;
33518
33651
  }
33519
33652
  for (const entry of entries) {
33520
33653
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
33521
- const record = recordAt(join20(directory, "runs", entry));
33654
+ const record = recordAt(join21(directory, "runs", entry));
33522
33655
  if (record !== null) {
33523
33656
  found.push(record);
33524
33657
  }
@@ -33530,7 +33663,7 @@ function everyRunRecord() {
33530
33663
  function recordAt(path) {
33531
33664
  let parsed;
33532
33665
  try {
33533
- parsed = JSON.parse(readFileSync18(path, "utf8"));
33666
+ parsed = JSON.parse(readFileSync19(path, "utf8"));
33534
33667
  } catch {
33535
33668
  return null;
33536
33669
  }
@@ -33583,7 +33716,7 @@ async function runningAs(pid) {
33583
33716
  const began = await startedAt(pid, null);
33584
33717
  const when = began === null ? {} : { startedAt: began };
33585
33718
  if (process.platform === "linux") {
33586
- const raw = readFileSync18(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
33719
+ const raw = readFileSync19(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
33587
33720
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
33588
33721
  }
33589
33722
  const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
@@ -33690,7 +33823,7 @@ function whyNotTheRun(record, running, needed) {
33690
33823
  if (after > SAME_RUN_WINDOW_MS) {
33691
33824
  return `that process started ${Math.round(after / 1000)} seconds after the run had`;
33692
33825
  }
33693
- const engine = record.command === undefined ? null : basename4(record.command);
33826
+ const engine = record.command === undefined ? null : basename5(record.command);
33694
33827
  const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
33695
33828
  const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
33696
33829
  if (wanted !== null && !said.includes(wanted)) {
@@ -33729,7 +33862,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
33729
33862
  }
33730
33863
  function clearRunRecord(projectPath) {
33731
33864
  for (const directory of runtimeDirectories()) {
33732
- const shared = join20(directory, "runs", SHARED_NOTE);
33865
+ const shared = join21(directory, "runs", SHARED_NOTE);
33733
33866
  const sharedRecord = recordAt(shared);
33734
33867
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
33735
33868
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
@@ -33754,7 +33887,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
33754
33887
  ]));
33755
33888
  let entries;
33756
33889
  try {
33757
- entries = readdirSync7(directory);
33890
+ entries = readdirSync8(directory);
33758
33891
  } catch {
33759
33892
  return;
33760
33893
  }
@@ -33763,7 +33896,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
33763
33896
  if (!transcript && !entry.endsWith(".partial")) {
33764
33897
  continue;
33765
33898
  }
33766
- const path = join20(directory, entry);
33899
+ const path = join21(directory, entry);
33767
33900
  if (kept.has(path) || ageMs(path, now) < keepMs) {
33768
33901
  continue;
33769
33902
  }
@@ -33774,7 +33907,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
33774
33907
  }
33775
33908
 
33776
33909
  // src/script-origins.ts
33777
- import { join as join21 } from "node:path";
33910
+ import { join as join22 } from "node:path";
33778
33911
  var UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
33779
33912
  function unresolvedClassIn(message) {
33780
33913
  return UNRESOLVED.exec(message)?.[1] ?? null;
@@ -33830,7 +33963,7 @@ function checkWithEngine(godotPath, projectPath) {
33830
33963
  const ran = await runEngine(godotPath, [
33831
33964
  "--headless",
33832
33965
  "--log-file",
33833
- join21(logDir, "engine.log"),
33966
+ join22(logDir, "engine.log"),
33834
33967
  "--path",
33835
33968
  projectPath,
33836
33969
  "--check-only",
@@ -33867,16 +34000,10 @@ function withoutListed(entries, errors) {
33867
34000
  }
33868
34001
 
33869
34002
  // src/script-profile.ts
33870
- import { readFileSync as readFileSync19 } from "node:fs";
33871
-
33872
- // src/godot-variant.ts
33873
- var FLAG_64 = 1 << 16;
33874
- var MOST_MESSAGE_BYTES = (8 << 20) + 4;
33875
-
33876
- // src/script-profile.ts
34003
+ import { readFileSync as readFileSync20 } from "node:fs";
33877
34004
  function readProfileFile(path) {
33878
34005
  try {
33879
- const parsed = JSON.parse(readFileSync19(path, "utf8"));
34006
+ const parsed = JSON.parse(readFileSync20(path, "utf8"));
33880
34007
  return typeof parsed === "object" && typeof parsed.connected === "boolean" ? parsed : null;
33881
34008
  } catch {
33882
34009
  return null;
@@ -34992,7 +35119,7 @@ var TOOL_SPECS = [
34992
35119
  type: "array",
34993
35120
  items: { type: "string" },
34994
35121
  ops: ["start", "check"],
34995
- description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here, so a flag the engine reads goes to the game instead, and the answer says so under argsNote. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
35122
+ description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here, so a flag the engine reads goes to the game instead, and the answer says so under argsNote. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so its debugger is the keeper holding it rather than the editor; the debug_* tools answer for it all the same.`
34996
35123
  },
34997
35124
  engineArgs: {
34998
35125
  type: "array",
@@ -35009,7 +35136,7 @@ var TOOL_SPECS = [
35009
35136
  silent: {
35010
35137
  type: "boolean",
35011
35138
  ops: ["start"],
35012
- description: "start: run the game with no sound out of the machine, by starting the engine with --audio-driver Dummy: every stream still plays and every playback position still advances, so what is playing can still be asked. The editor takes no engine flags for a game it plays, so a silent windowed run is started by this server, and the debug_* tools do not answer for it. A headless run, check's included, is silent already. Default false, or true when GDHARNESS_SILENT is set in the server's environment to anything but 0 or false; silent: false then plays through the editor as before."
35139
+ description: "start: run the game with no sound out of the machine, by starting the engine with --audio-driver Dummy: every stream still plays and every playback position still advances, so what is playing can still be asked. The editor takes no engine flags for a game it plays, so a silent windowed run is started by this server, whose keeper is its debugger rather than the editor. A headless run, check's included, is silent already. Default false, or true when GDHARNESS_SILENT is set in the server's environment to anything but 0 or false; silent: false then plays through the editor as before."
35013
35140
  },
35014
35141
  savesIn: {
35015
35142
  type: "string",
@@ -35335,12 +35462,12 @@ var TOOL_SPECS = [
35335
35462
  },
35336
35463
  metrics: {
35337
35464
  effect: "read",
35338
- summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked. memory_static is what the engine allocated on the CPU side, in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are what the renderer has on the GPU, in bytes, which memory_static does not count",
35465
+ summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. physics_steps_per_second is the physics steps taken a second over those frames, fewer than physics_ticks_per_second when a frame needs more than max_physics_steps_per_frame or the editor's Game view slows it. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked. memory_static is what the engine allocated on the CPU side, in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are what the renderer has on the GPU, in bytes, which memory_static does not count",
35339
35466
  requires: []
35340
35467
  },
35341
35468
  accessibility: {
35342
35469
  effect: "read",
35343
- summary: `what the game tells a screen reader, read through UI Automation as a Windows screen reader reads it, under windows: one tree per window, each element with its role as UI Automation names it (text for static text, button, check_box, group for a control holding others), its name, its value where it has one, its bounds in the window's pixels, the actions a screen reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the one holding the focus. The name is what is read aloud, so an empty one is read as nothing; static text takes it from DisplayServer.accessibility_update_set_value, not from a name set on it, and the answer says under _note how many text elements on screen have none. With no control focused, the engine reports the first node under the root as focused, which in a run with the runtime is GdharnessRuntime. Needs a windowed run on Windows started with engineArgs ["--accessibility", "always"] or with the project's accessibility/general/accessibility_support at Always Active, since the engine builds the tree only when asked; reading it takes a second or two, starting PowerShell inside the game so it reaches the game's own desktop`,
35470
+ summary: "what the game tells a screen reader, read through UI Automation as a Windows screen reader reads it, under windows: one tree per window, each element with its role (text for static text, button, check_box, group for a control holding others), name, value, description, placeholder, bounds in the window's pixels, the actions a reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the one with the focus. node is the path of the node that made the element; an element a node made as a part of itself has part_of instead, and part: announcement marks the text a window speaks announcements through. An empty name is read as nothing: static text takes it from accessibility_update_set_value, and _note counts the text elements on screen with none. tooltip is the node's tooltip_text, which a Windows reader is not told. With no control focused, the engine reports the first node under the root, GdharnessRuntime, as focused. Needs a windowed run on Windows started with engineArgs [\"--accessibility\", \"always\"] or accessibility_support at Always Active, since the engine builds the tree only when asked; reading takes a second or two",
35344
35471
  requires: []
35345
35472
  }
35346
35473
  },
@@ -35690,7 +35817,7 @@ var TOOL_SPECS = [
35690
35817
  {
35691
35818
  name: "debug_breakpoint",
35692
35819
  title: "Breakpoints",
35693
- description: "Sets or removes a breakpoint through the editor's debug adapter. Needs the editor, not a running game: set them first, then editor_run, and the game stops where you asked. The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor running an older addon is said so under breakpointsAtRisk, here and in editor_status.",
35820
+ description: "Sets or removes a breakpoint. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it at once (runningGame says so). The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor running an older addon is said so under breakpointsAtRisk, here and in editor_status.",
35694
35821
  parameters: {
35695
35822
  projectPath: PROJECT_PATH2,
35696
35823
  scriptPath: SCRIPT_PATH,
@@ -35705,23 +35832,33 @@ var TOOL_SPECS = [
35705
35832
  {
35706
35833
  name: "debug_control",
35707
35834
  title: "Step the debugger",
35708
- 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.",
35835
+ description: "Pauses, continues or steps the debugged game. 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. A game the editor plays is moved through the editor's debug adapter, which cannot pause or step out: it answers a pause by reporting the game stopped and leaving it running, and implements no stepOut, so hold such a game with a breakpoint and step over or into from there. A game this server started itself has its keeper as its debugger, which does both: pause breaks into the game wherever it is, a script spinning in a loop included, which is how a game hung on its first frame is asked where it is.",
35709
35836
  parameters: {},
35710
35837
  requires: [],
35711
35838
  operations: {
35839
+ pause: {
35840
+ effect: "additive",
35841
+ summary: "stop a game this server started where it is, answering with the stack. The game takes it between frames, and in a script every 2048 lines it runs, so a loop that sleeps between few lines can take seconds; the call waits thirty, and past that answers paused false with the request still standing. Stopped between frames, where no script runs, the game is stepped on to the next line of script it runs",
35842
+ requires: []
35843
+ },
35712
35844
  continue: { effect: "additive", summary: "resume after a breakpoint", requires: [] },
35713
35845
  step_over: { effect: "additive", summary: "run the current line", requires: [] },
35714
35846
  step_into: {
35715
35847
  effect: "additive",
35716
35848
  summary: "run the current line, stopping inside whatever it calls",
35717
35849
  requires: []
35850
+ },
35851
+ step_out: {
35852
+ effect: "additive",
35853
+ summary: "run to the end of the current function and stop in its caller; a game this server started only",
35854
+ requires: []
35718
35855
  }
35719
35856
  }
35720
35857
  },
35721
35858
  {
35722
35859
  name: "debug_state",
35723
35860
  title: "Debugger state",
35724
- 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.",
35861
+ description: "Where the debugged game is stopped: the stack trace, what is in scope at a frame with the values, or the debugger's console output so far, its last thousand lines, with any earlier ones counted under notShown. For a game this server started, its keeper's debugger answers: each frame names its script as the project spells it under script and on this machine under path, and held says which thread stopped and why. For a game the editor plays, the editor's debug adapter answers. 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.",
35725
35862
  parameters: {
35726
35863
  frameId: {
35727
35864
  type: "number",
@@ -35811,7 +35948,7 @@ function serverInstructions() {
35811
35948
  "Call editor_status first: it says whether an editor is connected and whether its addon matches this server.",
35812
35949
  "Start the game with editor_run start, never by spawning an engine, and read editor_output after every run:",
35813
35950
  "its clean verdict and its error entries are how a run is judged, and editor_run wait is how to wait for one.",
35814
- "The runtime_* tools work on the running game and the debug_* tools on the editor's debugger.",
35951
+ "The runtime_* tools work on the running game and the debug_* tools on its debugger.",
35815
35952
  "A refusal names what would have worked, so read it before calling again."
35816
35953
  ].join(" ");
35817
35954
  }
@@ -35821,16 +35958,16 @@ import { spawnSync } from "node:child_process";
35821
35958
  import { createHash as createHash4 } from "node:crypto";
35822
35959
  import {
35823
35960
  cpSync,
35824
- existsSync as existsSync16,
35961
+ existsSync as existsSync17,
35825
35962
  mkdirSync as mkdirSync13,
35826
- readdirSync as readdirSync8,
35827
- readFileSync as readFileSync20,
35963
+ readdirSync as readdirSync9,
35964
+ readFileSync as readFileSync21,
35828
35965
  rmdirSync,
35829
35966
  rmSync as rmSync11,
35830
35967
  statSync as statSync8,
35831
35968
  writeFileSync as writeFileSync14
35832
35969
  } from "node:fs";
35833
- import { dirname as dirname15, join as join22 } from "node:path";
35970
+ import { dirname as dirname16, join as join23 } from "node:path";
35834
35971
  import { fileURLToPath as fileURLToPath3 } from "node:url";
35835
35972
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
35836
35973
  var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
@@ -35849,25 +35986,25 @@ function runtimeAddonDigest(from = shippedAddonsDirectory()) {
35849
35986
  function addonsDigest(names, from) {
35850
35987
  const hash = createHash4("sha256");
35851
35988
  const walk = (directory, relative) => {
35852
- for (const name of readdirSync8(directory).sort()) {
35989
+ for (const name of readdirSync9(directory).sort()) {
35853
35990
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
35854
35991
  continue;
35855
35992
  }
35856
- const path = join22(directory, name);
35993
+ const path = join23(directory, name);
35857
35994
  const inside = `${relative}/${name}`;
35858
35995
  if (statSync8(path).isDirectory()) {
35859
35996
  walk(path, inside);
35860
35997
  } else {
35861
35998
  hash.update(`${inside}
35862
35999
  `);
35863
- hash.update(readFileSync20(path));
36000
+ hash.update(readFileSync21(path));
35864
36001
  hash.update(`
35865
36002
  `);
35866
36003
  }
35867
36004
  }
35868
36005
  };
35869
36006
  for (const name of names) {
35870
- walk(join22(from, name), name);
36007
+ walk(join23(from, name), name);
35871
36008
  }
35872
36009
  return hash.digest("hex");
35873
36010
  }
@@ -35900,22 +36037,22 @@ function installedRuntimeDigest(projectPath) {
35900
36037
  return digestMarker(projectPath, RUNTIME_ADDON);
35901
36038
  }
35902
36039
  function digestMarker(projectPath, addon) {
35903
- const marker = join22(projectPath, "addons", addon, DIGEST_MARKER);
35904
- if (!existsSync16(marker)) {
36040
+ const marker = join23(projectPath, "addons", addon, DIGEST_MARKER);
36041
+ if (!existsSync17(marker)) {
35905
36042
  return null;
35906
36043
  }
35907
- const said = readFileSync20(marker, "utf8").trim();
36044
+ const said = readFileSync21(marker, "utf8").trim();
35908
36045
  return said === "" ? null : said;
35909
36046
  }
35910
36047
  function shippedAddonsDirectory() {
35911
- return join22(dirname15(fileURLToPath3(import.meta.url)), "godot", "addons");
36048
+ return join23(dirname16(fileURLToPath3(import.meta.url)), "godot", "addons");
35912
36049
  }
35913
36050
  function installedAddonVersion(projectPath) {
35914
- const marker = join22(projectPath, "addons", ADDONS[0], VERSION_MARKER);
35915
- if (!existsSync16(marker)) {
36051
+ const marker = join23(projectPath, "addons", ADDONS[0], VERSION_MARKER);
36052
+ if (!existsSync17(marker)) {
35916
36053
  return null;
35917
36054
  }
35918
- const said = readFileSync20(marker, "utf8").trim();
36055
+ const said = readFileSync21(marker, "utf8").trim();
35919
36056
  return said === "" ? null : said;
35920
36057
  }
35921
36058
  function loaderAmong(projectPath, named) {
@@ -35926,7 +36063,7 @@ function loaderAmong(projectPath, named) {
35926
36063
  }
35927
36064
  let source = null;
35928
36065
  try {
35929
- source = readFileSync20(join22(projectPath, path), "utf8");
36066
+ source = readFileSync21(join23(projectPath, path), "utf8");
35930
36067
  } catch {}
35931
36068
  if (source?.includes(RUNTIME_AUTOLOAD.path)) {
35932
36069
  return { name: entry, path, exists: true };
@@ -35968,10 +36105,10 @@ function enabledPlugins(settings) {
35968
36105
  function inspectProject(projectPath) {
35969
36106
  const problems = [];
35970
36107
  const addons = ADDONS.map((name) => {
35971
- const directory = join22(projectPath, "addons", name);
35972
- const marker = join22(directory, VERSION_MARKER);
35973
- const installed = existsSync16(directory);
35974
- const version = existsSync16(marker) ? readFileSync20(marker, "utf8").trim() : null;
36108
+ const directory = join23(projectPath, "addons", name);
36109
+ const marker = join23(directory, VERSION_MARKER);
36110
+ const installed = existsSync17(directory);
36111
+ const version = existsSync17(marker) ? readFileSync21(marker, "utf8").trim() : null;
35975
36112
  const current = version === SERVER_VERSION;
35976
36113
  if (!installed) {
35977
36114
  problems.push(`addons/${name} is not installed; run gdharness setup`);
@@ -35980,7 +36117,7 @@ function inspectProject(projectPath) {
35980
36117
  }
35981
36118
  return { name, installed, version, current };
35982
36119
  });
35983
- const settings = parseProjectGodot(readFileSync20(join22(projectPath, "project.godot"), "utf8"));
36120
+ const settings = parseProjectGodot(readFileSync21(join23(projectPath, "project.godot"), "utf8"));
35984
36121
  const pluginsEnabled = enabledPlugins(settings);
35985
36122
  for (const name of EDITOR_PLUGINS) {
35986
36123
  if (!pluginsEnabled.includes(name)) {
@@ -35996,7 +36133,7 @@ function inspectProject(projectPath) {
35996
36133
  const tracked = trackedByGit(projectPath, [...named.values()]);
35997
36134
  const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
35998
36135
  for (const [name, path] of named) {
35999
- if (!existsSync16(join22(projectPath, path))) {
36136
+ if (!existsSync17(join23(projectPath, path))) {
36000
36137
  if (!addonMissing(path)) {
36001
36138
  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`);
36002
36139
  }
@@ -36047,7 +36184,7 @@ function argumentShape(schema) {
36047
36184
  var UPDATE_NOTICE_EVERY = 500;
36048
36185
  var FEEDBACK_NOTICE_EVERY = 250;
36049
36186
  var run4 = promisify5(execFile5);
36050
- var __dirname2 = dirname16(fileURLToPath4(import.meta.url));
36187
+ var __dirname2 = dirname17(fileURLToPath4(import.meta.url));
36051
36188
  var PARENT_ASKED_AFTER_MS = 5000;
36052
36189
  function editorRestartTimeoutMs() {
36053
36190
  return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
@@ -36079,22 +36216,22 @@ function writtenFileRefusal(path, rule) {
36079
36216
  if (/^(res|user):\/\//.test(path)) {
36080
36217
  return `${named} ${path} is a Godot path; ${what} is saved outside the project, at an absolute path on this machine.`;
36081
36218
  }
36082
- if (!isAbsolute3(path)) {
36219
+ if (!isAbsolute4(path)) {
36083
36220
  return `${named} ${path} is not absolute; name the whole path, so where it is written does not depend on this server's directory.`;
36084
36221
  }
36085
36222
  if (!rule.extensions.includes(extname3(path).toLowerCase())) {
36086
36223
  return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
36087
36224
  }
36088
- const directory = realPathOr(dirname16(path));
36089
- if (!existsSync17(directory)) {
36090
- return `${named}'s directory ${dirname16(path)} does not exist; ${what} creates no directories.`;
36225
+ const directory = realPathOr(dirname17(path));
36226
+ if (!existsSync18(directory)) {
36227
+ return `${named}'s directory ${dirname17(path)} does not exist; ${what} creates no directories.`;
36091
36228
  }
36092
- const where = join23(directory, basename5(path));
36229
+ const where = join24(directory, basename6(path));
36093
36230
  const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
36094
36231
  if (inside !== undefined) {
36095
36232
  return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
36096
36233
  }
36097
- const taken = rule.writes(where).find((file) => existsSync17(file));
36234
+ const taken = rule.writes(where).find((file) => existsSync18(file));
36098
36235
  if (taken !== undefined) {
36099
36236
  return taken === where ? `${named} ${path} already exists; ${what} is not written over a file. Name a new one.` : `${named} ${path} would write ${taken}, which already exists; ${what} is not written over a file. Name a new one.`;
36100
36237
  }
@@ -36144,6 +36281,7 @@ function captureDetail(payload) {
36144
36281
  function patienceForFrames(frames, atLeast) {
36145
36282
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
36146
36283
  }
36284
+ var ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.";
36147
36285
  function longerWaitNote(tool, op, waitedMs) {
36148
36286
  const spec = TOOL_SPECS.find((one) => one.name === tool);
36149
36287
  if (spec !== undefined && argumentsOf(spec, op).includes("timeoutMs")) {
@@ -36152,6 +36290,31 @@ function longerWaitNote(tool, op, waitedMs) {
36152
36290
  const how = tool === "runtime_wait" && op === "frames" ? `the wait is counted from the number of frames, at ${SLOWEST_FRAME_RATE} a second on top of GDHARNESS_RUNTIME_TIMEOUT_MS: a game drawing frames more slowly than that is waited on in fewer frames at a time, and a game inside one long frame, such as a long _ready, answers no call however few frames are asked for` : "the wait is GDHARNESS_RUNTIME_TIMEOUT_MS, which is set in the environment the server is started with";
36153
36291
  return `${tool} ${op} takes no timeoutMs: it was given ${waitedMs}ms, and ${how}.`;
36154
36292
  }
36293
+ function keptGameRequest(tool, op, args) {
36294
+ if (tool === "debug_state") {
36295
+ if (op === "output") {
36296
+ return { op: "output" };
36297
+ }
36298
+ if (op === "variables") {
36299
+ const frame = args["frameId"];
36300
+ return { op: "variables", frame: typeof frame === "number" ? frame : 0 };
36301
+ }
36302
+ return { op: "stack" };
36303
+ }
36304
+ switch (op) {
36305
+ case "pause":
36306
+ return { op: "pause" };
36307
+ case "step_over":
36308
+ return { op: "resume", how: "next" };
36309
+ case "step_into":
36310
+ return { op: "resume", how: "step" };
36311
+ case "step_out":
36312
+ return { op: "resume", how: "out" };
36313
+ default:
36314
+ return { op: "resume", how: "continue" };
36315
+ }
36316
+ }
36317
+ var OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.";
36155
36318
  function projectSpelling(projectPath, absolutePath) {
36156
36319
  return `res://${relative4(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
36157
36320
  }
@@ -36239,6 +36402,7 @@ function stopWaitMs() {
36239
36402
  return waitFromEnv("GDHARNESS_STOP_WAIT_MS", 1e4);
36240
36403
  }
36241
36404
  var KEEPER_ANSWER_MS = 15000;
36405
+ var KEEPER_DEBUG_MS = PAUSE_MS + STEP_MS + 2 * READ_MS;
36242
36406
  var EXIT_REPORTED_WITHIN_MS = 2000;
36243
36407
  async function untilGone(pids, withinMs = stopWaitMs()) {
36244
36408
  const gone = () => pids.every((pid) => !alive(pid));
@@ -36588,6 +36752,7 @@ function notRunNote(notRun, failFast) {
36588
36752
  function exportAnswer(ending, asked, before, after) {
36589
36753
  const { log } = ending;
36590
36754
  const written = after !== null && after !== before;
36755
+ const notCopied = librariesNotCopied(log.all);
36591
36756
  const verdict = {
36592
36757
  exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
36593
36758
  ...asked,
@@ -36596,6 +36761,10 @@ function exportAnswer(ending, asked, before, after) {
36596
36761
  failure: ending.failure ?? undefined,
36597
36762
  errors: log.count("error"),
36598
36763
  warnings: log.count("warning"),
36764
+ ...notCopied.length === 0 ? {} : {
36765
+ librariesNotLoaded: notCopied,
36766
+ extensionNote: `The export could not load ${notCopied.join(" and ")}: on Windows an engine running as the editor, which an export is, loads a GDExtension from a ~ copy beside the library, and another engine on this project held that copy: it could not be moved aside before the export, or another engine made a new one in between. Export again once that engine has loaded or exited.`
36767
+ },
36599
36768
  ...foldedEntriesAnswer(log)
36600
36769
  };
36601
36770
  if (verdict.exported) {
@@ -36640,7 +36809,7 @@ var EDITOR_FREE_INSTEAD = {
36640
36809
  scene_tree: "runtime_inspect tree reads the nodes of a game that is running, and needs no editor; scene_tree reads a scene file through the editor"
36641
36810
  };
36642
36811
  function editorAddonMissing(projectPath) {
36643
- if (!existsSync17(join23(projectPath, "project.godot"))) {
36812
+ if (!existsSync18(join24(projectPath, "project.godot"))) {
36644
36813
  return null;
36645
36814
  }
36646
36815
  const report = inspectProject(projectPath);
@@ -36922,7 +37091,7 @@ function profileFrameFunctions(projectPath) {
36922
37091
  let projectGodot = null;
36923
37092
  if (projectPath !== null) {
36924
37093
  try {
36925
- projectGodot = readFileSync21(join23(projectPath, "project.godot"), "utf8");
37094
+ projectGodot = readFileSync22(join24(projectPath, "project.godot"), "utf8");
36926
37095
  } catch {}
36927
37096
  }
36928
37097
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -36948,7 +37117,7 @@ function projectSourceOf(projectPath) {
36948
37117
  let source = null;
36949
37118
  if (inside.ok) {
36950
37119
  try {
36951
- source = readFileSync21(inside.absolutePath, "utf8");
37120
+ source = readFileSync22(inside.absolutePath, "utf8");
36952
37121
  } catch {}
36953
37122
  }
36954
37123
  read.set(script, source);
@@ -37100,7 +37269,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
37100
37269
  var ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd";
37101
37270
  function gdUnitFailsOnScriptErrors(projectPath) {
37102
37271
  try {
37103
- return settingKeys(readFileSync21(join23(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
37272
+ return settingKeys(readFileSync22(join24(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
37104
37273
  } catch {
37105
37274
  return true;
37106
37275
  }
@@ -37115,13 +37284,13 @@ function uncaughtNote(uncaught) {
37115
37284
  }
37116
37285
  function elsewhereIn(projectPath, asked) {
37117
37286
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
37118
- if (wanted === "" || existsSync17(join23(projectPath, wanted))) {
37287
+ if (wanted === "" || existsSync18(join24(projectPath, wanted))) {
37119
37288
  return "";
37120
37289
  }
37121
37290
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
37122
37291
  let near = [];
37123
37292
  try {
37124
- near = readdirSync9(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
37293
+ near = readdirSync10(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
37125
37294
  } catch {
37126
37295
  return "";
37127
37296
  }
@@ -37212,17 +37381,17 @@ function isDirectory(path) {
37212
37381
  }
37213
37382
  }
37214
37383
  function scriptsUnder(directory, prefix) {
37215
- if (existsSync17(join23(directory, ".gdignore"))) {
37384
+ if (existsSync18(join24(directory, ".gdignore"))) {
37216
37385
  return [];
37217
37386
  }
37218
37387
  const found = [];
37219
- for (const entry of readdirSync9(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
37388
+ for (const entry of readdirSync10(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
37220
37389
  if (entry.name.startsWith(".")) {
37221
37390
  continue;
37222
37391
  }
37223
37392
  const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
37224
37393
  if (entry.isDirectory()) {
37225
- found.push(...scriptsUnder(join23(directory, entry.name), path));
37394
+ found.push(...scriptsUnder(join24(directory, entry.name), path));
37226
37395
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
37227
37396
  found.push(path);
37228
37397
  }
@@ -37230,7 +37399,7 @@ function scriptsUnder(directory, prefix) {
37230
37399
  return found;
37231
37400
  }
37232
37401
  function hasMainScene(projectFile) {
37233
- const scene = parseProjectGodot(readFileSync21(projectFile, "utf8"))["application"]?.["run/main_scene"];
37402
+ const scene = parseProjectGodot(readFileSync22(projectFile, "utf8"))["application"]?.["run/main_scene"];
37234
37403
  return typeof scene === "string" && scene !== "";
37235
37404
  }
37236
37405
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -37238,13 +37407,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
37238
37407
  return false;
37239
37408
  }
37240
37409
  try {
37241
- return readFileSync21(join23(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
37410
+ return readFileSync22(join24(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
37242
37411
  } catch {
37243
37412
  return false;
37244
37413
  }
37245
37414
  }
37246
37415
  function editorPlaysHeadless(projectFile) {
37247
- const runArgs = parseProjectGodot(readFileSync21(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
37416
+ const runArgs = parseProjectGodot(readFileSync22(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
37248
37417
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
37249
37418
  }
37250
37419
  function camelCased(params) {
@@ -37264,7 +37433,7 @@ var STATIC_LIST_HINT = { ttlMs: 5 * 60000, cacheScope: "private" };
37264
37433
  class GodotServer {
37265
37434
  stdio = null;
37266
37435
  locator = new GodotLocator;
37267
- operationsScript = join23(__dirname2, "godot", "operations", "godot_operations.gd");
37436
+ operationsScript = join24(__dirname2, "godot", "operations", "godot_operations.gd");
37268
37437
  godotBridge;
37269
37438
  tools = buildToolDefinitions();
37270
37439
  activeProcess = null;
@@ -37289,6 +37458,7 @@ class GodotServer {
37289
37458
  launchedDropped = null;
37290
37459
  closedHere = null;
37291
37460
  inFlight = new Map;
37461
+ callsOnGames = new Set;
37292
37462
  callsTaken = 0;
37293
37463
  clientNoted = null;
37294
37464
  parentCommand = null;
@@ -37520,14 +37690,14 @@ class GodotServer {
37520
37690
  }
37521
37691
  noteTheStart() {
37522
37692
  const cwd = process.cwd();
37523
- const mcpJson = join23(cwd, ".mcp.json");
37693
+ const mcpJson = join24(cwd, ".mcp.json");
37524
37694
  const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
37525
37695
  const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
37526
37696
  writeLifecycle("started", {
37527
37697
  argv: process.argv.slice(1),
37528
37698
  cwd,
37529
37699
  project: this.ownProject,
37530
- mcpJson: existsSync17(mcpJson) ? mcpJson : null
37700
+ mcpJson: existsSync18(mcpJson) ? mcpJson : null
37531
37701
  });
37532
37702
  if (predecessor !== null) {
37533
37703
  writeLifecycle("replaced", { project: this.ownProject, predecessor });
@@ -37668,7 +37838,7 @@ class GodotServer {
37668
37838
  return checked.response;
37669
37839
  }
37670
37840
  const named = args["projectPath"];
37671
- if (typeof named === "string" && existsSync17(join23(named, "project.godot"))) {
37841
+ if (typeof named === "string" && existsSync18(join24(named, "project.godot"))) {
37672
37842
  this.lastProjectPath = named;
37673
37843
  }
37674
37844
  this.updates.refresh();
@@ -38139,28 +38309,70 @@ class GodotServer {
38139
38309
  return this.createErrorResponse(located.reason, PATH_SOLUTIONS);
38140
38310
  }
38141
38311
  const session = this.dapHoldingBreakpointsOf(project.value.path);
38142
- const answered = await this.handleDAP(op === "set" ? "dap_set_breakpoint" : "dap_remove_breakpoint", {
38143
- ...args,
38144
- scriptPath: realPathOr(located.absolutePath)
38145
- });
38146
- if (answered.isError === true) {
38147
- return answered;
38312
+ const scriptPath = realPathOr(located.absolutePath);
38313
+ const line = readPositiveNumber(args, "line");
38314
+ if (line === undefined) {
38315
+ return this.createErrorResponse("line is required: the one-based line to stop on.");
38316
+ }
38317
+ const editor = this.godotBridge.isConnected();
38318
+ let answer;
38319
+ if (editor) {
38320
+ const answered = await this.handleDAP(op === "set" ? "dap_set_breakpoint" : "dap_remove_breakpoint", {
38321
+ ...args,
38322
+ scriptPath
38323
+ });
38324
+ if (answered.isError === true) {
38325
+ return answered;
38326
+ }
38327
+ answer = asParams(JSON.parse(answered.content[0]?.text ?? "{}"));
38328
+ } else {
38329
+ if (op === "set") {
38330
+ session.holdBreakpoints([{ scriptPath, lines: [line] }]);
38331
+ } else {
38332
+ session.forgetBreakpoint(scriptPath, line);
38333
+ }
38334
+ answer = { [op === "set" ? "set" : "removed"]: true };
38148
38335
  }
38149
38336
  const held = session.breakpointsHeld();
38150
38337
  writeBreakpointNote(project.value.path, held);
38338
+ const kept = this.keptRun();
38339
+ const told = kept === null ? null : await askTheKeepersDebugger(kept.keeper, {
38340
+ op: "breakpoint",
38341
+ script: projectSpelling(project.value.path, scriptPath),
38342
+ line,
38343
+ set: op === "set"
38344
+ }, KEEPER_DEBUG_MS);
38151
38345
  return this.jsonTextResponse({
38152
- ...asParams(JSON.parse(answered.content[0]?.text ?? "{}")),
38346
+ ...answer,
38153
38347
  held: this.breakpointsAsProjectSpells(project.value.path, held),
38154
- setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor()),
38155
- note: "The editor keeps a breakpoint set this way for one play. Every breakpoint held here is sent again before each play editor_run starts, and kept in the project for the server after a reconnect.",
38156
- ...this.breakpointsAtRisk()
38348
+ ...editor ? {
38349
+ setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor())
38350
+ } : {},
38351
+ ...told === null ? {} : ("unreached" in told) || typeof told["refused"] === "string" ? {
38352
+ runningGame: `The game this server started is running, and its debugger was not told: ${String("unreached" in told ? told.unreached : told["refused"])}. It takes the line when it is started again.`
38353
+ } : {
38354
+ runningGame: `Told to the running game (pid ${kept?.pid ?? "?"}), which this server started.`
38355
+ },
38356
+ note: editor ? "The editor keeps a breakpoint set this way for one play. Every breakpoint held here is sent again before each play editor_run starts, handed to every game this server starts itself, and kept in the project for the server after a reconnect." : "No editor is connected, so the set under held is kept here: every game this server starts is handed it as it starts, an editor that connects is sent it before its first play, and it is kept in the project for the server after a reconnect.",
38357
+ ...editor ? this.breakpointsAtRisk() : {}
38157
38358
  });
38158
38359
  }
38159
38360
  case "debug_control":
38160
38361
  case "debug_state": {
38362
+ const kept = this.keptRun();
38363
+ if (kept !== null) {
38364
+ return await this.askTheKeptGame(kept.keeper, keptGameRequest(tool, op, args), kept.projectPath ?? "");
38365
+ }
38161
38366
  if (tool === "debug_state" && op === "output") {
38162
38367
  return await this.handleDAP("dap_get_output", args);
38163
38368
  }
38369
+ const played = this.activeProcess;
38370
+ if ((op === "pause" || op === "step_out") && played?.throughEditor === true && !exited(played)) {
38371
+ return this.createErrorResponse(op === "pause" ? "A game the editor plays cannot be paused from here: Godot's debug adapter answers a pause by reporting the game stopped and leaving it running." : "A game the editor plays cannot be stepped out of a function from here: Godot's debug adapter implements no stepOut.", [
38372
+ "Set a breakpoint where it should stop, then step over or into from there",
38373
+ "A game this server starts, as editor_run start does with args, savesIn or env, has a debugger here that pauses and steps out"
38374
+ ]);
38375
+ }
38164
38376
  const held = await this.debuggedGame(tool === "debug_control" ? "move" : "read");
38165
38377
  if (!held.ok) {
38166
38378
  return held.response;
@@ -38184,8 +38396,8 @@ class GodotServer {
38184
38396
  if (path === undefined) {
38185
38397
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
38186
38398
  }
38187
- const file = join23(path, "project.godot");
38188
- if (!existsSync17(file)) {
38399
+ const file = join24(path, "project.godot");
38400
+ if (!existsSync18(file)) {
38189
38401
  return {
38190
38402
  ok: false,
38191
38403
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -38231,7 +38443,7 @@ class GodotServer {
38231
38443
  }
38232
38444
  runEnvironment(args) {
38233
38445
  const savesIn = readNonEmptyString(args, "savesIn");
38234
- if (savesIn !== undefined && !isAbsolute3(savesIn)) {
38446
+ if (savesIn !== undefined && !isAbsolute4(savesIn)) {
38235
38447
  return {
38236
38448
  ok: false,
38237
38449
  response: this.createErrorResponse(`savesIn must be an absolute path, not "${savesIn}".`, [
@@ -38304,8 +38516,8 @@ class GodotServer {
38304
38516
  if (!game.throughEditor) {
38305
38517
  return {
38306
38518
  ok: false,
38307
- response: this.createErrorResponse("The running game is its own process, so no debugger is holding it: breakpoints never hit and there is no stack to read.", [
38308
- "editor_run start plays it through the open editor, whose debugger the debug_* tools speak to",
38519
+ response: this.createErrorResponse("The running game has no debugger: it was started with none, or by a keeper that could not take questions, so breakpoints never hit and there is no stack to read.", [
38520
+ "editor_run start starts it again, and the keeper holding a game this server starts is its debugger",
38309
38521
  "editor_status says whether an editor has reached this server"
38310
38522
  ])
38311
38523
  };
@@ -38815,11 +39027,11 @@ class GodotServer {
38815
39027
  if (!engine.ok) {
38816
39028
  return engine.response;
38817
39029
  }
38818
- const application = parseProjectGodot(readFileSync21(project.value.file, "utf8"))["application"] ?? {};
39030
+ const application = parseProjectGodot(readFileSync22(project.value.file, "utf8"))["application"] ?? {};
38819
39031
  const name = application["config/name"];
38820
39032
  const mainScene = application["run/main_scene"];
38821
39033
  const info = {
38822
- name: typeof name === "string" && name !== "" ? name : basename5(project.value.path),
39034
+ name: typeof name === "string" && name !== "" ? name : basename6(project.value.path),
38823
39035
  path: project.value.path,
38824
39036
  mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
38825
39037
  godotVersion: await this.godotVersion(engine.value),
@@ -38871,18 +39083,18 @@ class GodotServer {
38871
39083
  if (!engine.ok) {
38872
39084
  return engine.response;
38873
39085
  }
38874
- const folder = dirname16(output.absolutePath);
39086
+ const folder = dirname17(output.absolutePath);
38875
39087
  try {
38876
39088
  mkdirSync14(folder, { recursive: true });
38877
39089
  } catch (error) {
38878
- return this.createErrorResponse(`Cannot create ${dirname16(output.relativePath)} to export into: ${errorMessage(error)}`);
39090
+ return this.createErrorResponse(`Cannot create ${dirname17(output.relativePath)} to export into: ${errorMessage(error)}`);
38879
39091
  }
38880
39092
  const debug = readBoolean(args, "debug") ?? false;
38881
39093
  const exportLogs = scratchDirectory("export");
38882
39094
  const exportArgs = [
38883
39095
  "--headless",
38884
39096
  "--log-file",
38885
- join23(exportLogs, "engine.log"),
39097
+ join24(exportLogs, "engine.log"),
38886
39098
  "--path",
38887
39099
  project.value.path,
38888
39100
  debug ? "--export-debug" : "--export-release",
@@ -38890,8 +39102,12 @@ class GodotServer {
38890
39102
  output.absolutePath
38891
39103
  ];
38892
39104
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
38893
- const writtenAt = () => existsSync17(output.absolutePath) ? statSync9(output.absolutePath).mtimeMs : null;
39105
+ const writtenAt = () => existsSync18(output.absolutePath) ? statSync9(output.absolutePath).mtimeMs : null;
38894
39106
  const before = writtenAt();
39107
+ const freed = freeLibraryCopies(project.value.path);
39108
+ if (freed.setAside.length > 0 || freed.swept.length > 0) {
39109
+ this.logDebug(`Before the export: set aside ${freed.setAside.join(", ")}; swept ${freed.swept.join(", ")}`);
39110
+ }
38895
39111
  let ending;
38896
39112
  try {
38897
39113
  ending = await runEngine(engine.value, exportArgs, { timeout: { ms: 300000, said: "five minutes" } });
@@ -38954,10 +39170,10 @@ class GodotServer {
38954
39170
  if (movie !== null) {
38955
39171
  return this.createErrorResponse(movie);
38956
39172
  }
38957
- const missing = paths.value.filter((path) => !existsSync17(join23(project.value.path, path.slice("res://".length))));
39173
+ const missing = paths.value.filter((path) => !existsSync18(join24(project.value.path, path.slice("res://".length))));
38958
39174
  const asked = paths.value.filter((path) => !missing.includes(path));
38959
39175
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
38960
- const noRunner = !existsSync17(join23(project.value.path, runner));
39176
+ const noRunner = !existsSync18(join24(project.value.path, runner));
38961
39177
  if (asked.length === 0) {
38962
39178
  const verdict = `nothing at ${missing.join(" and ")}`;
38963
39179
  const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
@@ -39048,21 +39264,21 @@ class GodotServer {
39048
39264
  await untilGone([run.process.pid]);
39049
39265
  }
39050
39266
  await run.settled;
39051
- const reportsDir = join23(project.value.path, ".godot", ...ours.split("/"));
39267
+ const reportsDir = join24(project.value.path, ".godot", ...ours.split("/"));
39052
39268
  let report = null;
39053
39269
  let reportProblem = null;
39054
39270
  try {
39055
- const written = existsSync17(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
39271
+ const written = existsSync18(reportsDir) ? readdirSync10(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
39056
39272
  const newest = written.at(-1);
39057
39273
  if (newest !== undefined) {
39058
- report = parseJUnit(readFileSync21(join23(reportsDir, newest, "results.xml"), "utf8"));
39274
+ report = parseJUnit(readFileSync22(join24(reportsDir, newest, "results.xml"), "utf8"));
39059
39275
  }
39060
39276
  } catch (error) {
39061
39277
  reportProblem = errorMessage(error);
39062
39278
  } finally {
39063
39279
  discard(reportsDir, userData);
39064
39280
  try {
39065
- rmdirSync2(dirname16(reportsDir));
39281
+ rmdirSync2(dirname17(reportsDir));
39066
39282
  } catch {}
39067
39283
  }
39068
39284
  const engineEntries = foldedRepeats(run.log.select({
@@ -39322,11 +39538,11 @@ class GodotServer {
39322
39538
  const cached = cachedClasses(projectPath);
39323
39539
  const sourceOf = (resourcePath) => {
39324
39540
  const contained = resolveWithinProject(projectPath, resourcePath);
39325
- if (!contained.ok || !existsSync17(contained.absolutePath)) {
39541
+ if (!contained.ok || !existsSync18(contained.absolutePath)) {
39326
39542
  return null;
39327
39543
  }
39328
39544
  try {
39329
- return readFileSync21(contained.absolutePath, "utf8");
39545
+ return readFileSync22(contained.absolutePath, "utf8");
39330
39546
  } catch {
39331
39547
  return null;
39332
39548
  }
@@ -39391,11 +39607,11 @@ class GodotServer {
39391
39607
  }
39392
39608
  sourceOfClass(projectPath, resourcePath) {
39393
39609
  const contained = resolveWithinProject(projectPath, resourcePath);
39394
- if (!contained.ok || !existsSync17(contained.absolutePath)) {
39610
+ if (!contained.ok || !existsSync18(contained.absolutePath)) {
39395
39611
  return null;
39396
39612
  }
39397
39613
  try {
39398
- return readFileSync21(contained.absolutePath, "utf8");
39614
+ return readFileSync22(contained.absolutePath, "utf8");
39399
39615
  } catch {
39400
39616
  return null;
39401
39617
  }
@@ -39516,6 +39732,38 @@ class GodotServer {
39516
39732
  breakpointsAtRisk: status.syncsBreakpoints === false ? "This editor clears every breakpoint in its script editor whenever a debug session opens on its adapter, which this server does for its first breakpoint or stack read: network/debug_adapter/sync_breakpoints is off. The installed addon turns it on as it loads, so the editor is running an older one; editor_launch restart puts the installed addon in, or turn the setting on in Editor Settings > Network > Debug Adapter." : "Whether this editor keeps its breakpoints when a debug session opens cannot be told: the addon it is running predates the report. Godot clears them unless network/debug_adapter/sync_breakpoints is on, which the installed addon turns on as it loads; editor_launch restart puts the installed addon in."
39517
39733
  };
39518
39734
  }
39735
+ breakpointsForAGame(projectPath) {
39736
+ return this.dapHoldingBreakpointsOf(projectPath).breakpointsHeld().flatMap((one) => one.lines.map((line) => `${projectSpelling(projectPath, one.scriptPath)}:${line}`));
39737
+ }
39738
+ keptRun() {
39739
+ const game = this.activeProcess;
39740
+ if (game === null || game.throughEditor || exited(game) || game.keeper === undefined) {
39741
+ return null;
39742
+ }
39743
+ return { ...game, keeper: game.keeper };
39744
+ }
39745
+ async askTheKeptGame(keeper, request, projectPath) {
39746
+ const answered = await askTheKeepersDebugger(keeper, request, KEEPER_DEBUG_MS);
39747
+ if ("unreached" in answered) {
39748
+ return this.createErrorResponse(`The debugger of the running game did not answer: ${String(answered.unreached)}.`, [
39749
+ "editor_run start starts the game again, with a debugger",
39750
+ "editor_status says whether it is still running"
39751
+ ]);
39752
+ }
39753
+ if (typeof answered["refused"] === "string") {
39754
+ return this.createErrorResponse(answered["refused"]);
39755
+ }
39756
+ const stack = answered["stack"];
39757
+ return this.jsonTextResponse({
39758
+ ...answered,
39759
+ ...Array.isArray(stack) ? {
39760
+ stack: stack.map((frame) => ({
39761
+ ...frame,
39762
+ ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join24(projectPath, frame["script"].slice("res://".length)) } : {}
39763
+ }))
39764
+ } : {}
39765
+ });
39766
+ }
39519
39767
  breakpointsAsProjectSpells(projectPath, held) {
39520
39768
  return held.map((one) => ({
39521
39769
  scriptPath: projectSpelling(projectPath, one.scriptPath),
@@ -39781,7 +40029,7 @@ class GodotServer {
39781
40029
  return null;
39782
40030
  }
39783
40031
  try {
39784
- return settingKeys(readFileSync21(join23(projectPath, "project.godot"), "utf8"));
40032
+ return settingKeys(readFileSync22(join24(projectPath, "project.godot"), "utf8"));
39785
40033
  } catch {
39786
40034
  return null;
39787
40035
  }
@@ -40046,6 +40294,7 @@ class GodotServer {
40046
40294
  }
40047
40295
  async openAnEditor(engine, projectPath, ports, hidden) {
40048
40296
  clearEditorLog(projectPath);
40297
+ freeLibraryCopies(projectPath);
40049
40298
  const launched = await launchOutsideTheTree({
40050
40299
  command: engine,
40051
40300
  args: editorArguments(projectPath, ports, editorLogPath(projectPath), hidden),
@@ -40177,9 +40426,10 @@ class GodotServer {
40177
40426
  started.announcedBefore = alreadyPlaying;
40178
40427
  started.bootScene = runtimeWaitMs.scene;
40179
40428
  this.activeProcess = started;
40180
- const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && (given.value.length > 0 || flags.value.length > 0) ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
40429
+ const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && (given.value.length > 0 || flags.value.length > 0) ? ` The editor cannot be handed arguments for a game it plays, so this one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? ` The editor plays a game with its own environment, so a run given one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}, and silent: false plays it through the editor.${OWN_DEBUGGER}` : "";
40181
40430
  const endedForThis = endedToStartThis(ended);
40182
40431
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
40432
+ const handed = this.dapHoldingBreakpointsOf(project.value.path).breakpointsHeld();
40183
40433
  const alsoRunning = leftRunningNote(stranded);
40184
40434
  return this.jsonTextResponse({
40185
40435
  started: true,
@@ -40189,6 +40439,7 @@ class GodotServer {
40189
40439
  arguments: cmdArgs,
40190
40440
  argsNote: engineFlagsNote(given.value),
40191
40441
  transcript: started.transcript,
40442
+ breakpoints: started.keeper === undefined || handed.length === 0 ? undefined : this.breakpointsAsProjectSpells(project.value.path, handed),
40192
40443
  profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
40193
40444
  refreshedClasses: refreshed.value,
40194
40445
  ...scanWaitAnswer(scanned),
@@ -40197,12 +40448,12 @@ class GodotServer {
40197
40448
  previousRunLeft: previousRunLeft(ended),
40198
40449
  ...previousRunHow(ended),
40199
40450
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
40200
- message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
40451
+ message: `Use editor_output for what it prints and editor_run stop to end it.${started.keeper !== undefined && handed.length > 0 ? " It stops at the lines under breakpoints, and debug_state stack reads where it is held." : ""}${spawnedInstead}${endedForThis}${alsoRunning}`
40201
40452
  });
40202
40453
  }
40203
40454
  async runtimeUp(projectPath, before, budget, withArgs) {
40204
40455
  const budgetMs = budget.ms;
40205
- if (!existsSync17(join23(projectPath, RUNTIME_AUTOLOAD.path))) {
40456
+ if (!existsSync18(join24(projectPath, RUNTIME_AUTOLOAD.path))) {
40206
40457
  return runtimeVerdict(null, {
40207
40458
  addon: false,
40208
40459
  budgetMs,
@@ -40481,7 +40732,7 @@ class GodotServer {
40481
40732
  return 0;
40482
40733
  }
40483
40734
  try {
40484
- return reportAlreadyInTranscript(readFileSync21(game.transcript), readFileSync21(report));
40735
+ return reportAlreadyInTranscript(readFileSync22(game.transcript), readFileSync22(report));
40485
40736
  } catch (error) {
40486
40737
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
40487
40738
  return 0;
@@ -40724,7 +40975,8 @@ class GodotServer {
40724
40975
  startedAt,
40725
40976
  projectPath,
40726
40977
  ...this.servedBy(),
40727
- ...profile === undefined ? {} : { profile }
40978
+ ...profile === undefined ? {} : { profile },
40979
+ breakpoints: this.breakpointsForAGame(projectPath)
40728
40980
  },
40729
40981
  ...desktop === undefined ? {} : { desktop }
40730
40982
  });
@@ -41160,7 +41412,7 @@ class GodotServer {
41160
41412
  }
41161
41413
  const going = await this.runStillGoing(run);
41162
41414
  const projectPath = run.projectPath;
41163
- const projectGone = projectPath !== null && !existsSync17(join23(projectPath, "project.godot"));
41415
+ const projectGone = projectPath !== null && !existsSync18(join24(projectPath, "project.godot"));
41164
41416
  if (projectGone && callersName !== undefined) {
41165
41417
  return this.createErrorResponse(`callersOf reads the project's scripts, and the run's project at ${projectPath} has no project.godot any more.`, ["editor_run profile without callersOf answers with the profile itself"]);
41166
41418
  }
@@ -41448,7 +41700,7 @@ class GodotServer {
41448
41700
  notes.push(endedWithoutACode({
41449
41701
  throughEditor: run.throughEditor,
41450
41702
  announced: announced !== undefined || run.announcedPid !== undefined,
41451
- couldAnnounce: run.projectPath !== null && existsSync17(join23(run.projectPath, RUNTIME_AUTOLOAD.path)),
41703
+ couldAnnounce: run.projectPath !== null && existsSync18(join24(run.projectPath, RUNTIME_AUTOLOAD.path)),
41452
41704
  endedHere: typeof run.endedHere === "string",
41453
41705
  pickedUp: run.pickedUp === true
41454
41706
  }));
@@ -42013,7 +42265,7 @@ class GodotServer {
42013
42265
  if (!scriptPath.ok) {
42014
42266
  return this.createErrorResponse(scriptPath.reason);
42015
42267
  }
42016
- if (!existsSync17(scriptPath.absolutePath)) {
42268
+ if (!existsSync18(scriptPath.absolutePath)) {
42017
42269
  return this.createErrorResponse(`Script file does not exist: ${scriptPath.absolutePath}`);
42018
42270
  }
42019
42271
  const asked = readNonEmptyString(args, "newScriptPath");
@@ -42341,7 +42593,7 @@ class GodotServer {
42341
42593
  async ownRunOnItsWay(projectPath, budgetMs) {
42342
42594
  const run = await this.currentRun();
42343
42595
  const project = projectPath ?? run?.projectPath ?? null;
42344
- 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)) {
42596
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync18(join24(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
42345
42597
  return null;
42346
42598
  }
42347
42599
  const already = runtimesAnnounced().running;
@@ -42426,12 +42678,25 @@ class GodotServer {
42426
42678
  }
42427
42679
  }
42428
42680
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
42429
- const screenshotPath = screenshotDir ? join23(screenshotDir, "capture.png") : null;
42681
+ const screenshotPath = screenshotDir ? join24(screenshotDir, "capture.png") : null;
42430
42682
  const staleNote = this.runtimeStaleNote(choice.endpoint);
42431
42683
  const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
42432
42684
  const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
42433
42685
 
42434
42686
  ${staleNote}`);
42687
+ const onGame = {
42688
+ pid: choice.endpoint.pid,
42689
+ what: `${asked.tool} ${asked.op}`,
42690
+ alongside: new Set
42691
+ };
42692
+ for (const other of this.callsOnGames) {
42693
+ if (other.pid === onGame.pid) {
42694
+ other.alongside.add(onGame.what);
42695
+ onGame.alongside.add(other.what);
42696
+ }
42697
+ }
42698
+ this.callsOnGames.add(onGame);
42699
+ const alongside = () => onGame.alongside.size === 0 ? {} : { ranAlongside: [...onGame.alongside], alongsideNote: ALONGSIDE_NOTE };
42435
42700
  try {
42436
42701
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
42437
42702
  if (!reply.ok && reply.requestId !== undefined) {
@@ -42442,7 +42707,8 @@ ${staleNote}`);
42442
42707
  waitedMs: timeoutMs,
42443
42708
  note: `${reply.message} ${longerWaitNote(asked.tool, asked.op, timeoutMs)}`,
42444
42709
  ...answeredBy,
42445
- ...stale
42710
+ ...stale,
42711
+ ...alongside()
42446
42712
  });
42447
42713
  }
42448
42714
  if (!reply.ok) {
@@ -42450,7 +42716,7 @@ ${staleNote}`);
42450
42716
  }
42451
42717
  const { id: _id, ...payload } = reply.payload;
42452
42718
  if (!expectsScreenshot) {
42453
- return this.jsonTextResponse({ ...payload, ...answeredBy, ...stale });
42719
+ return this.jsonTextResponse({ ...payload, ...answeredBy, ...stale, ...alongside() });
42454
42720
  }
42455
42721
  const returnedPath = readString(payload, "path");
42456
42722
  if (readString(payload, "type") !== "screenshot_file" || !returnedPath || !screenshotPath) {
@@ -42467,18 +42733,22 @@ ${staleNote}`);
42467
42733
  return this.createErrorResponse(`The capture was taken and could not be saved at ${saveTo}: ${errorMessage(error)}`);
42468
42734
  }
42469
42735
  }
42736
+ const ranWith = [...onGame.alongside];
42470
42737
  return {
42471
42738
  content: [
42472
42739
  {
42473
42740
  type: "text",
42474
- text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${staleNote === undefined ? "" : `
42741
+ text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${ranWith.length === 0 ? "" : `
42742
+
42743
+ Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}${staleNote === undefined ? "" : `
42475
42744
 
42476
42745
  ${staleNote}`}`
42477
42746
  },
42478
- { type: "image", data: readFileSync21(screenshotPath).toString("base64"), mimeType: "image/png" }
42747
+ { type: "image", data: readFileSync22(screenshotPath).toString("base64"), mimeType: "image/png" }
42479
42748
  ]
42480
42749
  };
42481
42750
  } finally {
42751
+ this.callsOnGames.delete(onGame);
42482
42752
  discard(screenshotDir);
42483
42753
  }
42484
42754
  }