gdharness 1.3.7 → 1.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/build/cli.js +999 -864
  2. package/build/godot/addons/gdharness_editor/file_reads.gd +13 -0
  3. package/build/godot/addons/gdharness_editor/reading.gd +61 -5
  4. package/build/godot/addons/gdharness_editor/scene_file.gd +41 -35
  5. package/build/godot/addons/gdharness_editor/serialisation.gd +8 -7
  6. package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -2
  7. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +244 -0
  8. package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
  9. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +105 -157
  10. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +25 -1
  11. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -1
  12. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +72 -3
  14. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +7 -1
  15. package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
  16. package/build/godot/addons/gdharness_runtime/reading.gd +61 -5
  17. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +6 -1
  19. package/build/godot/addons/gdharness_runtime/runtime_input.gd +74 -54
  20. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +38 -6
  21. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +2 -0
  22. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +26 -0
  23. package/build/godot/addons/gdharness_runtime/runtime_values.gd +105 -6
  24. package/build/godot/addons/gdharness_runtime/serialisation.gd +8 -7
  25. package/build/godot/operations/dependencies.gd +61 -10
  26. package/build/godot/operations/editor_tools.gd +8 -0
  27. package/build/godot/operations/file_reads.gd +13 -0
  28. package/build/godot/operations/gdscript_analysis.gd +76 -6
  29. package/build/godot/operations/gdscript_authoring.gd +5 -2
  30. package/build/godot/operations/godot_operations.gd +4 -0
  31. package/build/godot/operations/import_pipeline.gd +45 -1
  32. package/build/godot/operations/input_actions.gd +22 -7
  33. package/build/godot/operations/project_config.gd +31 -0
  34. package/build/godot/operations/project_diagnostics.gd +6 -5
  35. package/build/godot/operations/reading.gd +61 -5
  36. package/build/godot/operations/serialisation.gd +8 -7
  37. package/build/keeper.js +44 -13
  38. package/package.json +1 -1
package/build/cli.js CHANGED
@@ -8367,9 +8367,493 @@ var init_doc_cache = __esm(() => {
8367
8367
  CACHE_FILE = /^editor_doc_cache-\d+\.\d+\.res$/;
8368
8368
  });
8369
8369
 
8370
+ // src/project-scan.ts
8371
+ import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync3 } from "node:fs";
8372
+ import { extname, join as join7 } from "node:path";
8373
+ function skipped(name) {
8374
+ return name.startsWith(".") || SKIPPED.has(name);
8375
+ }
8376
+ function steppedOver(directory) {
8377
+ return gdignored(directory) || installedByUs(directory);
8378
+ }
8379
+ function gdignored(directory) {
8380
+ return existsSync6(join7(directory, ".gdignore"));
8381
+ }
8382
+ function installedByUs(directory) {
8383
+ return existsSync6(join7(directory, VERSION_MARKER));
8384
+ }
8385
+ function projectStructure(projectPath) {
8386
+ const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
8387
+ const visit = (directory) => {
8388
+ if (steppedOver(directory)) {
8389
+ return;
8390
+ }
8391
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8392
+ if (skipped(entry.name)) {
8393
+ continue;
8394
+ }
8395
+ if (entry.isDirectory()) {
8396
+ visit(join7(directory, entry.name));
8397
+ } else if (entry.isFile()) {
8398
+ const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
8399
+ if (SIDECAR_EXTENSIONS.has(extension)) {
8400
+ continue;
8401
+ }
8402
+ if (SCENE_EXTENSIONS.has(extension)) {
8403
+ structure.scenes += 1;
8404
+ } else if (extension === "gd" || extension === "cs") {
8405
+ structure.scripts += 1;
8406
+ } else if (ASSET_EXTENSIONS.has(extension)) {
8407
+ structure.assets += 1;
8408
+ } else {
8409
+ structure.other += 1;
8410
+ }
8411
+ }
8412
+ }
8413
+ };
8414
+ visit(projectPath);
8415
+ return structure;
8416
+ }
8417
+ function projectFiles(projectPath, extensions) {
8418
+ return filesBelow(projectPath, extensions);
8419
+ }
8420
+ function folderFiles(directory, extensions) {
8421
+ return gdignored(directory) ? [] : filesBelow(directory, extensions);
8422
+ }
8423
+ function filesBelow(root, extensions) {
8424
+ const found = [];
8425
+ const visit = (directory, prefix) => {
8426
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8427
+ if (skipped(entry.name)) {
8428
+ continue;
8429
+ }
8430
+ const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
8431
+ const path = join7(directory, entry.name);
8432
+ if (entry.isDirectory()) {
8433
+ if (!steppedOver(path)) {
8434
+ visit(path, spelled);
8435
+ }
8436
+ } else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
8437
+ found.push(spelled);
8438
+ }
8439
+ }
8440
+ };
8441
+ visit(root, "");
8442
+ return found.sort();
8443
+ }
8444
+ function scriptsWithoutUid(projectPath) {
8445
+ const found = [];
8446
+ const visit = (directory, prefix) => {
8447
+ if (steppedOver(directory)) {
8448
+ return;
8449
+ }
8450
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8451
+ if (skipped(entry.name)) {
8452
+ continue;
8453
+ }
8454
+ const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
8455
+ if (entry.isDirectory()) {
8456
+ visit(join7(directory, entry.name), spelled);
8457
+ continue;
8458
+ }
8459
+ if (!entry.isFile()) {
8460
+ continue;
8461
+ }
8462
+ const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
8463
+ if (CARRIES_A_UID.has(extension) && !existsSync6(join7(directory, `${entry.name}.uid`))) {
8464
+ found.push(spelled);
8465
+ }
8466
+ }
8467
+ };
8468
+ visit(projectPath, "");
8469
+ return found.sort();
8470
+ }
8471
+ function pathsInUidCache(projectPath) {
8472
+ let data;
8473
+ try {
8474
+ data = readFileSync3(join7(projectPath, ".godot", "uid_cache.bin"));
8475
+ } catch {
8476
+ return null;
8477
+ }
8478
+ if (data.length < 4) {
8479
+ return null;
8480
+ }
8481
+ const count = data.readUInt32LE(0);
8482
+ const paths = new Map;
8483
+ let at = 4;
8484
+ for (let entry = 0;entry < count; entry++) {
8485
+ if (at + 12 > data.length) {
8486
+ return null;
8487
+ }
8488
+ const id = data.readBigUInt64LE(at);
8489
+ const length = data.readUInt32LE(at + 8);
8490
+ at += 12;
8491
+ if (at + length > data.length) {
8492
+ return null;
8493
+ }
8494
+ paths.set(id, data.toString("utf8", at, at + length));
8495
+ at += length;
8496
+ }
8497
+ return new Set(paths.values());
8498
+ }
8499
+ function isBinary(bytes) {
8500
+ return bytes.subarray(0, 8000).includes(0);
8501
+ }
8502
+ function projectTexts(projectPath) {
8503
+ const found = [];
8504
+ const visit = (directory, prefix) => {
8505
+ if (steppedOver(directory)) {
8506
+ return;
8507
+ }
8508
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8509
+ if (skipped(entry.name)) {
8510
+ continue;
8511
+ }
8512
+ const absolute = join7(directory, entry.name);
8513
+ const spelled = `${prefix}${entry.name}`;
8514
+ if (entry.isDirectory()) {
8515
+ visit(absolute, `${spelled}/`);
8516
+ continue;
8517
+ }
8518
+ const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
8519
+ if (!entry.isFile() || BINARY_EXTENSIONS.has(extension)) {
8520
+ continue;
8521
+ }
8522
+ const bytes = readFileSync3(absolute);
8523
+ if (!isBinary(bytes)) {
8524
+ found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
8525
+ }
8526
+ }
8527
+ };
8528
+ visit(projectPath, "");
8529
+ return found;
8530
+ }
8531
+ function ignoredTexts(projectPath) {
8532
+ const found = [];
8533
+ const visit = (directory, prefix, ignored) => {
8534
+ if (installedByUs(directory)) {
8535
+ return;
8536
+ }
8537
+ const under = ignored || gdignored(directory);
8538
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8539
+ if (skipped(entry.name)) {
8540
+ continue;
8541
+ }
8542
+ const absolute = join7(directory, entry.name);
8543
+ const spelled = `${prefix}${entry.name}`;
8544
+ if (entry.isDirectory()) {
8545
+ visit(absolute, `${spelled}/`, under);
8546
+ continue;
8547
+ }
8548
+ const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
8549
+ if (!under || !entry.isFile() || BINARY_EXTENSIONS.has(extension) || entry.name === ".gdignore") {
8550
+ continue;
8551
+ }
8552
+ const bytes = readFileSync3(absolute);
8553
+ if (!isBinary(bytes)) {
8554
+ found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
8555
+ }
8556
+ }
8557
+ };
8558
+ visit(projectPath, "", false);
8559
+ return found;
8560
+ }
8561
+ function plainMatch(line, needle, caseSensitive) {
8562
+ if (caseSensitive) {
8563
+ return line.includes(needle) ? [needle] : null;
8564
+ }
8565
+ const lowered = line.toLowerCase();
8566
+ const at = lowered.indexOf(needle);
8567
+ if (at < 0) {
8568
+ return null;
8569
+ }
8570
+ return [lowered.length === line.length ? line.slice(at, at + needle.length) : needle];
8571
+ }
8572
+ function compiledQuery(query, caseSensitive) {
8573
+ try {
8574
+ return new RegExp(query, caseSensitive ? "" : "i");
8575
+ } catch (error) {
8576
+ const why = errorMessage(error).split(": ").at(-1) ?? "";
8577
+ throw new Error(`'${query}' is not a regular expression: ${why.charAt(0).toLowerCase()}${why.slice(1)}. With regex true, a character such as ( ) [ ] { } . * + ? | ^ $ or \\ stands for itself only after a backslash, as in \\(; or leave regex out to search for the text as written.`);
8578
+ }
8579
+ }
8580
+ function searchProject(projectPath, options) {
8581
+ const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
8582
+ const result = {
8583
+ query: options.query,
8584
+ results: [],
8585
+ summary: { files_searched: 0, files_with_matches: 0, total_matches: 0, truncated: false }
8586
+ };
8587
+ const regex = options.regex ? compiledQuery(options.query, options.caseSensitive) : null;
8588
+ const needle = options.caseSensitive ? options.query : options.query.toLowerCase();
8589
+ const full = () => result.summary.truncated;
8590
+ const notSearched = [];
8591
+ const visit = (directory) => {
8592
+ if (steppedOver(directory)) {
8593
+ if (gdignored(directory)) {
8594
+ notSearched.push(`res://${directory.slice(projectPath.length + 1).replace(/\\/g, "/")}/`);
8595
+ }
8596
+ return;
8597
+ }
8598
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8599
+ if (full()) {
8600
+ return;
8601
+ }
8602
+ if (skipped(entry.name)) {
8603
+ continue;
8604
+ }
8605
+ const entryPath = join7(directory, entry.name);
8606
+ if (entry.isDirectory()) {
8607
+ visit(entryPath);
8608
+ continue;
8609
+ }
8610
+ const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
8611
+ if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
8612
+ continue;
8613
+ }
8614
+ const bytes = readFileSync3(entryPath);
8615
+ if (extensions === null && isBinary(bytes)) {
8616
+ continue;
8617
+ }
8618
+ result.summary.files_searched += 1;
8619
+ const matches = [];
8620
+ for (const [index, line] of bytes.toString("utf8").split(/\r?\n/).entries()) {
8621
+ const found = regex ? regex.exec(line) : plainMatch(line, needle, options.caseSensitive);
8622
+ if (found === null) {
8623
+ continue;
8624
+ }
8625
+ if (result.summary.total_matches >= options.maxResults) {
8626
+ result.summary.truncated = true;
8627
+ break;
8628
+ }
8629
+ matches.push({ line: index + 1, content: line.trim(), match: found[0] });
8630
+ result.summary.total_matches += 1;
8631
+ }
8632
+ if (matches.length > 0) {
8633
+ const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
8634
+ result.results.push({ file: `res://${relativePath}`, matches });
8635
+ result.summary.files_with_matches += 1;
8636
+ }
8637
+ }
8638
+ };
8639
+ visit(projectPath);
8640
+ return notSearched.length === 0 ? result : { ...result, not_searched: notSearched };
8641
+ }
8642
+ var SKIPPED, VERSION_MARKER = ".gdharness-version", SCENE_EXTENSIONS, SIDECAR_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID, BINARY_EXTENSIONS;
8643
+ var init_project_scan = __esm(() => {
8644
+ init_errors();
8645
+ SKIPPED = new Set(["node_modules"]);
8646
+ SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
8647
+ SIDECAR_EXTENSIONS = new Set(["import", "uid"]);
8648
+ ASSET_EXTENSIONS = new Set([
8649
+ "png",
8650
+ "jpg",
8651
+ "jpeg",
8652
+ "webp",
8653
+ "svg",
8654
+ "bmp",
8655
+ "tga",
8656
+ "exr",
8657
+ "hdr",
8658
+ "ktx",
8659
+ "dds",
8660
+ "ttf",
8661
+ "otf",
8662
+ "woff",
8663
+ "woff2",
8664
+ "fnt",
8665
+ "wav",
8666
+ "mp3",
8667
+ "ogg",
8668
+ "glb",
8669
+ "gltf",
8670
+ "obj",
8671
+ "fbx",
8672
+ "blend",
8673
+ "dae",
8674
+ "ogv",
8675
+ "gdshader",
8676
+ "shader"
8677
+ ]);
8678
+ CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
8679
+ BINARY_EXTENSIONS = new Set([
8680
+ ...[...ASSET_EXTENSIONS].filter((extension) => !["svg", "gltf", "gdshader", "shader", "fnt"].includes(extension)),
8681
+ "scn",
8682
+ "res",
8683
+ "zip",
8684
+ "pck",
8685
+ "exe",
8686
+ "dll",
8687
+ "so",
8688
+ "dylib"
8689
+ ]);
8690
+ });
8691
+
8692
+ // src/import-freshness.ts
8693
+ import { createHash } from "node:crypto";
8694
+ import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync4, statSync as statSync5 } from "node:fs";
8695
+ import { join as join8 } from "node:path";
8696
+ function md5Of(file) {
8697
+ let stats;
8698
+ try {
8699
+ stats = statSync5(file);
8700
+ } catch {
8701
+ return null;
8702
+ }
8703
+ const known = hashes.get(file);
8704
+ if (known !== undefined && known.size === stats.size && known.mtimeMs === stats.mtimeMs) {
8705
+ return known.md5;
8706
+ }
8707
+ let md5;
8708
+ try {
8709
+ md5 = createHash("md5").update(readFileSync4(file)).digest("hex");
8710
+ } catch {
8711
+ return null;
8712
+ }
8713
+ hashes.set(file, { size: stats.size, mtimeMs: stats.mtimeMs, md5 });
8714
+ return md5;
8715
+ }
8716
+ function sectionOf(text, section) {
8717
+ const keys = new Map;
8718
+ let inside = false;
8719
+ for (const line of text.split(/\r?\n/)) {
8720
+ const header = /^\[([^\]]+)\]\s*$/.exec(line);
8721
+ if (header !== null) {
8722
+ inside = header[1] === section;
8723
+ continue;
8724
+ }
8725
+ const pair = inside ? /^([A-Za-z0-9_./]+)=(.*)$/.exec(line) : null;
8726
+ if (pair?.[1] !== undefined && pair[2] !== undefined) {
8727
+ keys.set(pair[1], pair[2]);
8728
+ }
8729
+ }
8730
+ return keys;
8731
+ }
8732
+ function unquoted(value) {
8733
+ return value === undefined ? "" : value.replace(/^"(.*)"$/, "$1");
8734
+ }
8735
+ function listed(value) {
8736
+ return [...(value ?? "").matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1] ?? "");
8737
+ }
8738
+ function onDisk(projectPath, resPath) {
8739
+ return join8(projectPath, resPath.slice("res://".length));
8740
+ }
8741
+ function behind(projectPath, resPath, sidecar) {
8742
+ const remap = sectionOf(sidecar, "remap");
8743
+ const importer = unquoted(remap.get("importer"));
8744
+ if (importer === "keep" || importer === "skip") {
8745
+ return null;
8746
+ }
8747
+ const outputs = listed(sectionOf(sidecar, "deps").get("dest_files"));
8748
+ if (outputs.some((output) => !existsSync7(onDisk(projectPath, output)))) {
8749
+ return "its imported output is not on disk";
8750
+ }
8751
+ const name = resPath.slice(resPath.lastIndexOf("/") + 1);
8752
+ const record = join8(projectPath, ".godot", "imported", `${name}-${createHash("md5").update(resPath, "utf8").digest("hex")}.md5`);
8753
+ let recorded = "";
8754
+ try {
8755
+ recorded = /^source_md5="([0-9a-f]*)"/m.exec(readFileSync4(record, "utf8"))?.[1] ?? "";
8756
+ } catch {
8757
+ recorded = "";
8758
+ }
8759
+ if (recorded === "") {
8760
+ return "its import recorded no hash of its source";
8761
+ }
8762
+ const current = md5Of(onDisk(projectPath, resPath));
8763
+ return current === null || current === recorded ? null : "the source changed after it was imported";
8764
+ }
8765
+ function staleImports(projectPath) {
8766
+ const stale = [];
8767
+ const visit = (directory, resDirectory) => {
8768
+ if (existsSync7(join8(directory, ".gdignore")) || existsSync7(join8(directory, VERSION_MARKER))) {
8769
+ return;
8770
+ }
8771
+ let entries;
8772
+ try {
8773
+ entries = readdirSync5(directory, { withFileTypes: true });
8774
+ } catch {
8775
+ return;
8776
+ }
8777
+ const names = new Set(entries.map((entry) => entry.name));
8778
+ for (const entry of entries) {
8779
+ if (entry.name.startsWith(".")) {
8780
+ continue;
8781
+ }
8782
+ const resPath = `${resDirectory}${entry.name}`;
8783
+ if (entry.isDirectory()) {
8784
+ visit(join8(directory, entry.name), `${resPath}/`);
8785
+ continue;
8786
+ }
8787
+ if (!entry.isFile()) {
8788
+ continue;
8789
+ }
8790
+ if (entry.name.endsWith(".import")) {
8791
+ const source = resPath.slice(0, -".import".length);
8792
+ if (!names.has(entry.name.slice(0, -".import".length))) {
8793
+ continue;
8794
+ }
8795
+ let sidecar;
8796
+ try {
8797
+ sidecar = readFileSync4(join8(directory, entry.name), "utf8");
8798
+ } catch {
8799
+ continue;
8800
+ }
8801
+ const reason = behind(projectPath, source, sidecar);
8802
+ if (reason !== null) {
8803
+ stale.push({ path: source, reason });
8804
+ }
8805
+ continue;
8806
+ }
8807
+ const extension = entry.name.slice(entry.name.lastIndexOf(".") + 1).toLowerCase();
8808
+ if (IMPORTABLE.has(extension) && !names.has(`${entry.name}.import`)) {
8809
+ stale.push({ path: resPath, reason: "it has not been imported" });
8810
+ }
8811
+ }
8812
+ };
8813
+ visit(projectPath, "res://");
8814
+ return stale.sort((one, other) => one.path.localeCompare(other.path));
8815
+ }
8816
+ var IMPORTABLE_EXTENSIONS, IMPORTABLE, hashes;
8817
+ var init_import_freshness = __esm(() => {
8818
+ init_project_scan();
8819
+ IMPORTABLE_EXTENSIONS = [
8820
+ "png",
8821
+ "jpg",
8822
+ "jpeg",
8823
+ "webp",
8824
+ "svg",
8825
+ "bmp",
8826
+ "tga",
8827
+ "exr",
8828
+ "hdr",
8829
+ "dds",
8830
+ "ktx",
8831
+ "wav",
8832
+ "mp3",
8833
+ "ogg",
8834
+ "ttf",
8835
+ "otf",
8836
+ "ttc",
8837
+ "otc",
8838
+ "woff",
8839
+ "woff2",
8840
+ "pfb",
8841
+ "pfm",
8842
+ "fnt",
8843
+ "font",
8844
+ "glb",
8845
+ "gltf",
8846
+ "fbx",
8847
+ "obj",
8848
+ "dae"
8849
+ ];
8850
+ IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
8851
+ hashes = new Map;
8852
+ });
8853
+
8370
8854
  // src/library-copies.ts
8371
- import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync3, renameSync, unlinkSync } from "node:fs";
8372
- import { basename as basename5, dirname as dirname6, isAbsolute as isAbsolute3, join as join7 } from "node:path";
8855
+ import { existsSync as existsSync8, readdirSync as readdirSync6, readFileSync as readFileSync5, renameSync, unlinkSync } from "node:fs";
8856
+ import { basename as basename5, dirname as dirname6, isAbsolute as isAbsolute3, join as join9 } from "node:path";
8373
8857
  function freeLibraryCopies(projectPath, platform = process.platform) {
8374
8858
  const setAside = [];
8375
8859
  const swept = [];
@@ -8379,24 +8863,24 @@ function freeLibraryCopies(projectPath, platform = process.platform) {
8379
8863
  for (const library of windowsLibraries(projectPath)) {
8380
8864
  const folder = dirname6(library);
8381
8865
  const copy = `~${basename5(library)}`;
8382
- if (existsSync6(join7(folder, copy))) {
8383
- const aside = join7(folder, `${copy}.aside-${process.pid}-${Date.now()}`);
8866
+ if (existsSync8(join9(folder, copy))) {
8867
+ const aside = join9(folder, `${copy}.aside-${process.pid}-${Date.now()}`);
8384
8868
  try {
8385
- renameSync(join7(folder, copy), aside);
8869
+ renameSync(join9(folder, copy), aside);
8386
8870
  setAside.push(aside);
8387
8871
  } catch {}
8388
8872
  }
8389
8873
  let names;
8390
8874
  try {
8391
- names = readdirSync4(folder);
8875
+ names = readdirSync6(folder);
8392
8876
  } catch {
8393
8877
  continue;
8394
8878
  }
8395
8879
  for (const name of names) {
8396
8880
  if (name.length > copy.length && name.toLowerCase().startsWith(copy.toLowerCase())) {
8397
8881
  try {
8398
- unlinkSync(join7(folder, name));
8399
- swept.push(join7(folder, name));
8882
+ unlinkSync(join9(folder, name));
8883
+ swept.push(join9(folder, name));
8400
8884
  } catch {}
8401
8885
  }
8402
8886
  }
@@ -8404,7 +8888,7 @@ function freeLibraryCopies(projectPath, platform = process.platform) {
8404
8888
  return { setAside, swept };
8405
8889
  }
8406
8890
  function windowsLibraries(projectPath) {
8407
- const listed = readOrNull(join7(projectPath, ".godot", "extension_list.cfg"));
8891
+ const listed = readOrNull(join9(projectPath, ".godot", "extension_list.cfg"));
8408
8892
  if (listed === null) {
8409
8893
  return [];
8410
8894
  }
@@ -8436,13 +8920,13 @@ function resolveResource(projectPath, relativeTo, path) {
8436
8920
  return null;
8437
8921
  }
8438
8922
  if (path.startsWith("res://")) {
8439
- return join7(projectPath, path.slice("res://".length));
8923
+ return join9(projectPath, path.slice("res://".length));
8440
8924
  }
8441
- return isAbsolute3(path) ? path : join7(relativeTo, path);
8925
+ return isAbsolute3(path) ? path : join9(relativeTo, path);
8442
8926
  }
8443
8927
  function readOrNull(path) {
8444
8928
  try {
8445
- return readFileSync3(path, "utf8");
8929
+ return readFileSync5(path, "utf8");
8446
8930
  } catch {
8447
8931
  return null;
8448
8932
  }
@@ -8450,23 +8934,23 @@ function readOrNull(path) {
8450
8934
  var init_library_copies = () => {};
8451
8935
 
8452
8936
  // src/reimport.ts
8453
- import { createHash } from "node:crypto";
8454
- import { existsSync as existsSync7, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync5, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
8455
- import { basename as basename6, join as join8 } from "node:path";
8937
+ import { createHash as createHash2 } from "node:crypto";
8938
+ import { existsSync as existsSync9, readFileSync as readFileSync6, rmSync as rmSync3, statSync as statSync6, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
8939
+ import { basename as basename6, join as join10 } from "node:path";
8456
8940
  function forceNextImport(projectPath, resourcePath) {
8457
8941
  const relative = resourcePath.replace(/^res:\/\//, "");
8458
- const sidecar = join8(projectPath, `${relative}.import`);
8459
- if (!existsSync7(sidecar)) {
8942
+ const sidecar = join10(projectPath, `${relative}.import`);
8943
+ if (!existsSync9(sidecar)) {
8460
8944
  return;
8461
8945
  }
8462
- const { atime, mtimeMs } = statSync5(sidecar);
8463
- writeFileSync3(sidecar, readFileSync4(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
8946
+ const { atime, mtimeMs } = statSync6(sidecar);
8947
+ writeFileSync3(sidecar, readFileSync6(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
8464
8948
  utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
8465
8949
  rmSync3(importRecord(projectPath, resourcePath), { force: true });
8466
8950
  }
8467
8951
  function importRecord(projectPath, resourcePath) {
8468
- const digest = createHash("md5").update(resourcePath).digest("hex");
8469
- return join8(projectPath, ".godot", "imported", `${basename6(resourcePath)}-${digest}.md5`);
8952
+ const digest = createHash2("md5").update(resourcePath).digest("hex");
8953
+ return join10(projectPath, ".godot", "imported", `${basename6(resourcePath)}-${digest}.md5`);
8470
8954
  }
8471
8955
  var init_reimport = () => {};
8472
8956
 
@@ -8496,17 +8980,17 @@ function alive(pid) {
8496
8980
  var init_alive = () => {};
8497
8981
 
8498
8982
  // src/scratch.ts
8499
- import { existsSync as existsSync8, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync4 } from "node:fs";
8983
+ import { existsSync as existsSync10, mkdtempSync, readdirSync as readdirSync7, rmSync as rmSync4 } from "node:fs";
8500
8984
  import { tmpdir as tmpdir2 } from "node:os";
8501
- import { join as join9 } from "node:path";
8985
+ import { join as join11 } from "node:path";
8502
8986
  import process8 from "node:process";
8503
8987
  function scratchDirectory(kind) {
8504
- return mkdtempSync(join9(tmpdir2(), `gdharness-${kind}-${process8.pid}-`));
8988
+ return mkdtempSync(join11(tmpdir2(), `gdharness-${kind}-${process8.pid}-`));
8505
8989
  }
8506
8990
  function sweepAbandonedScratch(directory = tmpdir2()) {
8507
8991
  let entries;
8508
8992
  try {
8509
- entries = readdirSync5(directory);
8993
+ entries = readdirSync7(directory);
8510
8994
  } catch {
8511
8995
  return;
8512
8996
  }
@@ -8517,7 +9001,7 @@ function sweepAbandonedScratch(directory = tmpdir2()) {
8517
9001
  continue;
8518
9002
  }
8519
9003
  try {
8520
- remove(join9(directory, entry));
9004
+ remove(join11(directory, entry));
8521
9005
  } catch {}
8522
9006
  }
8523
9007
  }
@@ -8555,7 +9039,7 @@ function tryAgain(path, attempt) {
8555
9039
  lastTryOnExit();
8556
9040
  }
8557
9041
  const after = AGAIN_AFTER_MS[attempt];
8558
- if (after === undefined || !existsSync8(path)) {
9042
+ if (after === undefined || !existsSync10(path)) {
8559
9043
  waiting.delete(path);
8560
9044
  if (after === undefined) {
8561
9045
  atExit.add(path);
@@ -8744,6 +9228,29 @@ function ignoredYetRan(report, ignored) {
8744
9228
  }
8745
9229
  return found;
8746
9230
  }
9231
+ function ignoredLeftOut(report, ignored, declared) {
9232
+ const nameOf = (path) => (path.split("/").at(-1) ?? path).replace(/\.gd$/, "");
9233
+ const leftOut = new Set;
9234
+ const unmatched = [];
9235
+ for (const entry of ignored) {
9236
+ const bare = entry.replace("res://", "");
9237
+ const at = bare.lastIndexOf(":");
9238
+ const suiteNamed = at === -1 ? bare : bare.slice(0, at);
9239
+ const testNamed = at === -1 ? null : bare.slice(at + 1);
9240
+ const named = declared.filter((suite) => nameOf(suite.path) === suiteNamed || suite.path.includes(suiteNamed)).flatMap((suite) => suite.cases.filter((one) => testNamed === null || one === testNamed).map((test) => ({ suite, test })));
9241
+ if (named.length === 0) {
9242
+ unmatched.push(entry);
9243
+ continue;
9244
+ }
9245
+ for (const { suite, test } of named) {
9246
+ const reported = report.suites.filter((ran) => ran.path === suite.path || ran.path === null && ran.name === nameOf(suite.path)).some((ran) => ran.cases.some((one) => one.name === test || new RegExp(`^${test}\\W`).test(one.name)));
9247
+ if (!reported) {
9248
+ leftOut.add(`${nameOf(suite.path)}:${test}`);
9249
+ }
9250
+ }
9251
+ }
9252
+ return { leftOut: [...leftOut].sort(), unmatched };
9253
+ }
8747
9254
  function caseOf(suite, element) {
8748
9255
  const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
8749
9256
  const detail = child.text.trim();
@@ -9104,7 +9611,7 @@ var init_junit = __esm(() => {
9104
9611
  });
9105
9612
 
9106
9613
  // src/script-origins.ts
9107
- import { join as join10 } from "node:path";
9614
+ import { join as join12 } from "node:path";
9108
9615
  function unresolvedClassIn(message) {
9109
9616
  return UNRESOLVED.exec(message)?.[1] ?? null;
9110
9617
  }
@@ -9166,7 +9673,7 @@ function checkWithEngine(godotPath, projectPath) {
9166
9673
  const ran = await runEngine(godotPath, [
9167
9674
  "--headless",
9168
9675
  "--log-file",
9169
- join10(logDir, "engine.log"),
9676
+ join12(logDir, "engine.log"),
9170
9677
  "--path",
9171
9678
  projectPath,
9172
9679
  "--check-only",
@@ -9212,8 +9719,8 @@ var init_script_origins = __esm(() => {
9212
9719
  });
9213
9720
 
9214
9721
  // src/headless.ts
9215
- import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
9216
- import { dirname as dirname7, join as join11 } from "node:path";
9722
+ import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
9723
+ import { dirname as dirname7, join as join13 } from "node:path";
9217
9724
  function snakeCased(params) {
9218
9725
  const result = emptyRecord();
9219
9726
  for (const [key, value] of Object.entries(params)) {
@@ -9225,8 +9732,10 @@ function snakeCased(params) {
9225
9732
  function engineExtras(outcome) {
9226
9733
  const after = outcome.afterAnswerMessages ?? [];
9227
9734
  const before = outcome.beforeOperationMessages ?? [];
9735
+ const notImported = notImportedNote(outcome.messages);
9228
9736
  return {
9229
9737
  ...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
9738
+ ...notImported === undefined ? {} : { engine_messages_note: notImported },
9230
9739
  ...before.length > 0 ? {
9231
9740
  engine_messages_before_operation: before,
9232
9741
  engine_messages_before_operation_note: withNotCompiled(BEFORE_OPERATION_NOTE, before)
@@ -9245,6 +9754,20 @@ function withNotCompiled(note, entries) {
9245
9754
  const path = entries.map(notCompiledIn).find((found) => found !== undefined);
9246
9755
  return path === undefined ? note : `${note} ${path} did not compile, so every run of this project starts its autoloads without it until it is fixed; its errors are among these.`;
9247
9756
  }
9757
+ function notImportedNote(entries) {
9758
+ const paths = [
9759
+ ...new Set(entries.flatMap((entry) => {
9760
+ const path = /^No loader found for resource: (res:\/\/\S+)/.exec(entry.text)?.[1];
9761
+ const extension = path?.split(".").pop()?.toLowerCase() ?? "";
9762
+ return path !== undefined && IMPORTABLE_EXTENSIONS.includes(extension) ? [path] : [];
9763
+ }))
9764
+ ];
9765
+ if (paths.length === 0) {
9766
+ return;
9767
+ }
9768
+ const one = paths.length === 1;
9769
+ return `${paths.join(", ")} ${one ? "has" : "have"} no import the engine can load, which is how a file it imports stands before it is first imported, so what uses ${one ? "it" : "them"} reads ${one ? "it" : "them"} as missing although ${one ? "it is" : "they are"} on disk: project_import reimport imports ${one ? "it" : "them"}.`;
9770
+ }
9248
9771
  function withoutAsides(entries) {
9249
9772
  const kept = [];
9250
9773
  let aside = false;
@@ -9351,7 +9874,7 @@ async function importOnce(godotPath, projectPath) {
9351
9874
  ran = await runEngine(godotPath, [
9352
9875
  "--headless",
9353
9876
  "--log-file",
9354
- join11(logDir, "engine.log"),
9877
+ join13(logDir, "engine.log"),
9355
9878
  "--path",
9356
9879
  projectPath,
9357
9880
  "--import"
@@ -9373,13 +9896,13 @@ async function importOnce(godotPath, projectPath) {
9373
9896
  }
9374
9897
  async function runOperation(engine, operation, params, projectPath) {
9375
9898
  const paramsDir = scratchDirectory("params");
9376
- const paramsFile = join11(paramsDir, `${operation}.json`);
9377
- const answerFile = join11(paramsDir, "answer.json");
9899
+ const paramsFile = join13(paramsDir, `${operation}.json`);
9900
+ const answerFile = join13(paramsDir, "answer.json");
9378
9901
  writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
9379
9902
  const args = [
9380
9903
  "--headless",
9381
9904
  "--log-file",
9382
- join11(paramsDir, "engine.log"),
9905
+ join13(paramsDir, "engine.log"),
9383
9906
  "--path",
9384
9907
  projectPath,
9385
9908
  "--script",
@@ -9467,10 +9990,10 @@ async function runOperation(engine, operation, params, projectPath) {
9467
9990
  };
9468
9991
  }
9469
9992
  function answerIn(file) {
9470
- if (!existsSync9(file)) {
9993
+ if (!existsSync11(file)) {
9471
9994
  return { kind: "none" };
9472
9995
  }
9473
- const text = readFileSync5(file, "utf8");
9996
+ const text = readFileSync7(file, "utf8");
9474
9997
  try {
9475
9998
  const parsed = JSON.parse(text);
9476
9999
  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)}` };
@@ -9485,6 +10008,7 @@ var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, wh
9485
10008
  var init_headless = __esm(() => {
9486
10009
  init_doc_cache();
9487
10010
  init_engine_run();
10011
+ init_import_freshness();
9488
10012
  init_library_copies();
9489
10013
  init_paths();
9490
10014
  init_reimport();
@@ -9493,9 +10017,9 @@ var init_headless = __esm(() => {
9493
10017
  });
9494
10018
 
9495
10019
  // src/update-check.ts
9496
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
10020
+ import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
9497
10021
  import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
9498
- import { join as join12 } from "node:path";
10022
+ import { join as join14 } from "node:path";
9499
10023
  function packageUrl(root) {
9500
10024
  return `${root.replace(/\/+$/, "")}/gdharness/latest`;
9501
10025
  }
@@ -9517,28 +10041,28 @@ function cacheDirectory(environment) {
9517
10041
  };
9518
10042
  const home = set("HOME") ?? homedir4();
9519
10043
  if (process.platform === "win32") {
9520
- return join12(set("LOCALAPPDATA") ?? home, "gdharness");
10044
+ return join14(set("LOCALAPPDATA") ?? home, "gdharness");
9521
10045
  }
9522
10046
  if (process.platform === "darwin") {
9523
- return join12(home, "Library", "Caches", "gdharness");
10047
+ return join14(home, "Library", "Caches", "gdharness");
9524
10048
  }
9525
- return join12(set("XDG_CACHE_HOME") ?? join12(home, ".cache"), "gdharness");
10049
+ return join14(set("XDG_CACHE_HOME") ?? join14(home, ".cache"), "gdharness");
9526
10050
  }
9527
10051
  function cacheFile(environment = process.env) {
9528
10052
  try {
9529
10053
  const directory = cacheDirectory(environment);
9530
10054
  mkdirSync3(directory, { recursive: true, mode: 448 });
9531
- return join12(directory, "update-check.json");
10055
+ return join14(directory, "update-check.json");
9532
10056
  } catch {
9533
- return join12(tmpdir3(), "gdharness-update-check.json");
10057
+ return join14(tmpdir3(), "gdharness-update-check.json");
9534
10058
  }
9535
10059
  }
9536
10060
  function readCache(path) {
9537
10061
  try {
9538
- if (!existsSync10(path)) {
10062
+ if (!existsSync12(path)) {
9539
10063
  return null;
9540
10064
  }
9541
- const parsed = JSON.parse(readFileSync6(path, "utf8"));
10065
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
9542
10066
  if (typeof parsed !== "object" || parsed === null) {
9543
10067
  return null;
9544
10068
  }
@@ -9686,7 +10210,7 @@ var init_update_check = __esm(() => {
9686
10210
  });
9687
10211
 
9688
10212
  // src/server-version.ts
9689
- import { readFileSync as readFileSync7 } from "node:fs";
10213
+ import { readFileSync as readFileSync9 } from "node:fs";
9690
10214
  function staleAddon(addonDigest, shippedDigest) {
9691
10215
  return addonDigest !== shippedDigest;
9692
10216
  }
@@ -9745,7 +10269,7 @@ var init_server_version = __esm(() => {
9745
10269
  DEBUG_MODE = process.env["GDHARNESS_DEBUG"] === "1";
9746
10270
  SERVER_VERSION = (() => {
9747
10271
  try {
9748
- const pkg = JSON.parse(readFileSync7(new URL("../package.json", import.meta.url), "utf8"));
10272
+ const pkg = JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8"));
9749
10273
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
9750
10274
  } catch {
9751
10275
  return "0.0.0";
@@ -9754,7 +10278,7 @@ var init_server_version = __esm(() => {
9754
10278
  });
9755
10279
 
9756
10280
  // src/issues.ts
9757
- import { createHash as createHash2 } from "node:crypto";
10281
+ import { createHash as createHash3 } from "node:crypto";
9758
10282
  import process9 from "node:process";
9759
10283
  function feedbackNotice(every) {
9760
10284
  return JSON.stringify({
@@ -9773,7 +10297,7 @@ function runtime() {
9773
10297
  function defectSignature(where, message) {
9774
10298
  const normalised = `${where}
9775
10299
  ${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
9776
- return createHash2("sha256").update(normalised).digest("hex").slice(0, 8);
10300
+ return createHash3("sha256").update(normalised).digest("hex").slice(0, 8);
9777
10301
  }
9778
10302
  function facts(where, message, godotVersion) {
9779
10303
  return [
@@ -10530,8 +11054,8 @@ var init_gdscript_source = __esm(() => {
10530
11054
  });
10531
11055
 
10532
11056
  // src/class-cache.ts
10533
- import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync6 } from "node:fs";
10534
- import { join as join13, posix } from "node:path";
11057
+ import { existsSync as existsSync13, readdirSync as readdirSync8, readFileSync as readFileSync10, statSync as statSync7 } from "node:fs";
11058
+ import { join as join15, posix } from "node:path";
10535
11059
  function isToolScript(source) {
10536
11060
  return TOOL.test(codeOf(source));
10537
11061
  }
@@ -10550,18 +11074,18 @@ function declaredClassOf(source) {
10550
11074
  }
10551
11075
  function eachScript(projectPath, each) {
10552
11076
  const visit = (directory, prefix) => {
10553
- if (existsSync11(join13(directory, ".gdignore"))) {
11077
+ if (existsSync13(join15(directory, ".gdignore"))) {
10554
11078
  return;
10555
11079
  }
10556
- for (const entry of readdirSync6(directory, { withFileTypes: true })) {
11080
+ for (const entry of readdirSync8(directory, { withFileTypes: true })) {
10557
11081
  if (entry.name.startsWith(".")) {
10558
11082
  continue;
10559
11083
  }
10560
- const path = join13(directory, entry.name);
11084
+ const path = join15(directory, entry.name);
10561
11085
  if (entry.isDirectory()) {
10562
11086
  visit(path, `${prefix}${entry.name}/`);
10563
11087
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
10564
- each(`res://${prefix}${entry.name}`, readFileSync8(path, "utf8"));
11088
+ each(`res://${prefix}${entry.name}`, readFileSync10(path, "utf8"));
10565
11089
  }
10566
11090
  }
10567
11091
  };
@@ -10583,12 +11107,12 @@ function scriptsNaming(projectPath, classes) {
10583
11107
  return naming.sort((a, b) => a.script.localeCompare(b.script));
10584
11108
  }
10585
11109
  function cachedClasses(projectPath) {
10586
- const cache = join13(projectPath, ".godot", "global_script_class_cache.cfg");
10587
- if (!existsSync11(cache)) {
11110
+ const cache = join15(projectPath, ".godot", "global_script_class_cache.cfg");
11111
+ if (!existsSync13(cache)) {
10588
11112
  return null;
10589
11113
  }
10590
11114
  const listed = new Map;
10591
- const text = readFileSync8(cache, "utf8");
11115
+ const text = readFileSync10(cache, "utf8");
10592
11116
  for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
10593
11117
  listed.set(entry[1] ?? "", entry[2] ?? "");
10594
11118
  }
@@ -10881,14 +11405,14 @@ function uncachedForTheEngineNote(projectPath, types) {
10881
11405
  }
10882
11406
  const one = types.length === 1;
10883
11407
  const named = types.map((type) => `${type.type} (${type.declaredIn})`).join(", ");
10884
- const cacheExists = existsSync11(join13(projectPath, ".godot", "global_script_class_cache.cfg"));
11408
+ const cacheExists = existsSync13(join15(projectPath, ".godot", "global_script_class_cache.cfg"));
10885
11409
  const state = cacheExists ? `missing from .godot/global_script_class_cache.cfg, which is where the engine that compiled these scripts resolves a class name from, so the error is the cache being behind the files rather than the script.` : `the engine that compiled these scripts resolves a class name from .godot/global_script_class_cache.cfg, which has not been written yet, as in a project no editor has opened, so the error is the missing cache rather than the script.`;
10886
11410
  return `${named} ${one ? "is" : "are"} declared in this project${cacheExists ? " and" : ", but"} ${state} ` + "project_import refresh_classes writes the cache from the declarations on disk; editor_run start " + "writes it before a game, and an editor opening the project writes it when it scans.";
10887
11411
  }
10888
11412
  function cacheWrittenAt(projectPath) {
10889
- const cache = join13(projectPath, ".godot", "global_script_class_cache.cfg");
11413
+ const cache = join15(projectPath, ".godot", "global_script_class_cache.cfg");
10890
11414
  try {
10891
- return statSync6(cache).mtimeMs;
11415
+ return statSync7(cache).mtimeMs;
10892
11416
  } catch {
10893
11417
  return null;
10894
11418
  }
@@ -10900,14 +11424,14 @@ function declaredSince(projectPath, paths, names, since) {
10900
11424
  return false;
10901
11425
  }
10902
11426
  try {
10903
- return statSync6(join13(projectPath, path.slice("res://".length))).mtimeMs > since;
11427
+ return statSync7(join15(projectPath, path.slice("res://".length))).mtimeMs > since;
10904
11428
  } catch {
10905
11429
  return false;
10906
11430
  }
10907
11431
  });
10908
11432
  }
10909
11433
  function cachedAtMissingPaths(cached, projectPath) {
10910
- return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync11(join13(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
11434
+ return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync13(join15(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
10911
11435
  }
10912
11436
  function staleAgainst(cached, projectPath) {
10913
11437
  return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
@@ -10931,314 +11455,6 @@ var init_class_cache = __esm(() => {
10931
11455
  TOOL = /^[ \t]*(?:@[a-z_]+(?:\([^)\n]*\))?[ \t]+)*@tool\b/m;
10932
11456
  });
10933
11457
 
10934
- // src/project-scan.ts
10935
- import { existsSync as existsSync12, readdirSync as readdirSync7, readFileSync as readFileSync9 } from "node:fs";
10936
- import { extname, join as join14 } from "node:path";
10937
- function skipped(name) {
10938
- return name.startsWith(".") || SKIPPED.has(name);
10939
- }
10940
- function steppedOver(directory) {
10941
- return gdignored(directory) || installedByUs(directory);
10942
- }
10943
- function gdignored(directory) {
10944
- return existsSync12(join14(directory, ".gdignore"));
10945
- }
10946
- function installedByUs(directory) {
10947
- return existsSync12(join14(directory, VERSION_MARKER));
10948
- }
10949
- function projectStructure(projectPath) {
10950
- const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
10951
- const visit = (directory) => {
10952
- if (steppedOver(directory)) {
10953
- return;
10954
- }
10955
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
10956
- if (skipped(entry.name)) {
10957
- continue;
10958
- }
10959
- if (entry.isDirectory()) {
10960
- visit(join14(directory, entry.name));
10961
- } else if (entry.isFile()) {
10962
- const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
10963
- if (SCENE_EXTENSIONS.has(extension)) {
10964
- structure.scenes += 1;
10965
- } else if (extension === "gd" || extension === "cs") {
10966
- structure.scripts += 1;
10967
- } else if (ASSET_EXTENSIONS.has(extension)) {
10968
- structure.assets += 1;
10969
- } else {
10970
- structure.other += 1;
10971
- }
10972
- }
10973
- }
10974
- };
10975
- visit(projectPath);
10976
- return structure;
10977
- }
10978
- function projectFiles(projectPath, extensions) {
10979
- const found = [];
10980
- const visit = (directory, prefix) => {
10981
- if (steppedOver(directory)) {
10982
- return;
10983
- }
10984
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
10985
- if (skipped(entry.name)) {
10986
- continue;
10987
- }
10988
- const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
10989
- if (entry.isDirectory()) {
10990
- visit(join14(directory, entry.name), spelled);
10991
- } else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
10992
- found.push(spelled);
10993
- }
10994
- }
10995
- };
10996
- visit(projectPath, "");
10997
- return found.sort();
10998
- }
10999
- function scriptsWithoutUid(projectPath) {
11000
- const found = [];
11001
- const visit = (directory, prefix) => {
11002
- if (steppedOver(directory)) {
11003
- return;
11004
- }
11005
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
11006
- if (skipped(entry.name)) {
11007
- continue;
11008
- }
11009
- const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
11010
- if (entry.isDirectory()) {
11011
- visit(join14(directory, entry.name), spelled);
11012
- continue;
11013
- }
11014
- if (!entry.isFile()) {
11015
- continue;
11016
- }
11017
- const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
11018
- if (CARRIES_A_UID.has(extension) && !existsSync12(join14(directory, `${entry.name}.uid`))) {
11019
- found.push(spelled);
11020
- }
11021
- }
11022
- };
11023
- visit(projectPath, "");
11024
- return found.sort();
11025
- }
11026
- function pathsInUidCache(projectPath) {
11027
- let data;
11028
- try {
11029
- data = readFileSync9(join14(projectPath, ".godot", "uid_cache.bin"));
11030
- } catch {
11031
- return null;
11032
- }
11033
- if (data.length < 4) {
11034
- return null;
11035
- }
11036
- const count = data.readUInt32LE(0);
11037
- const paths = new Map;
11038
- let at = 4;
11039
- for (let entry = 0;entry < count; entry++) {
11040
- if (at + 12 > data.length) {
11041
- return null;
11042
- }
11043
- const id = data.readBigUInt64LE(at);
11044
- const length = data.readUInt32LE(at + 8);
11045
- at += 12;
11046
- if (at + length > data.length) {
11047
- return null;
11048
- }
11049
- paths.set(id, data.toString("utf8", at, at + length));
11050
- at += length;
11051
- }
11052
- return new Set(paths.values());
11053
- }
11054
- function isBinary(bytes) {
11055
- return bytes.subarray(0, 8000).includes(0);
11056
- }
11057
- function projectTexts(projectPath) {
11058
- const found = [];
11059
- const visit = (directory, prefix) => {
11060
- if (steppedOver(directory)) {
11061
- return;
11062
- }
11063
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
11064
- if (skipped(entry.name)) {
11065
- continue;
11066
- }
11067
- const absolute = join14(directory, entry.name);
11068
- const spelled = `${prefix}${entry.name}`;
11069
- if (entry.isDirectory()) {
11070
- visit(absolute, `${spelled}/`);
11071
- continue;
11072
- }
11073
- const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
11074
- if (!entry.isFile() || BINARY_EXTENSIONS.has(extension)) {
11075
- continue;
11076
- }
11077
- const bytes = readFileSync9(absolute);
11078
- if (!isBinary(bytes)) {
11079
- found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
11080
- }
11081
- }
11082
- };
11083
- visit(projectPath, "");
11084
- return found;
11085
- }
11086
- function ignoredTexts(projectPath) {
11087
- const found = [];
11088
- const visit = (directory, prefix, ignored) => {
11089
- if (installedByUs(directory)) {
11090
- return;
11091
- }
11092
- const under = ignored || gdignored(directory);
11093
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
11094
- if (skipped(entry.name)) {
11095
- continue;
11096
- }
11097
- const absolute = join14(directory, entry.name);
11098
- const spelled = `${prefix}${entry.name}`;
11099
- if (entry.isDirectory()) {
11100
- visit(absolute, `${spelled}/`, under);
11101
- continue;
11102
- }
11103
- const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
11104
- if (!under || !entry.isFile() || BINARY_EXTENSIONS.has(extension) || entry.name === ".gdignore") {
11105
- continue;
11106
- }
11107
- const bytes = readFileSync9(absolute);
11108
- if (!isBinary(bytes)) {
11109
- found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
11110
- }
11111
- }
11112
- };
11113
- visit(projectPath, "", false);
11114
- return found;
11115
- }
11116
- function plainMatch(line, needle, caseSensitive) {
11117
- if (caseSensitive) {
11118
- return line.includes(needle) ? [needle] : null;
11119
- }
11120
- const lowered = line.toLowerCase();
11121
- const at = lowered.indexOf(needle);
11122
- if (at < 0) {
11123
- return null;
11124
- }
11125
- return [lowered.length === line.length ? line.slice(at, at + needle.length) : needle];
11126
- }
11127
- function compiledQuery(query, caseSensitive) {
11128
- try {
11129
- return new RegExp(query, caseSensitive ? "" : "i");
11130
- } catch (error) {
11131
- const why = errorMessage(error).split(": ").at(-1) ?? "";
11132
- throw new Error(`'${query}' is not a regular expression: ${why.charAt(0).toLowerCase()}${why.slice(1)}. With regex true, a character such as ( ) [ ] { } . * + ? | ^ $ or \\ stands for itself only after a backslash, as in \\(; or leave regex out to search for the text as written.`);
11133
- }
11134
- }
11135
- function searchProject(projectPath, options) {
11136
- const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
11137
- const result = {
11138
- query: options.query,
11139
- results: [],
11140
- summary: { files_searched: 0, files_with_matches: 0, total_matches: 0, truncated: false }
11141
- };
11142
- const regex = options.regex ? compiledQuery(options.query, options.caseSensitive) : null;
11143
- const needle = options.caseSensitive ? options.query : options.query.toLowerCase();
11144
- const full = () => result.summary.truncated;
11145
- const visit = (directory) => {
11146
- if (steppedOver(directory)) {
11147
- return;
11148
- }
11149
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
11150
- if (full()) {
11151
- return;
11152
- }
11153
- if (skipped(entry.name)) {
11154
- continue;
11155
- }
11156
- const entryPath = join14(directory, entry.name);
11157
- if (entry.isDirectory()) {
11158
- visit(entryPath);
11159
- continue;
11160
- }
11161
- const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
11162
- if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
11163
- continue;
11164
- }
11165
- const bytes = readFileSync9(entryPath);
11166
- if (extensions === null && isBinary(bytes)) {
11167
- continue;
11168
- }
11169
- result.summary.files_searched += 1;
11170
- const matches = [];
11171
- for (const [index, line] of bytes.toString("utf8").split(/\r?\n/).entries()) {
11172
- const found = regex ? regex.exec(line) : plainMatch(line, needle, options.caseSensitive);
11173
- if (found === null) {
11174
- continue;
11175
- }
11176
- if (result.summary.total_matches >= options.maxResults) {
11177
- result.summary.truncated = true;
11178
- break;
11179
- }
11180
- matches.push({ line: index + 1, content: line.trim(), match: found[0] });
11181
- result.summary.total_matches += 1;
11182
- }
11183
- if (matches.length > 0) {
11184
- const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
11185
- result.results.push({ file: `res://${relativePath}`, matches });
11186
- result.summary.files_with_matches += 1;
11187
- }
11188
- }
11189
- };
11190
- visit(projectPath);
11191
- return result;
11192
- }
11193
- var SKIPPED, VERSION_MARKER = ".gdharness-version", SCENE_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID, BINARY_EXTENSIONS;
11194
- var init_project_scan = __esm(() => {
11195
- init_errors();
11196
- SKIPPED = new Set(["node_modules"]);
11197
- SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
11198
- ASSET_EXTENSIONS = new Set([
11199
- "png",
11200
- "jpg",
11201
- "jpeg",
11202
- "webp",
11203
- "svg",
11204
- "bmp",
11205
- "tga",
11206
- "exr",
11207
- "hdr",
11208
- "ktx",
11209
- "dds",
11210
- "ttf",
11211
- "otf",
11212
- "woff",
11213
- "woff2",
11214
- "fnt",
11215
- "wav",
11216
- "mp3",
11217
- "ogg",
11218
- "glb",
11219
- "gltf",
11220
- "obj",
11221
- "fbx",
11222
- "blend",
11223
- "dae",
11224
- "ogv",
11225
- "gdshader",
11226
- "shader"
11227
- ]);
11228
- CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
11229
- BINARY_EXTENSIONS = new Set([
11230
- ...[...ASSET_EXTENSIONS].filter((extension) => !["svg", "gltf", "gdshader", "shader", "fnt"].includes(extension)),
11231
- "scn",
11232
- "res",
11233
- "zip",
11234
- "pck",
11235
- "exe",
11236
- "dll",
11237
- "so",
11238
- "dylib"
11239
- ]);
11240
- });
11241
-
11242
11458
  // src/answer-json.ts
11243
11459
  function answerJson(value) {
11244
11460
  return JSON.stringify(value, null, 2).replace(SCALAR_ARRAY, (_whole, items) => `[${items.split(/,\n\s+/).join(", ")}]`);
@@ -11250,7 +11466,7 @@ var init_answer_json = __esm(() => {
11250
11466
  });
11251
11467
 
11252
11468
  // src/resources.ts
11253
- import { readFileSync as readFileSync10, realpathSync as realpathSync2 } from "node:fs";
11469
+ import { readFileSync as readFileSync11, realpathSync as realpathSync2 } from "node:fs";
11254
11470
  import { extname as extname2, resolve as resolve3 } from "node:path";
11255
11471
  function ensureProjectPath(getProjectPath) {
11256
11472
  const projectPath = getProjectPath();
@@ -11446,7 +11662,7 @@ function readResourceText(uri, getProjectPath) {
11446
11662
  const parsedUri = parseGodotUri(uri);
11447
11663
  if (parsedUri.kind === "project-info") {
11448
11664
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
11449
- const rawProject = readFileSync10(projectFilePath, "utf-8");
11665
+ const rawProject = readFileSync11(projectFilePath, "utf-8");
11450
11666
  const parsedProject = parseProjectGodot(rawProject);
11451
11667
  return {
11452
11668
  mimeType: "application/json",
@@ -11455,7 +11671,7 @@ function readResourceText(uri, getProjectPath) {
11455
11671
  }
11456
11672
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
11457
11673
  ensureAllowedExtension(parsedUri.kind, filePath);
11458
- const text = readFileSync10(filePath, "utf-8");
11674
+ const text = readFileSync11(filePath, "utf-8");
11459
11675
  return {
11460
11676
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
11461
11677
  text
@@ -11596,19 +11812,19 @@ function readParams(params, key) {
11596
11812
  }
11597
11813
 
11598
11814
  // src/setup.ts
11599
- import { createHash as createHash3 } from "node:crypto";
11815
+ import { createHash as createHash4 } from "node:crypto";
11600
11816
  import {
11601
11817
  cpSync,
11602
- existsSync as existsSync13,
11818
+ existsSync as existsSync14,
11603
11819
  mkdirSync as mkdirSync4,
11604
- readdirSync as readdirSync8,
11605
- readFileSync as readFileSync11,
11820
+ readdirSync as readdirSync9,
11821
+ readFileSync as readFileSync12,
11606
11822
  rmdirSync,
11607
11823
  rmSync as rmSync5,
11608
- statSync as statSync7,
11824
+ statSync as statSync8,
11609
11825
  writeFileSync as writeFileSync6
11610
11826
  } from "node:fs";
11611
- import { dirname as dirname8, join as join15 } from "node:path";
11827
+ import { dirname as dirname8, join as join16 } from "node:path";
11612
11828
  import { fileURLToPath } from "node:url";
11613
11829
  function editorAddonDigest(from = shippedAddonsDirectory()) {
11614
11830
  return addonsDigest(EDITOR_PLUGINS, from);
@@ -11617,27 +11833,27 @@ function runtimeAddonDigest(from = shippedAddonsDirectory()) {
11617
11833
  return addonsDigest([RUNTIME_ADDON], from);
11618
11834
  }
11619
11835
  function addonsDigest(names, from) {
11620
- const hash = createHash3("sha256");
11836
+ const hash = createHash4("sha256");
11621
11837
  const walk = (directory, relative) => {
11622
- for (const name of readdirSync8(directory).sort()) {
11838
+ for (const name of readdirSync9(directory).sort()) {
11623
11839
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
11624
11840
  continue;
11625
11841
  }
11626
- const path = join15(directory, name);
11842
+ const path = join16(directory, name);
11627
11843
  const inside = `${relative}/${name}`;
11628
- if (statSync7(path).isDirectory()) {
11844
+ if (statSync8(path).isDirectory()) {
11629
11845
  walk(path, inside);
11630
11846
  } else {
11631
11847
  hash.update(`${inside}
11632
11848
  `);
11633
- hash.update(readFileSync11(path));
11849
+ hash.update(readFileSync12(path));
11634
11850
  hash.update(`
11635
11851
  `);
11636
11852
  }
11637
11853
  }
11638
11854
  };
11639
11855
  for (const name of names) {
11640
- walk(join15(from, name), name);
11856
+ walk(join16(from, name), name);
11641
11857
  }
11642
11858
  return hash.digest("hex");
11643
11859
  }
@@ -11656,28 +11872,28 @@ function installedRuntimeDigest(projectPath) {
11656
11872
  return digestMarker(projectPath, RUNTIME_ADDON);
11657
11873
  }
11658
11874
  function digestMarker(projectPath, addon) {
11659
- const marker = join15(projectPath, "addons", addon, DIGEST_MARKER);
11660
- if (!existsSync13(marker)) {
11875
+ const marker = join16(projectPath, "addons", addon, DIGEST_MARKER);
11876
+ if (!existsSync14(marker)) {
11661
11877
  return null;
11662
11878
  }
11663
- const said = readFileSync11(marker, "utf8").trim();
11879
+ const said = readFileSync12(marker, "utf8").trim();
11664
11880
  return said === "" ? null : said;
11665
11881
  }
11666
11882
  function shippedAddonsDirectory() {
11667
- return join15(dirname8(fileURLToPath(import.meta.url)), "godot", "addons");
11883
+ return join16(dirname8(fileURLToPath(import.meta.url)), "godot", "addons");
11668
11884
  }
11669
11885
  function shippedOperationsScript() {
11670
- return join15(dirname8(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
11886
+ return join16(dirname8(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
11671
11887
  }
11672
11888
  function shippedListedRunner() {
11673
- return join15(dirname8(fileURLToPath(import.meta.url)), "godot", "tests", "listed.gd");
11889
+ return join16(dirname8(fileURLToPath(import.meta.url)), "godot", "tests", "listed.gd");
11674
11890
  }
11675
11891
  function gdUnit4Unusable(projectPath) {
11676
11892
  const marker = "addons/gdUnit4/plugin.cfg";
11677
- if (!existsSync13(join15(projectPath, ...marker.split("/")))) {
11893
+ if (!existsSync14(join16(projectPath, ...marker.split("/")))) {
11678
11894
  return { missing: true, reason: `gdUnit4 is not installed in this project: no ${marker}` };
11679
11895
  }
11680
- if (!existsSync13(join15(projectPath, ...GDUNIT4_CI_RUNNER.split("/")))) {
11896
+ if (!existsSync14(join16(projectPath, ...GDUNIT4_CI_RUNNER.split("/")))) {
11681
11897
  return {
11682
11898
  missing: false,
11683
11899
  reason: `gdUnit4 in this project is older than 6.0, which the shipped runner needs: no ${GDUNIT4_CI_RUNNER}`
@@ -11688,44 +11904,44 @@ function gdUnit4Unusable(projectPath) {
11688
11904
  function installAddons(projectPath, from = shippedAddonsDirectory()) {
11689
11905
  const installed = [];
11690
11906
  for (const name of ADDONS) {
11691
- const source = join15(from, name);
11692
- if (!existsSync13(join15(source, name === RUNTIME_ADDON ? "runtime_autoload.gd" : "plugin.cfg"))) {
11907
+ const source = join16(from, name);
11908
+ if (!existsSync14(join16(source, name === RUNTIME_ADDON ? "runtime_autoload.gd" : "plugin.cfg"))) {
11693
11909
  throw new Error(`The package holds no ${name} addon at ${source}.`);
11694
11910
  }
11695
- const target = join15(projectPath, "addons", name);
11696
- const replaced = existsSync13(target);
11911
+ const target = join16(projectPath, "addons", name);
11912
+ const replaced = existsSync14(target);
11697
11913
  rmSync5(target, { recursive: true, force: true });
11698
11914
  mkdirSync4(dirname8(target), { recursive: true });
11699
11915
  cpSync(source, target, { recursive: true });
11700
- writeFileSync6(join15(target, VERSION_MARKER), `${SERVER_VERSION}
11916
+ writeFileSync6(join16(target, VERSION_MARKER), `${SERVER_VERSION}
11701
11917
  `);
11702
11918
  installed.push({ name, path: target, replaced });
11703
11919
  }
11704
- writeFileSync6(join15(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
11920
+ writeFileSync6(join16(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
11705
11921
  `);
11706
- writeFileSync6(join15(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
11922
+ writeFileSync6(join16(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
11707
11923
  `);
11708
11924
  return installed;
11709
11925
  }
11710
11926
  function installedAddonVersion(projectPath) {
11711
- const marker = join15(projectPath, "addons", ADDONS[0], VERSION_MARKER);
11712
- if (!existsSync13(marker)) {
11927
+ const marker = join16(projectPath, "addons", ADDONS[0], VERSION_MARKER);
11928
+ if (!existsSync14(marker)) {
11713
11929
  return null;
11714
11930
  }
11715
- const said = readFileSync11(marker, "utf8").trim();
11931
+ const said = readFileSync12(marker, "utf8").trim();
11716
11932
  return said === "" ? null : said;
11717
11933
  }
11718
11934
  function removeAddons(projectPath) {
11719
11935
  const removed = [];
11720
11936
  for (const name of ADDONS) {
11721
- const target = join15(projectPath, "addons", name);
11722
- if (existsSync13(target)) {
11937
+ const target = join16(projectPath, "addons", name);
11938
+ if (existsSync14(target)) {
11723
11939
  rmSync5(target, { recursive: true, force: true });
11724
11940
  removed.push(target);
11725
11941
  }
11726
11942
  }
11727
- const addons = join15(projectPath, "addons");
11728
- if (removed.length > 0 && existsSync13(addons) && readdirSync8(addons).length === 0) {
11943
+ const addons = join16(projectPath, "addons");
11944
+ if (removed.length > 0 && existsSync14(addons) && readdirSync9(addons).length === 0) {
11729
11945
  rmdirSync(addons);
11730
11946
  removed.push(addons);
11731
11947
  }
@@ -11759,7 +11975,7 @@ function loaderAmong(projectPath, named) {
11759
11975
  }
11760
11976
  let source = null;
11761
11977
  try {
11762
- source = readFileSync11(join15(projectPath, path), "utf8");
11978
+ source = readFileSync12(join16(projectPath, path), "utf8");
11763
11979
  } catch {}
11764
11980
  if (source?.includes(RUNTIME_AUTOLOAD.path)) {
11765
11981
  return { name: entry, path, exists: true };
@@ -11801,10 +12017,10 @@ function enabledPlugins(settings) {
11801
12017
  function inspectProject(projectPath) {
11802
12018
  const problems = [];
11803
12019
  const addons = ADDONS.map((name) => {
11804
- const directory = join15(projectPath, "addons", name);
11805
- const marker = join15(directory, VERSION_MARKER);
11806
- const installed = existsSync13(directory);
11807
- const version = existsSync13(marker) ? readFileSync11(marker, "utf8").trim() : null;
12020
+ const directory = join16(projectPath, "addons", name);
12021
+ const marker = join16(directory, VERSION_MARKER);
12022
+ const installed = existsSync14(directory);
12023
+ const version = existsSync14(marker) ? readFileSync12(marker, "utf8").trim() : null;
11808
12024
  const current = version === SERVER_VERSION;
11809
12025
  if (!installed) {
11810
12026
  problems.push(`addons/${name} is not installed; run gdharness setup`);
@@ -11813,7 +12029,7 @@ function inspectProject(projectPath) {
11813
12029
  }
11814
12030
  return { name, installed, version, current };
11815
12031
  });
11816
- const settings = parseProjectGodot(readFileSync11(join15(projectPath, "project.godot"), "utf8"));
12032
+ const settings = parseProjectGodot(readFileSync12(join16(projectPath, "project.godot"), "utf8"));
11817
12033
  const pluginsEnabled = enabledPlugins(settings);
11818
12034
  for (const name of EDITOR_PLUGINS) {
11819
12035
  if (!pluginsEnabled.includes(name)) {
@@ -11829,7 +12045,7 @@ function inspectProject(projectPath) {
11829
12045
  const tracked = trackedByGit(projectPath, [...named.values()]);
11830
12046
  const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
11831
12047
  for (const [name, path] of named) {
11832
- if (!existsSync13(join15(projectPath, path))) {
12048
+ if (!existsSync14(join16(projectPath, path))) {
11833
12049
  if (!addonMissing(path)) {
11834
12050
  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`);
11835
12051
  }
@@ -11982,6 +12198,7 @@ var init_tool_definitions = __esm(() => {
11982
12198
  init_launch();
11983
12199
  PROJECT_INFO_SECTIONS = [
11984
12200
  "autoloads",
12201
+ "classes",
11985
12202
  "input_actions",
11986
12203
  "plugins",
11987
12204
  "export_presets",
@@ -12027,9 +12244,23 @@ var init_tool_definitions = __esm(() => {
12027
12244
  };
12028
12245
  ANIMATION_TRACK = {
12029
12246
  type: "object",
12030
- description: "The track to add.",
12247
+ description: "add_track: the track to add. remove_track: the track to take out, found by its type, nodePath and property as read answers them; its keyframes are not needed.",
12031
12248
  properties: {
12032
- type: { type: "string", enum: ["property", "method"] },
12249
+ type: {
12250
+ type: "string",
12251
+ enum: [
12252
+ "property",
12253
+ "method",
12254
+ "position_3d",
12255
+ "rotation_3d",
12256
+ "scale_3d",
12257
+ "blend_shape",
12258
+ "bezier",
12259
+ "audio",
12260
+ "animation"
12261
+ ],
12262
+ description: "add_track makes property and method tracks; remove_track takes any type read answers."
12263
+ },
12033
12264
  nodePath: { type: "string", description: "Target node, relative to the AnimationPlayer root." },
12034
12265
  property: { type: "string", description: "For property tracks: the property to animate." },
12035
12266
  method: {
@@ -12038,6 +12269,7 @@ var init_tool_definitions = __esm(() => {
12038
12269
  },
12039
12270
  keyframes: {
12040
12271
  type: "array",
12272
+ description: "add_track: the keys to add, each at its own time.",
12041
12273
  items: {
12042
12274
  type: "object",
12043
12275
  properties: {
@@ -12050,7 +12282,7 @@ var init_tool_definitions = __esm(() => {
12050
12282
  }
12051
12283
  }
12052
12284
  },
12053
- required: ["type", "nodePath", "keyframes"],
12285
+ required: ["type", "nodePath"],
12054
12286
  additionalProperties: false
12055
12287
  };
12056
12288
  SCRIPT_MODIFICATIONS = {
@@ -12090,7 +12322,7 @@ var init_tool_definitions = __esm(() => {
12090
12322
  };
12091
12323
  INPUT_EVENTS = {
12092
12324
  type: "array",
12093
- description: "The events that trigger the action.",
12325
+ description: "The events that trigger the action, each bound to every device, as the editor binds them. A read names device only on an event bound to one device.",
12094
12326
  items: {
12095
12327
  type: "object",
12096
12328
  properties: {
@@ -12101,7 +12333,11 @@ var init_tool_definitions = __esm(() => {
12101
12333
  axisValue: { type: "number", description: "Joypad axes: -1 or 1." },
12102
12334
  ctrl: { type: "boolean", description: "Keys: bind it with Ctrl held." },
12103
12335
  alt: { type: "boolean", description: "Keys: bind it with Alt held." },
12104
- shift: { type: "boolean", description: "Keys: bind it with Shift held." }
12336
+ shift: { type: "boolean", description: "Keys: bind it with Shift held." },
12337
+ physical: {
12338
+ type: "boolean",
12339
+ description: "Keys: bind the key by where it sits on the keyboard (physical_keycode) rather than by what it types, as the editor offers, so W stays the key above S on any layout. Default false."
12340
+ }
12105
12341
  },
12106
12342
  required: ["type"],
12107
12343
  additionalProperties: false
@@ -12149,13 +12385,13 @@ var init_tool_definitions = __esm(() => {
12149
12385
  name: "project_info",
12150
12386
  title: "Project info",
12151
12387
  effect: "read",
12152
- description: "What a project is: its name and main scene from project.godot, the Godot that answers, and how many scenes, scripts and assets it holds, other counting every remaining file, leaving out the addons gdharness installed and any folder holding a .gdignore, with optional sections on top.",
12388
+ description: "What a project is: its name and main scene from project.godot, the Godot that answers, and how many scenes, scripts and assets it holds, other counting every remaining file, leaving out the .import and .uid files the engine writes beside others, the addons gdharness installed and any folder holding a .gdignore, with optional sections on top.",
12153
12389
  parameters: {
12154
12390
  projectPath: PROJECT_PATH,
12155
12391
  include: {
12156
12392
  type: "array",
12157
12393
  items: { type: "string", enum: [...PROJECT_INFO_SECTIONS] },
12158
- description: "Extra sections: registered autoloads, the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets, the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, and each file a scene, resource or script refers to that does not exist."
12394
+ description: "Extra sections: registered autoloads, the classes the project's scripts declare with class_name, each with its script, read from the files, the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets, the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, and each file a scene, resource or script refers to that does not exist."
12159
12395
  },
12160
12396
  preset: { type: "string", description: "For validation: the export preset to validate against." },
12161
12397
  detail: {
@@ -12169,12 +12405,12 @@ var init_tool_definitions = __esm(() => {
12169
12405
  {
12170
12406
  name: "project_settings",
12171
12407
  title: "Project settings",
12172
- description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. project_info include lists the autoloads, input actions and audio buses as the project holds them. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, or one on another release's addon that was not asked, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events and deadzone, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change moves no other line; the save is the engine's, and like the editor's it writes Godot's header and config/features into a file that lacks them. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
12408
+ description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. project_info include lists the autoloads, input actions and audio buses as the project holds them. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, or one on another release's addon that was not asked, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events, and its deadzone when one is given, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. unset takes a setting's line out of project.godot, the way back from a set of a misspelt name, and answers removed, old_value and default_value, the value that holds now (null for a setting the engine does not declare); on one the file does not set it answers removed false and writes nothing. Every write keeps the sections and keys of project.godot in the order the file had them, so a change moves no other line; the save is the engine's, and like the editor's it writes Godot's header and config/features into a file that lacks them. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
12173
12409
  parameters: {
12174
12410
  projectPath: PROJECT_PATH,
12175
12411
  setting: {
12176
12412
  type: "string",
12177
- ops: ["get", "set"],
12413
+ ops: ["get", "set", "unset"],
12178
12414
  description: 'Setting path, such as "display/window/size/viewport_width".'
12179
12415
  },
12180
12416
  from: {
@@ -12221,7 +12457,7 @@ var init_tool_definitions = __esm(() => {
12221
12457
  ops: ["add_input_action"],
12222
12458
  minimum: 0,
12223
12459
  maximum: 1,
12224
- description: "Input actions: analogue deadzone, 0 to 1. Default 0.5."
12460
+ description: "Input actions: analogue deadzone, 0 to 1. Default: the deadzone of the action being rebound, or 0.5 for a new one."
12225
12461
  },
12226
12462
  pluginName: {
12227
12463
  type: "string",
@@ -12255,6 +12491,11 @@ var init_tool_definitions = __esm(() => {
12255
12491
  operations: {
12256
12492
  get: { effect: "read", summary: "read one setting, or every setting under prefix", requires: [] },
12257
12493
  set: { effect: "destructive", summary: "write one setting", requires: ["setting", "value"] },
12494
+ unset: {
12495
+ effect: "destructive",
12496
+ summary: "take a setting out of project.godot, so the engine default holds",
12497
+ requires: ["setting"]
12498
+ },
12258
12499
  add_autoload: {
12259
12500
  effect: "additive",
12260
12501
  summary: "register an autoload singleton",
@@ -12300,7 +12541,7 @@ var init_tool_definitions = __esm(() => {
12300
12541
  name: "project_search",
12301
12542
  title: "Search the project",
12302
12543
  effect: "read",
12303
- description: "Searches text or a regular expression across project files and returns file paths with line numbers. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs. The addons gdharness installed are not searched either, since nobody on the project wrote them.",
12544
+ description: "Searches text or a regular expression across project files and returns file paths with line numbers. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore, which not_searched names when the search passed one. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs. The addons gdharness installed are not searched either, since nobody on the project wrote them.",
12304
12545
  parameters: {
12305
12546
  projectPath: PROJECT_PATH,
12306
12547
  query: { type: "string", description: "The text or pattern to find." },
@@ -12326,7 +12567,7 @@ var init_tool_definitions = __esm(() => {
12326
12567
  direction: {
12327
12568
  type: "string",
12328
12569
  enum: ["forward", "reverse"],
12329
- description: "forward: what this resource loads, with cycles reported, a script's dependencies including every script whose class_name it uses, marked by_class with the name. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Each reference carries a kind: ext_resource, extends, preload, load or path for a reference by path; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about. Two kinds are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
12570
+ description: "forward: what this resource loads, with cycles reported, a script's dependencies including every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load or path for a reference by path; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Two kinds are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
12330
12571
  },
12331
12572
  depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
12332
12573
  includeBuiltin: {
@@ -12356,7 +12597,7 @@ var init_tool_definitions = __esm(() => {
12356
12597
  options: {
12357
12598
  type: "object",
12358
12599
  ops: ["set_options"],
12359
- description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads. One already holding the value given is answered under unchanged_options rather than updated_options, and when none changed nothing is written or reimported.`,
12600
+ description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads. One already holding the value given is answered under unchanged_options rather than updated_options, and when none changed nothing is written or reimported. changed gives each option written as {was, now}, was null for one the import file did not hold.`,
12360
12601
  additionalProperties: true
12361
12602
  },
12362
12603
  reimport: {
@@ -12414,7 +12655,7 @@ var init_tool_definitions = __esm(() => {
12414
12655
  {
12415
12656
  name: "project_export",
12416
12657
  title: "Export",
12417
- description: "Export presets and exports. add_preset writes a preset to export_presets.cfg with the keys the editor's Project > Export writes for a new one, every platform option left at its default, and makes it the platform's runnable preset when the platform has none. It answers where this engine's export templates belong and whether the preset's platform has them, since an export needs them; list says the same for every preset. An editor open on the project does not see a preset added this way until it restarts. run names, as alsoWritten, what else the export wrote beside its output, such as the .pck a Windows or Linux build cannot start without.",
12658
+ description: "Export presets and exports. add_preset writes a preset to export_presets.cfg with the keys the editor's Project > Export writes for a new one, every platform option left at its default, and makes it the platform's runnable preset when the platform has none. It answers where this engine's export templates belong and whether the preset's platform has them, since an export needs them; list says the same for every preset. An editor open on the project does not see a preset added this way until it restarts. run names, as alsoWritten, what else the export wrote beside its output, such as the .pck a Windows or Linux build cannot start without. Its entries are the errors and warnings the export printed, the newest two hundred distinct ones, one printed more than once carrying times, and errors and warnings count them; exported is true only when this run wrote the output and printed no error.",
12418
12659
  parameters: {
12419
12660
  projectPath: PROJECT_PATH,
12420
12661
  preset: {
@@ -12450,8 +12691,8 @@ var init_tool_definitions = __esm(() => {
12450
12691
  name: "project_test",
12451
12692
  title: "Run tests",
12452
12693
  effect: "additive",
12453
- description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. What changed on disk since it was imported is then imported before any suite runs, and named under importedFirst. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. A run that found nothing to run is never called a pass, and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and is not listed there, and none are when the project turns off gdUnit4's report/godot/script_error setting; a suite declares them all the same. Needs gdUnit4 under addons/gdUnit4.",
12454
- answer: "time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for. The run is judged as gdharness test judges it: undeclaredErrors names each engine error that no suite declared in its GDHARNESS_EXPECTED_ERRORS constant, with the suite and the case that printed it, and declaredNotPrinted each declared error its case did not print. Either fails the run, and each suite holding one is listed with its count. engineErrors counts every engine error the run printed but those a suite declared. " + IMPORTED_FIRST,
12694
+ description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. What changed on disk since it was imported is then imported before any suite runs, and named under importedFirst. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. A run that found nothing to run is never called a pass, and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but orphan nodes were left comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and is not listed there, and none are when the project turns off gdUnit4's report/godot/script_error setting; a suite declares them all the same. Needs gdUnit4 under addons/gdUnit4.",
12695
+ answer: "passed is the verdict to read. exitCode is the engine's own exit, gdUnit4's code for what it saw, and is 0 beside passed false when the run is failed for something gdUnit4 does not judge, such as an engine error no suite declared. time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for. The run is judged as gdharness test judges it: undeclaredErrors names each engine error that no suite declared in its GDHARNESS_EXPECTED_ERRORS constant, with the suite and the case that printed it, and declaredNotPrinted each declared error its case did not print. A declared text must be the whole error after ERROR: and its space, so a longer error its case printed holding the text is named under printedInstead. Either fails the run, and each suite holding one is listed with its count. engineErrors counts every engine error the run printed but those a suite declared. " + IMPORTED_FIRST,
12455
12696
  parameters: {
12456
12697
  projectPath: PROJECT_PATH,
12457
12698
  path: {
@@ -12462,7 +12703,7 @@ var init_tool_definitions = __esm(() => {
12462
12703
  ignore: {
12463
12704
  type: "array",
12464
12705
  items: { type: "string" },
12465
- description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name". A bare test name is read as a suite name and leaves out nothing; an entry that named a case which ran all the same comes back under ignoreNotApplied with the form that would have left it out.'
12706
+ description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name". A bare test name is read as a suite name and leaves out nothing; an entry that named a case which ran all the same comes back under ignoreNotApplied with the form that would have left it out. Every case the list left out comes back under ignored as suite_name:test_name, so a run called passed says what it did not run, and an entry naming no suite or case under path comes back under ignoreUnmatched.'
12466
12707
  },
12467
12708
  tests: {
12468
12709
  type: "array",
@@ -12493,7 +12734,7 @@ var init_tool_definitions = __esm(() => {
12493
12734
  {
12494
12735
  name: "scene_create",
12495
12736
  title: "Create or save a scene",
12496
- description: `Creates a scene file where none is, saves one, or saves a copy under a new path. create makes a folder the path names that is not there, and names it under madeFolder. A save of a scene open in the editor saves the editor's copy, unsaved changes included. ${WITHOUT_AN_EDITOR}`,
12737
+ description: `Creates a scene file where none is, saves one, or saves a copy under a new path. create makes a folder the path names that is not there, and names it under madeFolder; its answer names the root's type and name, and under script the script the root carries. A save of a scene open in the editor saves the editor's copy, unsaved changes included. ${WITHOUT_AN_EDITOR}`,
12497
12738
  parameters: {
12498
12739
  projectPath: PROJECT_PATH,
12499
12740
  scenePath: SCENE_PATH,
@@ -12531,7 +12772,7 @@ var init_tool_definitions = __esm(() => {
12531
12772
  name: "scene_tree",
12532
12773
  title: "Scene tree",
12533
12774
  effect: "read",
12534
- description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. A node that instances another scene names that file under instanceOf, and the nodes listed under it come from that file rather than this one; its editableChildren says whether this scene, or a scene it inherits, keeps values of its own for them (scene_node set editableChildren). A node in any group answers groups, sorted, from this file and every scene it comes from. Needs no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without making its nodes, so no script of theirs runs (autoloads still start once it has answered, as in every headless call). For the nodes of a game that is running, runtime_inspect tree.",
12775
+ description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. A node that instances another scene names that file under instanceOf, and the nodes listed under it come from that file rather than this one; its editableChildren says whether this scene, or a scene it inherits, keeps values of its own for them (scene_node set editableChildren). The root of a scene inheriting another names the scene it inherits under inherits, and every node that scene or one further down makes names its file under inheritedFrom; this file holds only the values it changes on them. A node in any group answers groups, sorted, from this file and every scene it comes from. Needs no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without making its nodes, so no script of theirs runs (autoloads still start once it has answered, as in every headless call). For the nodes of a game that is running, runtime_inspect tree.",
12535
12776
  parameters: {
12536
12777
  projectPath: PROJECT_PATH,
12537
12778
  scenePath: SCENE_PATH,
@@ -12547,7 +12788,7 @@ var init_tool_definitions = __esm(() => {
12547
12788
  name: "scene_node",
12548
12789
  title: "Scene nodes",
12549
12790
  description: `One node in a scene file: add, read, set, duplicate, reparent or delete it, or paint TileMap cells. Any ClassDB node type or script class can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties, and another scene is instanced by an add with instancePath. A set with properties {"name": "Boss"} renames a node, and answers nodePath as its new path. A rename or a reparent carries the node paths naming the node, as the editor's rename does: NodePath values on nodes (in arrays, dictionaries, built-in resources, %unique paths) and the tracks its players' animations hold, saving a library kept in a .tres of its own, and answers each rewrite under retargeted and each path it left under notRetargeted. The move is followed into every other .tscn that instances or inherits this scene at any depth: the editable children, values, added nodes and connections named by the old path are rewritten, since Godot finds a moved node's values by unique_id but editable children only by path and drops the values kept below them on the next save, and that scene's node paths and tracks are carried as above. A delete rewrites no path, and answers under stillNaming what named the deleted node and now names nothing. A property holding a Resource takes the res:// path of one, so a texture, a material or a theme is a set like any other. A write is refused while the editor holds unsaved changes to the scene. A node this scene inherits takes values of this scene's own, and so does a node inside an instance of another scene once that instance's editableChildren is true; neither can be moved, renamed, copied or deleted, since the other scene makes it again on load. ${WITHOUT_AN_EDITOR}`,
12550
- answer: "get answers nodeType, the node's engine class, with scriptClass when its script declares a class_name; nodePath, its path from the root however it was named; setHere, the properties this scene's own file sets on it; and valuesFrom, the scene every other value comes from, when another scene makes the node. retargeted lists each path a rename or a reparent rewrote, as {scenePath, nodePath, property or animation and track, from, to}, and notRetargeted each it left, with a reason: a track kept in another scene or an imported file, or a value another scene gives that the move left naming nothing. followed names the other scenes a move rewrote, and notFollowed any left as they were, with why. stillNaming lists each node path and track, here and in the scenes instancing or inheriting this one, that named a deleted node, and deletedWithIt each node of this scene's own that went with it, the nodes inside an instance going with the instance. groups lists, sorted, every group a node is in, on a get, an add that gave addGroups and every set that changes them; get adds groupsHere, the groups this file puts it in, when another scene puts it in others. Each of these lists is answered only when it names anything. alsoChanged gives the other properties the engine changed in step with a set, as a Control's offsets with its size, and those a reparent changed to keep the node where it is drawn, as {was, now}.",
12791
+ answer: "get answers nodeType, the node's engine class, with scriptClass when its script declares a class_name; nodePath, its path from the root however it was named; setHere, the properties this scene's own file sets on it; and valuesFrom, the scene every other value comes from, when another scene makes the node. A set that gives a property the value the node has without it (what the scene it comes from gives, or its default) leaves it out of the file, as the editor does, and answers it under notStored, with notStoredNote saying what it follows now. retargeted lists each path a rename or a reparent rewrote, as {scenePath, nodePath, property or animation and track, from, to}, and notRetargeted each it left, with a reason: a track kept in another scene or an imported file, or a value another scene gives that the move left naming nothing. followed names the other scenes a move rewrote, and notFollowed any left as they were, with why. stillNaming lists each node path and track, here and in the scenes instancing or inheriting this one, that named a deleted node, and deletedWithIt each node of this scene's own that went with it, the nodes inside an instance going with the instance. groups lists, sorted, every group a node is in, on a get, an add that gave addGroups and every set that changes them; get adds groupsHere, the groups this file puts it in, when another scene puts it in others. Each of these lists is answered only when it names anything. add and set answer each property given under properties as the node holds it once written; alsoChanged gives the other properties the engine changed in step with a set, as a Control's offsets with its size, and those a reparent changed to keep the node where it is drawn, as {was, now}.",
12551
12792
  parameters: {
12552
12793
  projectPath: PROJECT_PATH,
12553
12794
  scenePath: SCENE_PATH,
@@ -12595,7 +12836,7 @@ var init_tool_definitions = __esm(() => {
12595
12836
  keepGlobalTransform: {
12596
12837
  type: "boolean",
12597
12838
  ops: ["reparent"],
12598
- description: "reparent: whether the node stays where it is drawn, its transform worked out again under the new parent, as the editor's reparent and Node.reparent keep it. A Control keeps its position only. Default true; false keeps its own transform, so it moves with the new parent."
12839
+ description: "reparent: whether the node stays where it is drawn, its transform worked out again under the new parent, as the editor's reparent and Node.reparent keep it. A Control keeps its position only. A Node3D kept in place under a parent scaled unevenly across its turn ends up sheared, which no position, rotation and scale describe, and the answer then carries its transform under alsoChanged and says so under sheared. Default true; false keeps its own transform, so it moves with the new parent."
12599
12840
  },
12600
12841
  index: {
12601
12842
  type: "integer",
@@ -12698,18 +12939,18 @@ var init_tool_definitions = __esm(() => {
12698
12939
  {
12699
12940
  name: "scene_animation",
12700
12941
  title: "Animations",
12701
- description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. The player and the tree are nodes, so scene_node add puts one in the scene first. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. add_track on a path the animation already has a track of that type for adds the keys to that track, as the editor keys a property it already animates, answering existingTrack true and keysAdded beside keys, the track's count; a key at a time that track already has a key at is refused. A key past the animation's end, where it never plays, is written and named under pastLength. remove clears the player's autoplay when it named the animation and names under statesPlayingNothing the states that played it. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write go through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.",
12942
+ description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. The player and the tree are nodes, so scene_node add puts one in the scene first. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. add_track on a path the animation already has a track of that type for adds the keys to that track, as the editor keys a property it already animates, answering existingTrack true and keysAdded beside keys, the track's count; a key at a time that track already has a key at is refused. A key past the animation's end, where it never plays, is written and named under pastLength. remove_track takes out every track of the type, nodePath and property given, with its keys. remove clears the player's autoplay when it named the animation and names under statesPlayingNothing the states that played it, which stay in their trees playing nothing until remove_state takes them out. Every state machine has Start and End, which cannot be added or removed; a transition from Start sets the state it begins in. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write go through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.",
12702
12943
  parameters: {
12703
12944
  projectPath: PROJECT_PATH,
12704
12945
  scenePath: SCENE_PATH,
12705
12946
  playerNodePath: {
12706
12947
  type: "string",
12707
- ops: ["read", "create", "add_track", "set", "rename", "remove"],
12948
+ ops: ["read", "create", "add_track", "remove_track", "set", "rename", "remove"],
12708
12949
  description: "The AnimationPlayer node."
12709
12950
  },
12710
12951
  animationName: {
12711
12952
  type: "string",
12712
- ops: ["read", "create", "add_track", "set", "rename", "remove", "add_state"],
12953
+ ops: ["read", "create", "add_track", "remove_track", "set", "rename", "remove", "add_state"],
12713
12954
  description: "The animation in the player. read: the one to answer with whole, its tracks a list; without it, read lists them, each with the number of its tracks under tracks. add_state: the one that state plays. One the player does not have yet is written all the same, answering animationFound false and why under animationMissing."
12714
12955
  },
12715
12956
  newName: {
@@ -12733,31 +12974,31 @@ var init_tool_definitions = __esm(() => {
12733
12974
  ops: ["create", "set"],
12734
12975
  description: "create and set: keyframe snap in seconds, 0 or more. create: default 0.1."
12735
12976
  },
12736
- track: { ...ANIMATION_TRACK, ops: ["add_track"] },
12977
+ track: { ...ANIMATION_TRACK, ops: ["add_track", "remove_track"] },
12737
12978
  animTreePath: {
12738
12979
  type: "string",
12739
- ops: ["read_states", "add_state", "connect_states"],
12980
+ ops: ["read_states", "add_state", "remove_state", "connect_states", "disconnect_states"],
12740
12981
  description: "The AnimationTree node."
12741
12982
  },
12742
12983
  stateName: {
12743
12984
  type: "string",
12744
- ops: ["add_state"],
12745
- description: "add_state: the state to add. It goes right of the rightmost state in the editor's graph, answered under position."
12985
+ ops: ["add_state", "remove_state"],
12986
+ description: "add_state: the state to add. It goes right of the rightmost state in the editor's graph, answered under position. remove_state: the state to take out, with every transition leaving or reaching it, named under transitionsRemoved."
12746
12987
  },
12747
12988
  stateMachinePath: {
12748
12989
  type: "string",
12749
- ops: ["read_states", "add_state", "connect_states"],
12990
+ ops: ["read_states", "add_state", "remove_state", "connect_states", "disconnect_states"],
12750
12991
  description: "A nested state machine, by the names of the states that lead to it. Default the root."
12751
12992
  },
12752
12993
  fromState: {
12753
12994
  type: "string",
12754
- ops: ["connect_states"],
12755
- description: "connect_states: the state the transition leaves."
12995
+ ops: ["connect_states", "disconnect_states"],
12996
+ description: "connect_states and disconnect_states: the state the transition leaves."
12756
12997
  },
12757
12998
  toState: {
12758
12999
  type: "string",
12759
- ops: ["connect_states"],
12760
- description: "connect_states: the state it arrives at."
13000
+ ops: ["connect_states", "disconnect_states"],
13001
+ description: "connect_states and disconnect_states: the state it arrives at."
12761
13002
  },
12762
13003
  transitionType: {
12763
13004
  type: "string",
@@ -12793,6 +13034,11 @@ var init_tool_definitions = __esm(() => {
12793
13034
  summary: "a property or method track with keyframes",
12794
13035
  requires: ["playerNodePath", "animationName", "track"]
12795
13036
  },
13037
+ remove_track: {
13038
+ effect: "destructive",
13039
+ summary: "a track and its keys from an animation",
13040
+ requires: ["playerNodePath", "animationName", "track"]
13041
+ },
12796
13042
  set: {
12797
13043
  effect: "destructive",
12798
13044
  summary: "an animation's length, loop mode or step",
@@ -12813,10 +13059,20 @@ var init_tool_definitions = __esm(() => {
12813
13059
  summary: "a state playing an animation, in an AnimationTree",
12814
13060
  requires: ["animTreePath", "stateName", "animationName"]
12815
13061
  },
13062
+ remove_state: {
13063
+ effect: "destructive",
13064
+ summary: "a state and its transitions from an AnimationTree",
13065
+ requires: ["animTreePath", "stateName"]
13066
+ },
12816
13067
  connect_states: {
12817
13068
  effect: "additive",
12818
13069
  summary: "a transition between two states",
12819
13070
  requires: ["animTreePath", "fromState", "toState"]
13071
+ },
13072
+ disconnect_states: {
13073
+ effect: "destructive",
13074
+ summary: "the transition from one state to another",
13075
+ requires: ["animTreePath", "fromState", "toState"]
12820
13076
  }
12821
13077
  }
12822
13078
  },
@@ -12880,7 +13136,7 @@ var init_tool_definitions = __esm(() => {
12880
13136
  {
12881
13137
  name: "script_info",
12882
13138
  title: "Script info",
12883
- description: "What a script contains: its structure from the file, or symbols, completions and hover text from the editor's language server.",
13139
+ description: "What a script contains: its structure from the file, or symbols, completions and hover text from the editor's language server. It reads .gd files only, and answers what a script uses rather than what uses it: project_search finds where a class, a member or a signal is named.",
12884
13140
  parameters: {
12885
13141
  projectPath: PROJECT_PATH,
12886
13142
  scriptPath: SCRIPT_PATH,
@@ -12906,7 +13162,7 @@ var init_tool_definitions = __esm(() => {
12906
13162
  operations: {
12907
13163
  structure: {
12908
13164
  effect: "read",
12909
- summary: "functions, variables (static ones marked is_static; one written with := has the type of a literal or built-in constructor it is given, marked inferred, as a constant always does), signals, constants, enums, inner_classes (each with its name, line and extends, and its own members in these same fields), class_name, extends and line_count, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class; autoloads_used names each autoload the code uses by its global name, with the autoload's script, which the project loads rather than this script, so it is not under dependencies",
13165
+ summary: "functions, variables (static ones marked is_static; one written with := has the type of a literal or built-in constructor it is given, marked inferred, as a constant always does), signals, constants, enums, inner_classes (each with its name, line and extends, and its own members in these same fields), class_name, extends and line_count, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class; autoloads_used names each autoload the code uses by its global name, with the autoload's script, which the project loads rather than this script, so it is not under dependencies. A member or inner class with a ## documentation comment directly above it carries the comment's text as doc, and the script's own, the ## block above class_name or extends or one followed by a blank line before any member, comes back as class_doc",
12910
13166
  requires: []
12911
13167
  },
12912
13168
  symbols: {
@@ -12941,7 +13197,7 @@ var init_tool_definitions = __esm(() => {
12941
13197
  {
12942
13198
  name: "resource_edit",
12943
13199
  title: "Edit resources",
12944
- description: 'Resource files: read one, create any ClassDB resource as .tres, change one, write a shader, build a TileSet, or set a Theme colour or font size. A material is a create with resourceType StandardMaterial3D, ShaderMaterial or CanvasItemMaterial. read answers with the properties the file sets, in the form modify takes, with resourceType the engine class and scriptClass the class_name of its script when it declares one, and a Theme lists its items there as "Button/colors/font_color". A create or modify whose properties made the engine change others, as a Gradient given more colours than offsets gives each new point an offset of 0, names each of those under alsoChanged with what it held before and holds now. A property holding a resource takes its res:// path; a file never imported is refused with the reimport that imports it. move takes any file of the project, a script or an image as much as a resource, or a directory, and works from the files, so it needs no editor; one serving the project is brought up to it as after a move in its own FileSystem dock, its open scenes and cached resources named for the new paths and the project settings the move rewrote taken from the file. read needs no editor either: with none serving the project, a headless engine reads the file. The ops that write a resource go through the editor when one serves the project; with none, a headless engine runs the tool the editor would and the answer says so under writtenBy. That engine runs the script of the resource, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before the save, and a resource where that cannot be done is refused and the file left as it was.',
13200
+ description: 'Resource files: read one, create any ClassDB resource as .tres, change one, write a shader, build a TileSet, or set a Theme colour or font size. A material is a create with resourceType StandardMaterial3D, ShaderMaterial or CanvasItemMaterial. read answers with the properties the file sets, in the form modify takes, with resourceType the engine class and scriptClass the class_name of its script when it declares one, and a Theme lists its items there as "Button/colors/font_color". create and modify answer each property given under properties as the file holds it once saved, in the same form. A create or modify whose properties made the engine change others, as a Gradient given more colours than offsets gives each new point an offset of 0, names each of those under alsoChanged with what it held before and holds now. A property holding a resource takes its res:// path; a file never imported is refused with the reimport that imports it. move takes any file of the project, a script or an image as much as a resource, or a directory, and works from the files, so it needs no editor; one serving the project is brought up to it as after a move in its own FileSystem dock, its open scenes and cached resources named for the new paths and the project settings the move rewrote taken from the file. read needs no editor either: with none serving the project, a headless engine reads the file. The ops that write a resource go through the editor when one serves the project; with none, a headless engine runs the tool the editor would and the answer says so under writtenBy. That engine runs the script of the resource, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before the save, and a resource where that cannot be done is refused and the file left as it was.',
12945
13201
  parameters: {
12946
13202
  projectPath: PROJECT_PATH,
12947
13203
  resourcePath: {
@@ -13228,7 +13484,7 @@ var init_tool_definitions = __esm(() => {
13228
13484
  {
13229
13485
  name: "editor_output",
13230
13486
  title: "Game and editor output",
13231
- description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries: clean is true when no error was reported, and warnings, counted beside it, do not make it false. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
13487
+ description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. A run answers with counts and a verdict: clean is true when no error was reported, and warnings, counted beside it, do not make it false. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one reported a game as playing fifteen seconds after its process was ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, where the output worth reading usually is, since the class cache, the plugins and the language server speak before any harness connects. It answers counts with no clean, since an editor neither passes nor fails, the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
13232
13488
  answer: "Each entry's source says where it was read: transcript for a line of the run's transcript file, which holds both its streams and is how every run with one is read; stdout or stderr for a line of that stream of a run with no transcript, such as an editor-played run picked up after a reconnect whose first server left no note; debugger for what the editor's debug adapter reported and the game never printed, such as the script error a game stopped on.",
13233
13489
  parameters: {
13234
13490
  projectPath: {
@@ -13294,7 +13550,7 @@ var init_tool_definitions = __esm(() => {
13294
13550
  name: "editor_rescan",
13295
13551
  title: "Rescan the project",
13296
13552
  effect: "additive",
13297
- description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. A scan or an import the editor is already running is waited out first rather than scanned over, which would start a second import over it, and the answer says how long under waitedForEditorScanMs. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. The walk also passes over a file whose .uid alone changed, so the editor keeps the old UID and every engine reading its cache warns of a duplicate; before scanning, each file whose .uid names a UID the editor does not hold for it is read again and named under uidsReread, and files on disk naming one UID between them are named under uidsDuplicatedOnDisk, which reading again cannot settle. A class the scan brings in is named under broughtIn, and every script naming it is reloaded in the editor and named under dependentsReloaded: what the editor compiled from them while it could not see the class stays compiled that way after the scan, and a script extending a new base class went on reporting \"Could not find base class\" until reloaded. One that would not reload is named under dependentsNotReloaded and the call is not ok, as it is not when the script named in reloadScript was not reloaded. An editor importing answers nothing until the import is over, and a hidden one imports on the thread that answers, so a large addon install can leave the rescan itself unanswered: that is waited for as any busy editor is, within timeoutMs, and when the wait runs out first the answer is stillWorking. The classes the editor could not see before that scan are then noted in the project, and the next editor_rescan of the same editor, from any server, names them under broughtIn and reloads what names them, which it could not otherwise know to do, since by then the editor sees them. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
13553
+ description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. A scan or an import the editor is already running is waited out first rather than scanned over, which would start a second import over it. waitedMs is the whole call, and waitedForEditorScanMs, given only when a scan or an import was already running, the part spent waiting it out. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. The walk also passes over a file whose .uid alone changed, so the editor keeps the old UID and every engine reading its cache warns of a duplicate; before scanning, each file whose .uid names a UID the editor does not hold for it is read again and named under uidsReread, and files on disk naming one UID between them are named under uidsDuplicatedOnDisk, which reading again cannot settle. A class the scan brings in is named under broughtIn, and every script naming it is reloaded in the editor and named under dependentsReloaded: what the editor compiled from them while it could not see the class stays compiled that way after the scan, and a script extending a new base class went on reporting \"Could not find base class\" until reloaded. One that would not reload is named under dependentsNotReloaded and the call is not ok, as it is not when the script named in reloadScript was not reloaded. An editor importing answers nothing until the import is over, and a hidden one imports on the thread that answers, so a large addon install can leave the rescan itself unanswered: that is waited for as any busy editor is, within timeoutMs, and when the wait runs out first the answer is stillWorking. The classes the editor could not see before that scan are then noted in the project, and the next editor_rescan of the same editor, from any server, names them under broughtIn and reloads what names them, which it could not otherwise know to do, since by then the editor sees them. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
13298
13554
  parameters: {
13299
13555
  projectPath: PROJECT_PATH,
13300
13556
  timeoutMs: {
@@ -13456,7 +13712,7 @@ var init_tool_definitions = __esm(() => {
13456
13712
  tree: { effect: "read", summary: "the live scene tree", requires: [] },
13457
13713
  text: {
13458
13714
  effect: "read",
13459
- summary: "every line of text under nodePath, the values in its fields included, in reading order, leaving out what is hidden and everything under it. Every line reads as drawn: rich text without its tags, text only as far as it has been typed out, a translation rather than its key, an upper-case label in capitals and a secret field as its mask. Tab titles, menu bar titles, list items, a tree's column titles and rows, and an open menu's items and the titles of its separators read a line each",
13715
+ summary: "every line of text under nodePath, the values in its fields included, in reading order, leaving out what is hidden and everything under it. Every line reads as drawn: rich text without tags, text only as far as it is typed out, a translation rather than its key, an upper-case label in capitals and a secret field as its mask. Tab titles, menu bar titles, list items, a tree's column titles and rows, and an open menu's items and the titles of its separators read a line each",
13460
13716
  requires: []
13461
13717
  },
13462
13718
  find: {
@@ -13476,7 +13732,7 @@ var init_tool_definitions = __esm(() => {
13476
13732
  },
13477
13733
  metrics: {
13478
13734
  effect: "read",
13479
- 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, 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. 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",
13735
+ summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of the last frames_timed frames (up to 120; the game, physics and the wait for the display included), frame_time_max the slowest, 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. process_frames and physics_frames count frames and physics steps since the start; get_frames_drawn() stays 0 headless. physics_steps_per_second is the physics steps 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, set once a second: the last second's frames and its slowest process and physics step, so they trail a change by up to a second, and the last two are worst cases, not the frame. Memory, object and render counts are read as asked. memory_static is the engine's CPU-side allocation in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are the renderer's GPU memory in bytes, which memory_static leaves out",
13480
13736
  requires: []
13481
13737
  },
13482
13738
  accessibility: {
@@ -13490,7 +13746,7 @@ var init_tool_definitions = __esm(() => {
13490
13746
  {
13491
13747
  name: "runtime_invoke",
13492
13748
  title: "Set or call in the running game",
13493
- description: "Sets a property or calls a method on a node, or on one of the engine singletons nodePath names, in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. A method that awaits is answered with the value it returns once it ends, and awaited true; elapsed_usec then runs to its end, and one that outlasts timeout is collected with op result. Errors the engine reported while a called method ran come back under errors and warnings under warnings, the first 20 of each, with errors_not_shown and warnings_not_shown counting the rest. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
13749
+ description: 'Sets a property or calls a method on a node, or on one of the engine singletons nodePath names, in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. A method that awaits is answered with the value it returns once it ends, and awaited true; elapsed_usec then runs to its end, and one that outlasts timeout is collected with op result. Errors the engine reported while a called method ran come back under errors and warnings under warnings, the first 20 of each, with errors_not_shown and warnings_not_shown counting the rest. A script error ends the function it is raised in, which then returns the default of its return type, 0, "" or null, so one that ended the called method itself is named under ended_by_error_at, and result is not a value the method returned. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes. A game this server started that its debugger holds is said so, with where, before a call and under heldAt when a call reached a breakpoint.',
13494
13750
  parameters: {
13495
13751
  projectPath: RUNNING_PROJECT_PATH,
13496
13752
  pid: RUNNING_PID,
@@ -13615,7 +13871,7 @@ var init_tool_definitions = __esm(() => {
13615
13871
  name: "runtime_input",
13616
13872
  title: "Send input to the game",
13617
13873
  description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button, mouse motion or drag. All of it works headless, where the viewport is the project's window size and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
13618
- answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again and pressed whether it is left held down (true only after pressed true), for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer. A mouse_click, mouse_motion or mouse_drag with a point outside the window's viewport says so under outside_viewport, with the viewport's rectangle, since no control there receives it.`,
13874
+ answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again and pressed whether it is left held down (true only after pressed true), for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. key, mouse_click and click answer actions, the InputMap actions the event also presses. mouse_click answers hovered, the control under the point afterwards; the other events are aimed at nothing. A click that did not land on a control whose mouse_filter is ignore, as a Label's is, says so under why_not_landed. An action or a key answers focused with the path of the Control holding the keyboard focus when it went in, since that Control gets it before the game's _unhandled_input: Space or Enter on a focused button presses the button. A mouse_click, mouse_motion or mouse_drag with a point outside the window's viewport says so under outside_viewport, with the viewport's rectangle, since no control there receives it.`,
13619
13875
  parameters: {
13620
13876
  projectPath: RUNNING_PROJECT_PATH,
13621
13877
  pid: RUNNING_PID,
@@ -13632,7 +13888,7 @@ var init_tool_definitions = __esm(() => {
13632
13888
  action: {
13633
13889
  type: "string",
13634
13890
  ops: ["action"],
13635
- description: "action: the InputMap action name. An engine dialog is not answered this way: AcceptDialog reads the Escape key itself and never asks the InputMap, so ui_cancel goes in and the question stays up. Dismiss one with the key op and keycode Escape, or click its button."
13891
+ description: "action: the InputMap action name. A Viewport never hands an action to a shortcut, so the buttons whose Shortcut holds the action are sent it as their Shortcut too, and pressed as by the key bound to it: one button at most, the last in the tree that takes it, passing over one that is disabled, hidden or outside its shortcut context. The answer names those buttons under shortcuts and the one pressed under shortcut_pressed, null when none took it. An engine dialog is not answered this way: AcceptDialog reads the Escape key itself and never asks the InputMap, so ui_cancel goes in and the question stays up. Dismiss one with the key op and keycode Escape, or click its button."
13636
13892
  },
13637
13893
  pressed: {
13638
13894
  type: "boolean",
@@ -13897,7 +14153,7 @@ var init_tool_definitions = __esm(() => {
13897
14153
  {
13898
14154
  name: "debug_control",
13899
14155
  title: "Step the debugger",
13900
- 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.",
14156
+ 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. For such a game a step that ends in gdharness's runtime addon, as one past a function's last line does when the addon's _process runs next, is stepped on to the game's own script as a pause is; and since the engine reports every stop the same way, a step that stops on a line holding a breakpoint says onBreakpoint under held, whether the step ended there or ran into it.",
13901
14157
  parameters: {},
13902
14158
  requires: [],
13903
14159
  operations: {
@@ -13954,6 +14210,7 @@ var init_headless_operations = __esm(() => {
13954
14210
  project_settings: dictionary({
13955
14211
  get: "get_project_setting",
13956
14212
  set: "set_project_setting",
14213
+ unset: "unset_project_setting",
13957
14214
  add_autoload: "add_autoload",
13958
14215
  remove_autoload: "remove_autoload",
13959
14216
  set_main_scene: "set_main_scene",
@@ -14051,9 +14308,9 @@ function whatItCanChange(tool) {
14051
14308
  if (destroying.length === 0) {
14052
14309
  return "Adds or starts things, and overwrites and removes nothing itself.";
14053
14310
  }
14054
- return `Can overwrite, remove or end what is there, through ${listed(destroying)}.`;
14311
+ return `Can overwrite, remove or end what is there, through ${listed2(destroying)}.`;
14055
14312
  }
14056
- function listed(items) {
14313
+ function listed2(items) {
14057
14314
  const last = items.at(-1) ?? "";
14058
14315
  return items.length === 1 ? last : `${items.slice(0, -1).join(", ")} and ${last}`;
14059
14316
  }
@@ -14111,30 +14368,30 @@ var init_tool_reference = __esm(() => {
14111
14368
  });
14112
14369
 
14113
14370
  // src/desktop.ts
14114
- import { createHash as createHash4 } from "node:crypto";
14371
+ import { createHash as createHash5 } from "node:crypto";
14115
14372
  import {
14116
14373
  closeSync,
14117
- existsSync as existsSync15,
14374
+ existsSync as existsSync16,
14118
14375
  mkdtempSync as mkdtempSync2,
14119
14376
  openSync,
14120
- readFileSync as readFileSync12,
14377
+ readFileSync as readFileSync13,
14121
14378
  rmSync as rmSync7,
14122
- statSync as statSync8,
14379
+ statSync as statSync9,
14123
14380
  writeFileSync as writeFileSync8,
14124
14381
  writeSync
14125
14382
  } from "node:fs";
14126
14383
  import { tmpdir as tmpdir4 } from "node:os";
14127
- import { join as join17 } from "node:path";
14384
+ import { join as join18 } from "node:path";
14128
14385
  import { setTimeout as delay } from "node:timers/promises";
14129
14386
  function helperAssembly(directory = tmpdir4()) {
14130
- return join17(directory, `gdharness-desktop-${DIGEST}.dll`);
14387
+ return join18(directory, `gdharness-desktop-${DIGEST}.dll`);
14131
14388
  }
14132
14389
  async function ensureHelperCompiled(directory = tmpdir4(), budgetMs = HELPER_COMPILE_MS, wait = true) {
14133
14390
  const assembly = helperAssembly(directory);
14134
14391
  const lock = `${assembly}.compiling`;
14135
14392
  const until = Date.now() + budgetMs;
14136
14393
  while (Date.now() < until) {
14137
- if (existsSync15(assembly)) {
14394
+ if (existsSync16(assembly)) {
14138
14395
  return null;
14139
14396
  }
14140
14397
  let mine = false;
@@ -14152,8 +14409,8 @@ async function ensureHelperCompiled(directory = tmpdir4(), budgetMs = HELPER_COM
14152
14409
  }
14153
14410
  }
14154
14411
  try {
14155
- const holder = Number(readFileSync12(lock, "utf8").trim());
14156
- const old = Date.now() - statSync8(lock).mtimeMs > budgetMs;
14412
+ const holder = Number(readFileSync13(lock, "utf8").trim());
14413
+ const old = Date.now() - statSync9(lock).mtimeMs > budgetMs;
14157
14414
  if (old || Number.isInteger(holder) && holder > 0 && !alive(holder)) {
14158
14415
  rmSync7(lock, { force: true });
14159
14416
  continue;
@@ -14167,8 +14424,8 @@ async function ensureHelperCompiled(directory = tmpdir4(), budgetMs = HELPER_COM
14167
14424
  return `the desktop helper was not compiled within ${String(budgetMs)} ms`;
14168
14425
  }
14169
14426
  async function compileHelper(directory, assembly, budgetMs) {
14170
- const scratch = mkdtempSync2(join17(directory, "gdharness-desktop-source-"));
14171
- const source = join17(scratch, "helper.cs");
14427
+ const scratch = mkdtempSync2(join18(directory, "gdharness-desktop-source-"));
14428
+ const source = join18(scratch, "helper.cs");
14172
14429
  writeFileSync8(source, SOURCE, "utf8");
14173
14430
  const literal = (path) => `'${path.replaceAll("'", "''")}'`;
14174
14431
  const script = [
@@ -14206,7 +14463,7 @@ async function compileHelper(directory, assembly, budgetMs) {
14206
14463
  });
14207
14464
  compiler.once("close", (code) => {
14208
14465
  clearTimeout(timer);
14209
- resolve(existsSync15(assembly) ? null : `Windows PowerShell could not compile it${code === null ? " in time" : ""}: ${complained.trim() || `exit code ${String(code)}`}`);
14466
+ resolve(existsSync16(assembly) ? null : `Windows PowerShell could not compile it${code === null ? " in time" : ""}: ${complained.trim() || `exit code ${String(code)}`}`);
14210
14467
  });
14211
14468
  });
14212
14469
  } finally {
@@ -14431,11 +14688,11 @@ public static class GdharnessDesktop {
14431
14688
  var init_desktop = __esm(() => {
14432
14689
  init_alive();
14433
14690
  init_spawn();
14434
- DIGEST = createHash4("sha256").update(SOURCE).digest("hex").slice(0, 16);
14691
+ DIGEST = createHash5("sha256").update(SOURCE).digest("hex").slice(0, 16);
14435
14692
  });
14436
14693
 
14437
14694
  // src/suite-judge.ts
14438
- import { readFileSync as readFileSync15 } from "node:fs";
14695
+ import { readFileSync as readFileSync16 } from "node:fs";
14439
14696
  import { stripVTControlCharacters } from "node:util";
14440
14697
  function secondsOf(time) {
14441
14698
  let ms = 0;
@@ -14540,9 +14797,16 @@ function readRun(printed) {
14540
14797
  const unloadable = scriptErrorsPrinted(lines).map((error) => `${error.path}${error.line === null ? "" : `:${String(error.line)}`}: ${error.message}`);
14541
14798
  return { lines, suites, errors, leaks, starved, executed, unloadable, stoppedIn };
14542
14799
  }
14800
+ function withNestedErrors(printed, piped) {
14801
+ const nested = piped.flatMap((entry, at) => {
14802
+ const next = piped[at + 1];
14803
+ return entry.severity === "info" && entry.text.trim() === NESTED_ERROR && next?.severity === "error" ? [{ text: next.text, at: printed.lines.length, suite: null, during: null }] : [];
14804
+ });
14805
+ return nested.length === 0 ? printed : { ...printed, errors: [...printed.errors, ...nested] };
14806
+ }
14543
14807
  function readExpected(file) {
14544
14808
  try {
14545
- const parsed = JSON.parse(readFileSync15(file, "utf8"));
14809
+ const parsed = JSON.parse(readFileSync16(file, "utf8"));
14546
14810
  return typeof parsed === "object" && parsed !== null ? parsed : {};
14547
14811
  } catch {
14548
14812
  return {};
@@ -14710,7 +14974,7 @@ ${error.during}`);
14710
14974
  const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text })));
14711
14975
  return { printed: unexpected, neverPrinted };
14712
14976
  }
14713
- var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, EXECUTED, DRIVER_FALLBACK, LEAK, STARVED, WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_EXITS, TAIL = 60, MOST_NAMED = 8;
14977
+ var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, EXECUTED, DRIVER_FALLBACK, LEAK, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_EXITS, TAIL = 60, MOST_NAMED = 8;
14714
14978
  var init_suite_judge = __esm(() => {
14715
14979
  init_junit();
14716
14980
  SUITE_OPENED = /^Run Test Suite:\s*(res:\/\/.+?\.gd)\s*$/;
@@ -14776,7 +15040,7 @@ var init_whole_file = __esm(() => {
14776
15040
  });
14777
15041
 
14778
15042
  // src/suite-plan.ts
14779
- import { existsSync as existsSync17, mkdirSync as mkdirSync7, readFileSync as readFileSync16 } from "node:fs";
15043
+ import { existsSync as existsSync18, mkdirSync as mkdirSync7, readFileSync as readFileSync17 } from "node:fs";
14780
15044
  import { dirname as dirname10, posix as posix3 } from "node:path";
14781
15045
 
14782
15046
  class ProjectScripts {
@@ -14865,7 +15129,7 @@ class ProjectScripts {
14865
15129
  }
14866
15130
  function readTimes(file) {
14867
15131
  try {
14868
- const parsed = JSON.parse(readFileSync16(file, "utf8"));
15132
+ const parsed = JSON.parse(readFileSync17(file, "utf8"));
14869
15133
  return typeof parsed === "object" && parsed !== null ? parsed : {};
14870
15134
  } catch {
14871
15135
  return {};
@@ -14946,19 +15210,27 @@ function unitsOf(suites, times, splitPast = SPLIT_PAST_SECONDS) {
14946
15210
  function dealt(units, declared, most) {
14947
15211
  const windowed = units.filter((unit) => unit.windowed);
14948
15212
  const headless = units.filter((unit) => !unit.windowed).sort((a, b) => b.cost - a.cost || a.suite.localeCompare(b.suite) || (a.cases[0] ?? "").localeCompare(b.cases[0] ?? ""));
14949
- const total = headless.reduce((sum, unit) => sum + unit.cost, 0);
14950
- const slowest = headless[0]?.cost ?? 0;
14951
- const room = Math.max(1, most - (windowed.length > 0 && most > 1 ? 1 : 0));
14952
- const count = headless.length === 0 ? 0 : Math.max(1, Math.min(room, headless.length, Math.ceil(total / Math.max(slowest, ENGINE_START_SECONDS))));
14953
- const bins = Array.from({ length: count }, () => []);
14954
- const loads = Array.from({ length: count }, () => 0);
15213
+ if (units.length === 0) {
15214
+ return [];
15215
+ }
15216
+ const together = windowed.reduce((sum, unit) => sum + unit.cost, 0);
15217
+ const total = units.reduce((sum, unit) => sum + unit.cost, 0);
15218
+ const slowest = Math.max(together, headless[0]?.cost ?? 0);
15219
+ const count = Math.max(1, Math.min(most, headless.length + (windowed.length > 0 ? 1 : 0), Math.ceil(total / Math.max(slowest, ENGINE_START_SECONDS))));
15220
+ const bins = Array.from({ length: count }, (_, index) => index === 0 ? [...windowed] : []);
15221
+ const loads = Array.from({ length: count }, (_, index) => index === 0 ? together : 0);
14955
15222
  for (const unit of headless) {
14956
15223
  const lightest = loads.indexOf(Math.min(...loads));
14957
15224
  bins[lightest]?.push(unit);
14958
15225
  loads[lightest] = (loads[lightest] ?? 0) + unit.cost;
14959
15226
  }
14960
- const plans = bins.map((bin) => planOf(bin, declared, false));
14961
- return windowed.length > 0 ? [planOf(windowed, declared, true), ...plans] : plans;
15227
+ return bins.map((bin, index) => planOf(bin, declared, index === 0 && windowed.length > 0));
15228
+ }
15229
+ function windowedSaid(plans) {
15230
+ if (!plans.some((plan) => plan.windowed)) {
15231
+ return "";
15232
+ }
15233
+ return plans.length === 1 ? ", with a window" : ", one of them windowed";
14962
15234
  }
14963
15235
  function inOneEngine(units, declared) {
14964
15236
  return planOf(units, declared, units.some((unit) => unit.windowed));
@@ -14978,10 +15250,10 @@ function planOf(bin, declared, windowed) {
14978
15250
  }
14979
15251
  function viewportOf(projectPath) {
14980
15252
  const file = `${projectPath}/project.godot`;
14981
- if (!existsSync17(file)) {
15253
+ if (!existsSync18(file)) {
14982
15254
  return null;
14983
15255
  }
14984
- const text = readFileSync16(file, "utf8");
15256
+ const text = readFileSync17(file, "utf8");
14985
15257
  const width = /^window\/size\/viewport_width\s*=\s*(\d+)/m.exec(text)?.[1];
14986
15258
  const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
14987
15259
  return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
@@ -36597,13 +36869,13 @@ var init_stdio = __esm(() => {
36597
36869
  });
36598
36870
 
36599
36871
  // src/autoload-failure.ts
36600
- import { existsSync as existsSync19, readFileSync as readFileSync19 } from "node:fs";
36601
- import { join as join22 } from "node:path";
36872
+ import { existsSync as existsSync20, readFileSync as readFileSync20 } from "node:fs";
36873
+ import { join as join23 } from "node:path";
36602
36874
  function scriptsAutoloaded(projectPath) {
36603
36875
  const scripts = new Map;
36604
36876
  let settings;
36605
36877
  try {
36606
- settings = parseProjectGodot(readFileSync19(join22(projectPath, "project.godot"), "utf8"));
36878
+ settings = parseProjectGodot(readFileSync20(join23(projectPath, "project.godot"), "utf8"));
36607
36879
  } catch {
36608
36880
  return scripts;
36609
36881
  }
@@ -36612,12 +36884,12 @@ function scriptsAutoloaded(projectPath) {
36612
36884
  scripts.set(`res://${path}`, name);
36613
36885
  continue;
36614
36886
  }
36615
- const scene = join22(projectPath, path);
36616
- if (!existsSync19(scene)) {
36887
+ const scene = join23(projectPath, path);
36888
+ if (!existsSync20(scene)) {
36617
36889
  continue;
36618
36890
  }
36619
36891
  try {
36620
- for (const named of readFileSync19(scene, "utf8").matchAll(/^\[ext_resource\b[^\]]*\btype="Script"[^\]]*\bpath="(res:\/\/[^"]+)"/gm)) {
36892
+ for (const named of readFileSync20(scene, "utf8").matchAll(/^\[ext_resource\b[^\]]*\btype="Script"[^\]]*\bpath="(res:\/\/[^"]+)"/gm)) {
36621
36893
  scripts.set(named[1] ?? "", name);
36622
36894
  }
36623
36895
  } catch {}
@@ -36636,10 +36908,10 @@ var init_autoload_failure = __esm(() => {
36636
36908
  });
36637
36909
 
36638
36910
  // src/runtime-client.ts
36639
- import { existsSync as existsSync20, readdirSync as readdirSync12, readFileSync as readFileSync20, statSync as statSync12, unlinkSync as unlinkSync2 } from "node:fs";
36911
+ import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync21, statSync as statSync13, unlinkSync as unlinkSync2 } from "node:fs";
36640
36912
  import { createConnection } from "node:net";
36641
36913
  import { tmpdir as tmpdir5 } from "node:os";
36642
- import { dirname as dirname12, join as join23, resolve as resolve5 } from "node:path";
36914
+ import { dirname as dirname12, join as join24, resolve as resolve5 } from "node:path";
36643
36915
  import { setTimeout as delay4 } from "node:timers/promises";
36644
36916
  function runtimeDirectory(variables = process.env) {
36645
36917
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
@@ -36647,20 +36919,20 @@ function runtimeDirectory(variables = process.env) {
36647
36919
  return explicit;
36648
36920
  }
36649
36921
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
36650
- return join23(perUser ?? tmpdir5(), "gdharness");
36922
+ return join24(perUser ?? tmpdir5(), "gdharness");
36651
36923
  }
36652
36924
  function runtimeDirectories(variables = process.env) {
36653
36925
  const candidates = [runtimeDirectory(variables)];
36654
36926
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
36655
36927
  for (const base of fallbacks) {
36656
- candidates.push(join23(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
36928
+ candidates.push(join24(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
36657
36929
  }
36658
36930
  return [...new Set(candidates.map((path) => resolve5(path)))];
36659
36931
  }
36660
36932
  function errorReportOf(pid, directories = runtimeDirectories()) {
36661
36933
  for (const directory of directories) {
36662
- const path = join23(directory, `runtime-${pid}.log`);
36663
- if (existsSync20(path)) {
36934
+ const path = join24(directory, `runtime-${pid}.log`);
36935
+ if (existsSync21(path)) {
36664
36936
  return path;
36665
36937
  }
36666
36938
  }
@@ -36691,7 +36963,7 @@ function reportAlreadyInTranscript(transcript, report) {
36691
36963
  function parseAnnouncement(file, pid) {
36692
36964
  let fields;
36693
36965
  try {
36694
- fields = asParams(JSON.parse(readFileSync20(file, "utf8")));
36966
+ fields = asParams(JSON.parse(readFileSync21(file, "utf8")));
36695
36967
  } catch {
36696
36968
  return { kind: "rubbish" };
36697
36969
  }
@@ -36708,7 +36980,7 @@ function parseAnnouncement(file, pid) {
36708
36980
  };
36709
36981
  let announcedAt;
36710
36982
  try {
36711
- announcedAt = statSync12(file).mtimeMs;
36983
+ announcedAt = statSync13(file).mtimeMs;
36712
36984
  } catch {}
36713
36985
  const when = announcedAt === undefined ? {} : { announcedAt };
36714
36986
  if (protocol !== RUNTIME_PROTOCOL) {
@@ -36761,7 +37033,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
36761
37033
  function wentAway(file, pid) {
36762
37034
  let project = "";
36763
37035
  try {
36764
- const fields = asParams(JSON.parse(readFileSync20(file, "utf8")));
37036
+ const fields = asParams(JSON.parse(readFileSync21(file, "utf8")));
36765
37037
  project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
36766
37038
  } catch {
36767
37039
  return;
@@ -36804,29 +37076,29 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
36804
37076
  }).map((one) => one.file));
36805
37077
  }
36806
37078
  function announcedIn(directory) {
36807
- if (!existsSync20(directory)) {
37079
+ if (!existsSync21(directory)) {
36808
37080
  return [];
36809
37081
  }
36810
37082
  const found = [];
36811
37083
  const candidates = [];
36812
37084
  const entries = [];
36813
- for (const entry of readdirSync12(directory)) {
37085
+ for (const entry of readdirSync13(directory)) {
36814
37086
  const report = ERROR_REPORT_PATTERN.exec(entry);
36815
37087
  if (report) {
36816
- sweepErrorReport(join23(directory, entry), Number.parseInt(report[1] ?? "", 10));
37088
+ sweepErrorReport(join24(directory, entry), Number.parseInt(report[1] ?? "", 10));
36817
37089
  continue;
36818
37090
  }
36819
37091
  const match = ANNOUNCEMENT_PATTERN.exec(entry);
36820
37092
  if (!match) {
36821
37093
  continue;
36822
37094
  }
36823
- const file = join23(directory, entry);
37095
+ const file = join24(directory, entry);
36824
37096
  const pid = Number.parseInt(match[1] ?? "", 10);
36825
37097
  const running = alive(pid);
36826
37098
  entries.push({ file, pid, running });
36827
37099
  if (running) {
36828
37100
  try {
36829
- candidates.push({ file, pid, writtenAt: statSync12(file).mtimeMs });
37101
+ candidates.push({ file, pid, writtenAt: statSync13(file).mtimeMs });
36830
37102
  } catch {}
36831
37103
  }
36832
37104
  }
@@ -36848,7 +37120,7 @@ function announcedIn(directory) {
36848
37120
  found.push(announced);
36849
37121
  }
36850
37122
  const held = new Set(entries.map((one) => one.file));
36851
- const here = join23(directory, ".");
37123
+ const here = join24(directory, ".");
36852
37124
  for (const file of verdicts.keys()) {
36853
37125
  if (dirname12(file) === here && !held.has(file)) {
36854
37126
  verdicts.delete(file);
@@ -36857,7 +37129,7 @@ function announcedIn(directory) {
36857
37129
  return found;
36858
37130
  }
36859
37131
  async function announcementEnded(pid, directories = runtimeDirectories()) {
36860
- const files = directories.map((directory) => join23(directory, `runtime-${pid}.json`)).filter((file) => existsSync20(file));
37132
+ const files = directories.map((directory) => join24(directory, `runtime-${pid}.json`)).filter((file) => existsSync21(file));
36861
37133
  const first = files[0];
36862
37134
  if (first === undefined) {
36863
37135
  return;
@@ -36883,7 +37155,7 @@ function sweepErrorReport(path, pid, now = Date.now()) {
36883
37155
  return;
36884
37156
  }
36885
37157
  try {
36886
- if (now - statSync12(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
37158
+ if (now - statSync13(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
36887
37159
  unlinkSync2(path);
36888
37160
  }
36889
37161
  } catch {}
@@ -37276,14 +37548,14 @@ var init_runtime_client = __esm(() => {
37276
37548
  });
37277
37549
 
37278
37550
  // src/boot-note.ts
37279
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync21, writeFileSync as writeFileSync11 } from "node:fs";
37280
- import { dirname as dirname13, join as join24 } from "node:path";
37551
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
37552
+ import { dirname as dirname13, join as join25 } from "node:path";
37281
37553
  function bootNotePath(projectPath) {
37282
- return join24(projectPath, ".godot", "gdharness-boot.json");
37554
+ return join25(projectPath, ".godot", "gdharness-boot.json");
37283
37555
  }
37284
37556
  function readScenes(projectPath) {
37285
37557
  try {
37286
- return asParams(asParams(JSON.parse(readFileSync21(bootNotePath(projectPath), "utf8")))["scenes"]);
37558
+ return asParams(asParams(JSON.parse(readFileSync22(bootNotePath(projectPath), "utf8")))["scenes"]);
37287
37559
  } catch {
37288
37560
  return {};
37289
37561
  }
@@ -37325,10 +37597,10 @@ var init_boot_note = __esm(() => {
37325
37597
  });
37326
37598
 
37327
37599
  // src/breakpoint-note.ts
37328
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync22, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "node:fs";
37329
- import { dirname as dirname14, isAbsolute as isAbsolute5, join as join25, relative as relative4 } from "node:path";
37600
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync23, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "node:fs";
37601
+ import { dirname as dirname14, isAbsolute as isAbsolute5, join as join26, relative as relative4 } from "node:path";
37330
37602
  function breakpointNotePath(projectPath) {
37331
- return join25(projectPath, ".godot", "gdharness-breakpoints.json");
37603
+ return join26(projectPath, ".godot", "gdharness-breakpoints.json");
37332
37604
  }
37333
37605
  function writeBreakpointNote(projectPath, held) {
37334
37606
  const path = breakpointNotePath(projectPath);
@@ -37354,7 +37626,7 @@ function writeBreakpointNote(projectPath, held) {
37354
37626
  }
37355
37627
  function readBreakpointNote(projectPath) {
37356
37628
  try {
37357
- const parsed = JSON.parse(readFileSync22(breakpointNotePath(projectPath), "utf8"));
37629
+ const parsed = JSON.parse(readFileSync23(breakpointNotePath(projectPath), "utf8"));
37358
37630
  if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
37359
37631
  return [];
37360
37632
  }
@@ -37369,7 +37641,7 @@ function readBreakpointNote(projectPath) {
37369
37641
  }
37370
37642
  const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
37371
37643
  if (lines.length > 0) {
37372
- held.push({ scriptPath: realAsFarAsItExists(join25(projectPath, script)), lines });
37644
+ held.push({ scriptPath: realAsFarAsItExists(join26(projectPath, script)), lines });
37373
37645
  }
37374
37646
  }
37375
37647
  return held;
@@ -37387,14 +37659,14 @@ var init_breakpoint_note = __esm(() => {
37387
37659
  });
37388
37660
 
37389
37661
  // src/class-note.ts
37390
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync23 } from "node:fs";
37391
- import { dirname as dirname15, join as join26 } from "node:path";
37662
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync24 } from "node:fs";
37663
+ import { dirname as dirname15, join as join27 } from "node:path";
37392
37664
  function classNotePath(projectPath) {
37393
- return join26(projectPath, ".godot", "gdharness-classes.json");
37665
+ return join27(projectPath, ".godot", "gdharness-classes.json");
37394
37666
  }
37395
37667
  function readClassNote(projectPath) {
37396
37668
  try {
37397
- const fields = asParams(JSON.parse(readFileSync23(classNotePath(projectPath), "utf8")));
37669
+ const fields = asParams(JSON.parse(readFileSync24(classNotePath(projectPath), "utf8")));
37398
37670
  const rebuiltWith = readArray(fields, "rebuiltWith");
37399
37671
  if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
37400
37672
  return null;
@@ -37436,16 +37708,16 @@ var init_class_note = __esm(() => {
37436
37708
  });
37437
37709
 
37438
37710
  // src/config-pin.ts
37439
- import { existsSync as existsSync21, readFileSync as readFileSync24 } from "node:fs";
37440
- import { join as join27 } from "node:path";
37711
+ import { existsSync as existsSync22, readFileSync as readFileSync25 } from "node:fs";
37712
+ import { join as join28 } from "node:path";
37441
37713
  function versionsNamedInConfig(projectPath) {
37442
- const config = join27(projectPath, ".mcp.json");
37443
- if (!existsSync21(config)) {
37714
+ const config = join28(projectPath, ".mcp.json");
37715
+ if (!existsSync22(config)) {
37444
37716
  return [];
37445
37717
  }
37446
37718
  let text;
37447
37719
  try {
37448
- text = readFileSync24(config, "utf8");
37720
+ text = readFileSync25(config, "utf8");
37449
37721
  JSON.parse(text);
37450
37722
  } catch {
37451
37723
  return [];
@@ -37466,10 +37738,10 @@ var init_config_pin = __esm(() => {
37466
37738
  });
37467
37739
 
37468
37740
  // src/cut-test-run.ts
37469
- import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync25, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "node:fs";
37470
- import { dirname as dirname16, join as join28 } from "node:path";
37741
+ import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync26, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "node:fs";
37742
+ import { dirname as dirname16, join as join29 } from "node:path";
37471
37743
  function keptAt(projectPath) {
37472
- return join28(projectPath, ".godot", "gdharness-reports", "cut-run.json");
37744
+ return join29(projectPath, ".godot", "gdharness-reports", "cut-run.json");
37473
37745
  }
37474
37746
  function keepCutTestRun(projectPath, run) {
37475
37747
  const path = keptAt(projectPath);
@@ -37481,11 +37753,11 @@ function keepCutTestRun(projectPath, run) {
37481
37753
  }
37482
37754
  function takeCutTestRun(projectPath) {
37483
37755
  const path = keptAt(projectPath);
37484
- if (!existsSync22(path)) {
37756
+ if (!existsSync23(path)) {
37485
37757
  return null;
37486
37758
  }
37487
37759
  try {
37488
- const run = JSON.parse(readFileSync25(path, "utf8"));
37760
+ const run = JSON.parse(readFileSync26(path, "utf8"));
37489
37761
  rmSync12(path, { force: true });
37490
37762
  return typeof run.cutAt === "string" && Array.isArray(run.paths) && run.paths.every((path) => typeof path === "string") ? run : null;
37491
37763
  } catch {
@@ -38298,25 +38570,25 @@ var init_dap_client = __esm(() => {
38298
38570
  // src/editor-log.ts
38299
38571
  import {
38300
38572
  closeSync as closeSync4,
38301
- existsSync as existsSync23,
38573
+ existsSync as existsSync24,
38302
38574
  mkdirSync as mkdirSync13,
38303
38575
  openSync as openSync4,
38304
- readFileSync as readFileSync26,
38576
+ readFileSync as readFileSync27,
38305
38577
  readSync as readSync2,
38306
38578
  rmSync as rmSync13,
38307
- statSync as statSync13,
38579
+ statSync as statSync14,
38308
38580
  writeFileSync as writeFileSync14
38309
38581
  } from "node:fs";
38310
- import { dirname as dirname17, join as join29 } from "node:path";
38582
+ import { dirname as dirname17, join as join30 } from "node:path";
38311
38583
  function editorLogPath(projectPath) {
38312
- return join29(projectPath, ".godot", "gdharness-editor.log");
38584
+ return join30(projectPath, ".godot", "gdharness-editor.log");
38313
38585
  }
38314
38586
  function editorLogNotePath(projectPath) {
38315
- return join29(projectPath, ".godot", "gdharness-editor.json");
38587
+ return join30(projectPath, ".godot", "gdharness-editor.json");
38316
38588
  }
38317
38589
  function readEditorLogNote(projectPath) {
38318
38590
  try {
38319
- const fields = asParams(JSON.parse(readFileSync26(editorLogNotePath(projectPath), "utf8")));
38591
+ const fields = asParams(JSON.parse(readFileSync27(editorLogNotePath(projectPath), "utf8")));
38320
38592
  const pid = readNumber(fields, "pid");
38321
38593
  const startedAt = readString(fields, "startedAt");
38322
38594
  return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
@@ -38357,7 +38629,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
38357
38629
  return { kind: "another editor", notedPid: noted?.pid ?? null };
38358
38630
  }
38359
38631
  const path = editorLogPath(projectPath);
38360
- if (!existsSync23(path)) {
38632
+ if (!existsSync24(path)) {
38361
38633
  return { kind: "not written" };
38362
38634
  }
38363
38635
  const log = new GameLog;
@@ -38413,9 +38685,9 @@ function foldProgress(text) {
38413
38685
  `);
38414
38686
  }
38415
38687
  function theFirstOf(path) {
38416
- const whole = statSync13(path).size;
38688
+ const whole = statSync14(path).size;
38417
38689
  if (whole <= MOST_CONSOLE_BYTES) {
38418
- return { text: readFileSync26(path, "utf8") };
38690
+ return { text: readFileSync27(path, "utf8") };
38419
38691
  }
38420
38692
  const handle = openSync4(path, "r");
38421
38693
  try {
@@ -38576,7 +38848,7 @@ var init_editor_log = __esm(() => {
38576
38848
  });
38577
38849
 
38578
38850
  // src/rename.ts
38579
- import { existsSync as existsSync24, mkdirSync as mkdirSync14, readFileSync as readFileSync27, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "node:fs";
38851
+ import { existsSync as existsSync25, mkdirSync as mkdirSync14, readFileSync as readFileSync28, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "node:fs";
38580
38852
  import { dirname as dirname18, posix as posix4 } from "node:path";
38581
38853
 
38582
38854
  class Script {
@@ -39281,11 +39553,11 @@ function rewritesOf(plan, texts, move) {
39281
39553
  }
39282
39554
  function applyRewrites(rewrites) {
39283
39555
  for (const rewrite of rewrites) {
39284
- const now = readFileSync27(rewrite.absolute, "utf8");
39556
+ const now = readFileSync28(rewrite.absolute, "utf8");
39285
39557
  if (now !== rewrite.before) {
39286
39558
  throw new Error(`${rewrite.path} changed on disk while the rename was being worked out; nothing was written`);
39287
39559
  }
39288
- if (rewrite.writeTo !== rewrite.absolute && existsSync24(rewrite.writeTo)) {
39560
+ if (rewrite.writeTo !== rewrite.absolute && existsSync25(rewrite.writeTo)) {
39289
39561
  throw new Error(`${rewrite.writeTo} already exists; nothing was written`);
39290
39562
  }
39291
39563
  }
@@ -39299,7 +39571,7 @@ function applyRewrites(rewrites) {
39299
39571
  let movedUid = false;
39300
39572
  if (rewrite.writeTo !== rewrite.absolute) {
39301
39573
  unlinkSync3(rewrite.absolute);
39302
- if (existsSync24(`${rewrite.absolute}.uid`)) {
39574
+ if (existsSync25(`${rewrite.absolute}.uid`)) {
39303
39575
  renameSync3(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
39304
39576
  movedUid = true;
39305
39577
  }
@@ -39345,10 +39617,10 @@ var init_rename = __esm(() => {
39345
39617
  });
39346
39618
 
39347
39619
  // src/lsp_client.ts
39348
- import { existsSync as existsSync25, realpathSync as realpathSync3 } from "node:fs";
39620
+ import { existsSync as existsSync26, realpathSync as realpathSync3 } from "node:fs";
39349
39621
  import { readFile, realpath } from "node:fs/promises";
39350
39622
  import { createConnection as createConnection3 } from "node:net";
39351
- import { dirname as dirname19, join as join30, resolve as resolve6 } from "node:path";
39623
+ import { dirname as dirname19, join as join31, resolve as resolve6 } from "node:path";
39352
39624
  import { setTimeout as delay6 } from "node:timers/promises";
39353
39625
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
39354
39626
  function initializeTimeoutMs() {
@@ -40026,7 +40298,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
40026
40298
  } catch {
40027
40299
  throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
40028
40300
  }
40029
- if (!existsSync25(join30(projectPath, "project.godot"))) {
40301
+ if (!existsSync26(join31(projectPath, "project.godot"))) {
40030
40302
  throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
40031
40303
  }
40032
40304
  const contained = resolveWithinProject(projectPath, scriptPathValue);
@@ -40186,8 +40458,8 @@ var init_lsp_client = __esm(() => {
40186
40458
  });
40187
40459
 
40188
40460
  // src/rename-symbol.ts
40189
- import { existsSync as existsSync26, mkdirSync as mkdirSync15, readdirSync as readdirSync13, realpathSync as realpathSync4, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "node:fs";
40190
- import { basename as basename8, dirname as dirname20, join as join31, relative as relative5, resolve as resolve7 } from "node:path";
40461
+ import { existsSync as existsSync27, mkdirSync as mkdirSync15, readdirSync as readdirSync14, realpathSync as realpathSync4, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "node:fs";
40462
+ import { basename as basename8, dirname as dirname20, join as join32, relative as relative5, resolve as resolve7 } from "node:path";
40191
40463
  function resourcePathOf(projectPath, absolute) {
40192
40464
  const inside = relative5(onDiskSpelling(projectPath), onDiskSpelling(absolute));
40193
40465
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
@@ -40200,7 +40472,7 @@ function onDiskSpelling(path) {
40200
40472
  let at = resolve7(path);
40201
40473
  for (;; ) {
40202
40474
  try {
40203
- return join31(realpathSync4.native(at), ...missing.reverse());
40475
+ return join32(realpathSync4.native(at), ...missing.reverse());
40204
40476
  } catch {
40205
40477
  const parent = dirname20(at);
40206
40478
  if (parent === at) {
@@ -40254,7 +40526,7 @@ async function renameSymbol(request, services) {
40254
40526
  reason: `newScriptPath must be a .gd path inside the project, and ${request.newScriptPath} is not.`
40255
40527
  };
40256
40528
  }
40257
- if (existsSync26(request.newScriptPath)) {
40529
+ if (existsSync27(request.newScriptPath)) {
40258
40530
  return { ok: false, reason: `${moveTo} already exists, so the script cannot be moved there.` };
40259
40531
  }
40260
40532
  }
@@ -40430,7 +40702,7 @@ async function renameSymbol(request, services) {
40430
40702
  scripts,
40431
40703
  moves: new Map([[declaringPath, moveTo]]),
40432
40704
  named: [declaringPath],
40433
- exists: (path) => existsSync26(`${projectPath}/${path.slice("res://".length)}`)
40705
+ exists: (path) => existsSync27(`${projectPath}/${path.slice("res://".length)}`)
40434
40706
  }, plan);
40435
40707
  }
40436
40708
  plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? [] : [declaringPath]);
@@ -40629,15 +40901,15 @@ ${mention.kind}`;
40629
40901
  };
40630
40902
  }
40631
40903
  function writeLeftAloneDetail(projectPath, symbol, detail) {
40632
- const directory = join31(projectPath, ".godot", "gdharness-renames");
40904
+ const directory = join32(projectPath, ".godot", "gdharness-renames");
40633
40905
  mkdirSync15(directory, { recursive: true });
40634
40906
  detailWrites += 1;
40635
- const path = join31(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
40907
+ const path = join32(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
40636
40908
  writeFileSync16(path, `${JSON.stringify(detail, null, 2)}
40637
40909
  `, "utf8");
40638
- const kept = readdirSync13(directory).filter((name) => name.endsWith(".json")).sort().reverse();
40910
+ const kept = readdirSync14(directory).filter((name) => name.endsWith(".json")).sort().reverse();
40639
40911
  for (const old of kept.slice(10)) {
40640
- rmSync14(join31(directory, old), { force: true });
40912
+ rmSync14(join32(directory, old), { force: true });
40641
40913
  }
40642
40914
  return path;
40643
40915
  }
@@ -40678,25 +40950,25 @@ var init_rename_symbol = __esm(() => {
40678
40950
 
40679
40951
  // src/file-move.ts
40680
40952
  import {
40681
- existsSync as existsSync27,
40953
+ existsSync as existsSync28,
40682
40954
  mkdirSync as mkdirSync16,
40683
- readdirSync as readdirSync14,
40684
- readFileSync as readFileSync28,
40955
+ readdirSync as readdirSync15,
40956
+ readFileSync as readFileSync29,
40685
40957
  renameSync as renameSync4,
40686
- statSync as statSync14,
40958
+ statSync as statSync15,
40687
40959
  writeFileSync as writeFileSync17
40688
40960
  } from "node:fs";
40689
- import { basename as basename9, dirname as dirname21, extname as extname3, join as join32, relative as relative6 } from "node:path";
40961
+ import { basename as basename9, dirname as dirname21, extname as extname3, join as join33, relative as relative6 } from "node:path";
40690
40962
  function filesUnder(directory) {
40691
- return readdirSync14(directory, { withFileTypes: true }).flatMap((entry) => {
40692
- const path = join32(directory, entry.name);
40963
+ return readdirSync15(directory, { withFileTypes: true }).flatMap((entry) => {
40964
+ const path = join33(directory, entry.name);
40693
40965
  return entry.isDirectory() ? filesUnder(path) : [path];
40694
40966
  });
40695
40967
  }
40696
40968
  function sameFile(one, other) {
40697
40969
  try {
40698
- const a = statSync14(one);
40699
- const b = statSync14(other);
40970
+ const a = statSync15(one);
40971
+ const b = statSync15(other);
40700
40972
  return a.ino === b.ino && a.dev === b.dev;
40701
40973
  } catch {
40702
40974
  return false;
@@ -40710,8 +40982,8 @@ function unseenByTheEngine(projectPath, path) {
40710
40982
  return `${path} is under ${step}, and the engine passes over every directory whose name starts with a dot`;
40711
40983
  }
40712
40984
  if (at < steps.length - 1) {
40713
- directory = join32(directory, step);
40714
- if (existsSync27(join32(directory, ".gdignore"))) {
40985
+ directory = join33(directory, step);
40986
+ if (existsSync28(join33(directory, ".gdignore"))) {
40715
40987
  return `${path} is under a directory holding a .gdignore, which the engine does not load`;
40716
40988
  }
40717
40989
  }
@@ -40723,21 +40995,21 @@ function installedAddonOf(projectPath, path) {
40723
40995
  if (steps[0] !== "addons" || steps[1] === undefined) {
40724
40996
  return null;
40725
40997
  }
40726
- return existsSync27(join32(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
40998
+ return existsSync28(join33(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
40727
40999
  }
40728
41000
  function binaryResources(projectPath) {
40729
41001
  const found = [];
40730
41002
  const visit = (directory, prefix) => {
40731
- if (existsSync27(join32(directory, ".gdignore"))) {
41003
+ if (existsSync28(join33(directory, ".gdignore"))) {
40732
41004
  return;
40733
41005
  }
40734
- for (const entry of readdirSync14(directory, { withFileTypes: true })) {
41006
+ for (const entry of readdirSync15(directory, { withFileTypes: true })) {
40735
41007
  if (entry.name.startsWith(".")) {
40736
41008
  continue;
40737
41009
  }
40738
41010
  const path = `${prefix}${entry.name}`;
40739
41011
  if (entry.isDirectory()) {
40740
- visit(join32(directory, entry.name), `${path}/`);
41012
+ visit(join33(directory, entry.name), `${path}/`);
40741
41013
  } else if (/\.(scn|res)$/i.test(entry.name)) {
40742
41014
  found.push(path);
40743
41015
  }
@@ -40747,7 +41019,7 @@ function binaryResources(projectPath) {
40747
41019
  return found;
40748
41020
  }
40749
41021
  function targetPathOf(projectPath, absolute) {
40750
- const inside = relative6(onDiskSpelling(projectPath), join32(onDiskSpelling(dirname21(absolute)), basename9(absolute)));
41022
+ const inside = relative6(onDiskSpelling(projectPath), join33(onDiskSpelling(dirname21(absolute)), basename9(absolute)));
40751
41023
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
40752
41024
  return null;
40753
41025
  }
@@ -40765,13 +41037,13 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40765
41037
  if (to === null) {
40766
41038
  return { ok: false, reason: `newPath must be a path inside the project, and ${absoluteTo} is not.` };
40767
41039
  }
40768
- if (!existsSync27(absoluteFrom)) {
41040
+ if (!existsSync28(absoluteFrom)) {
40769
41041
  return { ok: false, reason: `There is nothing at ${from} to move.` };
40770
41042
  }
40771
41043
  if (from === to) {
40772
41044
  return { ok: false, reason: `${from} is already there; nothing to move.` };
40773
41045
  }
40774
- const isDirectory = statSync14(absoluteFrom).isDirectory();
41046
+ const isDirectory = statSync15(absoluteFrom).isDirectory();
40775
41047
  if (from === "res://project.godot") {
40776
41048
  return {
40777
41049
  ok: false,
@@ -40794,7 +41066,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40794
41066
  if (isDirectory && to.startsWith(`${from}/`)) {
40795
41067
  return { ok: false, reason: `${from} cannot be moved into itself.` };
40796
41068
  }
40797
- if (existsSync27(absoluteTo) && !sameFile(absoluteFrom, absoluteTo)) {
41069
+ if (existsSync28(absoluteTo) && !sameFile(absoluteFrom, absoluteTo)) {
40798
41070
  return { ok: false, reason: `${to} already exists, so nothing was moved there.` };
40799
41071
  }
40800
41072
  const unseen = unseenByTheEngine(projectPath, to);
@@ -40802,7 +41074,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40802
41074
  return { ok: false, reason: `${unseen}, so nothing there could load what was moved.` };
40803
41075
  }
40804
41076
  if (!isDirectory) {
40805
- const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync27(join32(projectPath, path.slice("res://".length))));
41077
+ const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync28(join33(projectPath, path.slice("res://".length))));
40806
41078
  if (owner !== undefined && owner !== null) {
40807
41079
  return { ok: false, reason: `${from} belongs to ${owner} and moves with it: move ${owner} instead.` };
40808
41080
  }
@@ -40824,7 +41096,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40824
41096
  } else {
40825
41097
  moves.set(from, to);
40826
41098
  for (const sidecar of SIDECARS) {
40827
- if (existsSync27(`${absoluteFrom}${sidecar}`)) {
41099
+ if (existsSync28(`${absoluteFrom}${sidecar}`)) {
40828
41100
  moves.set(`${from}${sidecar}`, `${to}${sidecar}`);
40829
41101
  }
40830
41102
  }
@@ -40836,12 +41108,12 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40836
41108
  scripts,
40837
41109
  moves,
40838
41110
  named: [from],
40839
- exists: (path) => existsSync27(join32(projectPath, path.slice("res://".length)))
41111
+ exists: (path) => existsSync28(join33(projectPath, path.slice("res://".length)))
40840
41112
  }, { edits: [], mentions: [] });
40841
41113
  plan = mentionIgnored(plan, ignoredTexts(projectPath), null, [from]);
40842
41114
  const binaries = [];
40843
41115
  for (const path of binaryResources(projectPath)) {
40844
- const bytes = readFileSync28(join32(projectPath, path.slice("res://".length)));
41116
+ const bytes = readFileSync29(join33(projectPath, path.slice("res://".length)));
40845
41117
  const names = [...moves.keys()].find((moving) => bytes.includes(moving));
40846
41118
  if (names !== undefined) {
40847
41119
  binaries.push({
@@ -40860,7 +41132,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40860
41132
  }));
40861
41133
  const left = leftAloneReport(mentions);
40862
41134
  const rewrites = rewritesOf(plan, texts, null);
40863
- const absoluteOf = (path) => join32(projectPath, path.slice("res://".length));
41135
+ const absoluteOf = (path) => join33(projectPath, path.slice("res://".length));
40864
41136
  const rewritten = new Map(rewrites.map((rewrite) => [rewrite.path, rewrite]));
40865
41137
  const moved = [...moves].map(([source, target]) => {
40866
41138
  const rewrite = rewritten.get(source);
@@ -40902,18 +41174,18 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40902
41174
  }
40903
41175
  function applyFileMove(outcome) {
40904
41176
  for (const rewrite of outcome.inPlace) {
40905
- if (readFileSync28(rewrite.absolute, "utf8") !== rewrite.before) {
41177
+ if (readFileSync29(rewrite.absolute, "utf8") !== rewrite.before) {
40906
41178
  throw new Error(`${rewrite.path} changed on disk while the move was being worked out; nothing was written`);
40907
41179
  }
40908
41180
  }
40909
41181
  for (const file of outcome.moved) {
40910
- if (!existsSync27(file.absoluteFrom)) {
41182
+ if (!existsSync28(file.absoluteFrom)) {
40911
41183
  throw new Error(`${file.from} is gone from disk; nothing was written`);
40912
41184
  }
40913
- if (file.text !== null && readFileSync28(file.absoluteFrom, "utf8") !== file.text.before) {
41185
+ if (file.text !== null && readFileSync29(file.absoluteFrom, "utf8") !== file.text.before) {
40914
41186
  throw new Error(`${file.from} changed on disk while the move was being worked out; nothing was written`);
40915
41187
  }
40916
- if (existsSync27(file.absoluteTo) && !sameFile(file.absoluteFrom, file.absoluteTo)) {
41188
+ if (existsSync28(file.absoluteTo) && !sameFile(file.absoluteFrom, file.absoluteTo)) {
40917
41189
  throw new Error(`${file.to} already exists; nothing was written`);
40918
41190
  }
40919
41191
  }
@@ -44580,168 +44852,6 @@ var init_godot_bridge = __esm(() => {
44580
44852
  };
44581
44853
  });
44582
44854
 
44583
- // src/import-freshness.ts
44584
- import { createHash as createHash6 } from "node:crypto";
44585
- import { existsSync as existsSync28, readdirSync as readdirSync15, readFileSync as readFileSync29, statSync as statSync15 } from "node:fs";
44586
- import { join as join33 } from "node:path";
44587
- function md5Of(file) {
44588
- let stats;
44589
- try {
44590
- stats = statSync15(file);
44591
- } catch {
44592
- return null;
44593
- }
44594
- const known = hashes.get(file);
44595
- if (known !== undefined && known.size === stats.size && known.mtimeMs === stats.mtimeMs) {
44596
- return known.md5;
44597
- }
44598
- let md5;
44599
- try {
44600
- md5 = createHash6("md5").update(readFileSync29(file)).digest("hex");
44601
- } catch {
44602
- return null;
44603
- }
44604
- hashes.set(file, { size: stats.size, mtimeMs: stats.mtimeMs, md5 });
44605
- return md5;
44606
- }
44607
- function sectionOf(text, section) {
44608
- const keys = new Map;
44609
- let inside = false;
44610
- for (const line of text.split(/\r?\n/)) {
44611
- const header = /^\[([^\]]+)\]\s*$/.exec(line);
44612
- if (header !== null) {
44613
- inside = header[1] === section;
44614
- continue;
44615
- }
44616
- const pair = inside ? /^([A-Za-z0-9_./]+)=(.*)$/.exec(line) : null;
44617
- if (pair?.[1] !== undefined && pair[2] !== undefined) {
44618
- keys.set(pair[1], pair[2]);
44619
- }
44620
- }
44621
- return keys;
44622
- }
44623
- function unquoted(value) {
44624
- return value === undefined ? "" : value.replace(/^"(.*)"$/, "$1");
44625
- }
44626
- function listed2(value) {
44627
- return [...(value ?? "").matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1] ?? "");
44628
- }
44629
- function onDisk(projectPath, resPath) {
44630
- return join33(projectPath, resPath.slice("res://".length));
44631
- }
44632
- function behind(projectPath, resPath, sidecar) {
44633
- const remap = sectionOf(sidecar, "remap");
44634
- const importer = unquoted(remap.get("importer"));
44635
- if (importer === "keep" || importer === "skip") {
44636
- return null;
44637
- }
44638
- const outputs = listed2(sectionOf(sidecar, "deps").get("dest_files"));
44639
- if (outputs.some((output) => !existsSync28(onDisk(projectPath, output)))) {
44640
- return "its imported output is not on disk";
44641
- }
44642
- const name = resPath.slice(resPath.lastIndexOf("/") + 1);
44643
- const record = join33(projectPath, ".godot", "imported", `${name}-${createHash6("md5").update(resPath, "utf8").digest("hex")}.md5`);
44644
- let recorded = "";
44645
- try {
44646
- recorded = /^source_md5="([0-9a-f]*)"/m.exec(readFileSync29(record, "utf8"))?.[1] ?? "";
44647
- } catch {
44648
- recorded = "";
44649
- }
44650
- if (recorded === "") {
44651
- return "its import recorded no hash of its source";
44652
- }
44653
- const current = md5Of(onDisk(projectPath, resPath));
44654
- return current === null || current === recorded ? null : "the source changed after it was imported";
44655
- }
44656
- function staleImports(projectPath) {
44657
- const stale = [];
44658
- const visit = (directory, resDirectory) => {
44659
- if (existsSync28(join33(directory, ".gdignore")) || existsSync28(join33(directory, VERSION_MARKER))) {
44660
- return;
44661
- }
44662
- let entries;
44663
- try {
44664
- entries = readdirSync15(directory, { withFileTypes: true });
44665
- } catch {
44666
- return;
44667
- }
44668
- const names = new Set(entries.map((entry) => entry.name));
44669
- for (const entry of entries) {
44670
- if (entry.name.startsWith(".")) {
44671
- continue;
44672
- }
44673
- const resPath = `${resDirectory}${entry.name}`;
44674
- if (entry.isDirectory()) {
44675
- visit(join33(directory, entry.name), `${resPath}/`);
44676
- continue;
44677
- }
44678
- if (!entry.isFile()) {
44679
- continue;
44680
- }
44681
- if (entry.name.endsWith(".import")) {
44682
- const source = resPath.slice(0, -".import".length);
44683
- if (!names.has(entry.name.slice(0, -".import".length))) {
44684
- continue;
44685
- }
44686
- let sidecar;
44687
- try {
44688
- sidecar = readFileSync29(join33(directory, entry.name), "utf8");
44689
- } catch {
44690
- continue;
44691
- }
44692
- const reason = behind(projectPath, source, sidecar);
44693
- if (reason !== null) {
44694
- stale.push({ path: source, reason });
44695
- }
44696
- continue;
44697
- }
44698
- const extension = entry.name.slice(entry.name.lastIndexOf(".") + 1).toLowerCase();
44699
- if (IMPORTABLE.has(extension) && !names.has(`${entry.name}.import`)) {
44700
- stale.push({ path: resPath, reason: "it has not been imported" });
44701
- }
44702
- }
44703
- };
44704
- visit(projectPath, "res://");
44705
- return stale.sort((one, other) => one.path.localeCompare(other.path));
44706
- }
44707
- var IMPORTABLE_EXTENSIONS, IMPORTABLE, hashes;
44708
- var init_import_freshness = __esm(() => {
44709
- init_project_scan();
44710
- IMPORTABLE_EXTENSIONS = [
44711
- "png",
44712
- "jpg",
44713
- "jpeg",
44714
- "webp",
44715
- "svg",
44716
- "bmp",
44717
- "tga",
44718
- "exr",
44719
- "hdr",
44720
- "dds",
44721
- "ktx",
44722
- "wav",
44723
- "mp3",
44724
- "ogg",
44725
- "ttf",
44726
- "otf",
44727
- "ttc",
44728
- "otc",
44729
- "woff",
44730
- "woff2",
44731
- "pfb",
44732
- "pfm",
44733
- "fnt",
44734
- "font",
44735
- "glb",
44736
- "gltf",
44737
- "fbx",
44738
- "obj",
44739
- "dae"
44740
- ];
44741
- IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
44742
- hashes = new Map;
44743
- });
44744
-
44745
44855
  // src/keeper-channel.ts
44746
44856
  import { connect as connect2, createServer as createServer2 } from "node:net";
44747
44857
  async function askTheKeeperToStop(address, withinMs) {
@@ -46676,6 +46786,7 @@ function settingsNamedBy(op, args) {
46676
46786
  const text = (key) => readString(args, key) ?? "";
46677
46787
  switch (op) {
46678
46788
  case "set":
46789
+ case "unset":
46679
46790
  return text("setting") === "" ? [] : [text("setting")];
46680
46791
  case "add_autoload":
46681
46792
  case "remove_autoload":
@@ -47178,7 +47289,7 @@ function scriptsToDiagnose(projectPath, given) {
47178
47289
  found.push(relative);
47179
47290
  continue;
47180
47291
  }
47181
- const inside = scriptsUnder(location.absolutePath, relative);
47292
+ const inside = folderFiles(location.absolutePath, [".gd"]).map((script) => relative === "" ? script : `${relative}/${script}`);
47182
47293
  if (inside.length === 0) {
47183
47294
  empty.push(entry);
47184
47295
  }
@@ -47194,7 +47305,7 @@ function engineChecked(why) {
47194
47305
  return {
47195
47306
  checkedBy: "engine",
47196
47307
  languageServer: why,
47197
- note: "No editor of this project is open to ask, so a headless engine compiled the scripts the way loading them does. It reports every error that stops a script compiling, at its line and without a column, and no warnings, which the engine tells only an editor: warnings is null rather than 0. editor_launch with hidden: true opens this project's editor without a window, and its language server reports both."
47308
+ note: "No editor of this project is open to ask, so a headless engine compiled the scripts the way loading them does. It reports every error that stops a script compiling, at its line and without a column, and no warnings, which the engine tells only an editor: warnings is null rather than 0. editor_launch with hidden: true opens this project's editor without a window, and its language server reports both. Until then clean says only that every script compiles."
47198
47309
  };
47199
47310
  }
47200
47311
  function isDirectory(path) {
@@ -47204,24 +47315,6 @@ function isDirectory(path) {
47204
47315
  return false;
47205
47316
  }
47206
47317
  }
47207
- function scriptsUnder(directory, prefix) {
47208
- if (existsSync30(join38(directory, ".gdignore"))) {
47209
- return [];
47210
- }
47211
- const found = [];
47212
- for (const entry of readdirSync17(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
47213
- if (entry.name.startsWith(".")) {
47214
- continue;
47215
- }
47216
- const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
47217
- if (entry.isDirectory()) {
47218
- found.push(...scriptsUnder(join38(directory, entry.name), path));
47219
- } else if (entry.isFile() && entry.name.endsWith(".gd")) {
47220
- found.push(path);
47221
- }
47222
- }
47223
- return found;
47224
- }
47225
47318
  function mainSceneOf(projectFile) {
47226
47319
  const scene = parseProjectGodot(readFileSync34(projectFile, "utf8"))["application"]?.["run/main_scene"];
47227
47320
  return typeof scene === "string" && scene !== "" ? scene : null;
@@ -48058,6 +48151,8 @@ class GodotServer {
48058
48151
  return await editorOrEngine("create_animation", "writes");
48059
48152
  case "add_track":
48060
48153
  return await editorOrEngine("add_animation_track", "writes");
48154
+ case "remove_track":
48155
+ return await editorOrEngine("remove_animation_track", "writes");
48061
48156
  case "set":
48062
48157
  return await editorOrEngine("set_animation", "writes");
48063
48158
  case "rename":
@@ -48066,6 +48161,10 @@ class GodotServer {
48066
48161
  return await editorOrEngine("remove_animation", "writes");
48067
48162
  case "add_state":
48068
48163
  return await editorOrEngine("add_animation_state", "writes");
48164
+ case "remove_state":
48165
+ return await editorOrEngine("remove_animation_state", "writes");
48166
+ case "disconnect_states":
48167
+ return await editorOrEngine("disconnect_animation_states", "writes");
48069
48168
  default:
48070
48169
  return await editorOrEngine("connect_animation_states", "writes");
48071
48170
  }
@@ -49410,7 +49509,7 @@ class GodotServer {
49410
49509
  const reportsDir = join38(project.value.path, ".godot", ...ours.split("/"));
49411
49510
  let report = null;
49412
49511
  let reportProblem = null;
49413
- const casePrinted = readRun(existsSync30(engineLog) ? readFileSync34(engineLog, "utf8") : "");
49512
+ const casePrinted = withNestedErrors(readRun(existsSync30(engineLog) ? readFileSync34(engineLog, "utf8") : ""), run.log.all);
49414
49513
  const declaredErrors = readExpected(expectedFile);
49415
49514
  try {
49416
49515
  const written = existsSync30(reportsDir) && notCompiled === null ? readdirSync17(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
@@ -49525,7 +49624,7 @@ class GodotServer {
49525
49624
  path: suite.path,
49526
49625
  name: report.suites.find((entry) => entry.path === suite.path)?.name ?? suite.path,
49527
49626
  orphans: suite.orphans,
49528
- message: `${suite.orphans} node${suite.orphans === 1 ? "" : "s"} were still in the tree when this suite finished. A node built in a test and neither freed nor added to the tree is the usual cause; free it, or add it with auto_free().`
49627
+ message: `${suite.orphans === 1 ? "1 node" : `${String(suite.orphans)} nodes`} built during this suite ${suite.orphans === 1 ? "was" : "were"} neither freed nor in the tree when it finished, which gdUnit4 counts as orphans. Free each one, or make it with auto_free(), which frees it once the case is over.`
49529
49628
  }));
49530
49629
  const judgedErrors = unexpectedErrors(casePrinted, declaredErrors, [...casePrinted.suites.keys()]);
49531
49630
  const uncaughtTexts = new Set(uncaught.map((entry) => entry.text));
@@ -49555,7 +49654,8 @@ ${error.text}`;
49555
49654
  const declaredNotPrinted = judgedErrors.neverPrinted.map((missing) => ({
49556
49655
  suite: missing.suite,
49557
49656
  case: missing.during,
49558
- text: missing.text
49657
+ text: missing.text,
49658
+ printedInstead: judgedErrors.printed.find((error) => error.suite === missing.suite && error.during === missing.during && error.text.includes(missing.text))?.text
49559
49659
  }));
49560
49660
  const erring = (path) => ({
49561
49661
  undeclared: undeclaredPrinted.filter((error) => error.suite === path).length,
@@ -49580,6 +49680,9 @@ ${error.text}`;
49580
49680
  const uncaughtFolded = foldedRepeats(uncaught.map(aboveTheRunner));
49581
49681
  const listedAsUncaught = new Set(uncaughtFolded.map((entry) => entry.index));
49582
49682
  const notApplied = ignoredYetRan(report, ignored);
49683
+ const scripts = ignored.length === 0 ? null : new ProjectScripts(project.value.path);
49684
+ const ignoreRead = ignoredLeftOut(report, ignored, asked.flatMap((path) => scripts === null ? [] : path.endsWith(".gd") ? scripts.suite(path) ?? [] : scripts.suitesUnder([path])));
49685
+ const unmatched = ignoreRead.unmatched.filter((entry) => !notApplied.some((one) => one.entry === entry));
49583
49686
  const testsNotRun = [
49584
49687
  ...new Set((selection.value?.selected ?? []).filter((one) => !report.suites.some((suite) => (suite.path === one.suite || suite.name === basename12(one.suite, ".gd")) && suite.cases.some((ran) => ran.name === one.test || new RegExp(`^${one.test}\\W`).test(ran.name)))).map((one) => `${basename12(one.suite, ".gd")}:${one.test}`))
49585
49688
  ];
@@ -49613,6 +49716,8 @@ ${error.text}`;
49613
49716
  orphans: orphans.total > 0 ? orphans.total : undefined,
49614
49717
  notRun: notRun > 0 ? notRun : undefined,
49615
49718
  ignoreNotApplied: notApplied.length > 0 ? notApplied : undefined,
49719
+ ignored: ignoreRead.leftOut.length > 0 ? ignoreRead.leftOut : undefined,
49720
+ ignoreUnmatched: unmatched.length > 0 ? unmatched : undefined,
49616
49721
  ...printedLines,
49617
49722
  savesNote,
49618
49723
  ...scanned,
@@ -49621,6 +49726,7 @@ ${error.text}`;
49621
49726
  testsNotRun.length > 0 ? `${testsNotRun.join(", ")} ${testsNotRun.length === 1 ? "was" : "were"} asked for and not run, so the run is not called passed: gdUnit4 runs a test_ function only in a script that extends GdUnitTestSuite, and leaves out a suite whose script does not load.` : undefined,
49622
49727
  briefed > 0 ? `${briefed} of the ${failed.length} failing cases, those with the largest reports, are named with their first message only, since their reports would have made the answer too large to deliver; pass one case's suite path to see its reports.` : undefined,
49623
49728
  notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
49729
+ unmatched.length > 0 ? `${unmatched.join(", ")} ${unmatched.length === 1 ? "names" : "name"} no suite or case under the paths run, so ${unmatched.length === 1 ? "it" : "they"} left nothing out.` : undefined,
49624
49730
  uncaughtNote(uncaught),
49625
49731
  undeclaredErrors.length > 0 ? `An engine error a suite does not declare fails the run, as gdharness test judges it; ${declaringAdvice(undeclaredErrors[0]?.case ?? "test_name")}, once for each time it prints it.` : undefined,
49626
49732
  declaredNotPrinted.length > 0 ? "A declared error the case did not print fails the run too, since the case no longer reaches what it was written to test." : undefined,
@@ -53380,6 +53486,24 @@ ${error.text}`;
53380
53486
  }
53381
53487
  return session.whereItStopped();
53382
53488
  }
53489
+ async keptHoldOn(endpoint) {
53490
+ const kept = this.keptRun();
53491
+ if (kept === null || this.announcedPidOf(kept) !== endpoint.pid) {
53492
+ return null;
53493
+ }
53494
+ const answered = await askTheKeepersDebugger(kept.keeper, { op: "stack" }, KEEPER_DEBUG_MS);
53495
+ if ("unreached" in answered || !Array.isArray(answered["stack"])) {
53496
+ return null;
53497
+ }
53498
+ const top = answered["stack"][0];
53499
+ const held = answered["held"];
53500
+ const reason = typeof held === "object" && held !== null && typeof held["reason"] === "string" ? ` (${String(held["reason"])})` : "";
53501
+ if (typeof top !== "object" || top === null) {
53502
+ return `held by its debugger between frames${reason}`;
53503
+ }
53504
+ const frame = top;
53505
+ return `held by its debugger at ${String(frame["script"])}:${String(frame["line"])} in ${String(frame["name"])}${reason}`;
53506
+ }
53383
53507
  async holdOf(run, going) {
53384
53508
  if (!run.throughEditor || !(going ?? await this.runStillGoing(run))) {
53385
53509
  return { heldAt: null };
@@ -53495,6 +53619,14 @@ ${error.text}`;
53495
53619
  "editor_output says heldAt for the run, and editor_run stop ends it"
53496
53620
  ]);
53497
53621
  }
53622
+ const keptHeld = await this.keptHoldOn(choice.endpoint);
53623
+ if (keptHeld !== null) {
53624
+ return this.createErrorResponse(`The game (pid ${choice.endpoint.pid}) is ${keptHeld}, and answers no runtime call while it is.`, [
53625
+ "debug_control continue lets it go, after which this call answers",
53626
+ "debug_state stack reads where it is held, and debug_state variables what it is holding there",
53627
+ "editor_run stop ends it"
53628
+ ]);
53629
+ }
53498
53630
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
53499
53631
  const saveTo = expectsScreenshot && typeof outputPath === "string" ? outputPath : null;
53500
53632
  const sendsThePicture = readBoolean(callArgs, "inline") !== false;
@@ -53538,12 +53670,14 @@ ${error.text}`;
53538
53670
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, expectsScreenshot ? () => discard(screenshotDir) : "keep");
53539
53671
  savedLater = !reply.ok && reply.stillWithTheGame === true;
53540
53672
  if (!reply.ok && reply.requestId !== undefined) {
53673
+ const heldSince = await this.keptHoldOn(choice.endpoint);
53541
53674
  return this.jsonTextResponse({
53542
53675
  pending: true,
53543
53676
  requestId: reply.requestId,
53544
53677
  command,
53545
53678
  waitedMs: timeoutMs,
53546
- note: `${reply.message} ${longerWaitNote(asked.tool, asked.op, timeoutMs)}`,
53679
+ ...heldSince === null ? {} : { heldAt: heldSince },
53680
+ note: heldSince === null ? `${reply.message} ${longerWaitNote(asked.tool, asked.op, timeoutMs)}` : `The game is ${heldSince}, so it answers once it goes on: debug_control continue lets it go, and runtime_invoke op result with requestId ${reply.requestId} then answers with what the call returned.`,
53547
53681
  ...answeredBy,
53548
53682
  ...alongside()
53549
53683
  });
@@ -53807,6 +53941,7 @@ var init_server = __esm(() => {
53807
53941
  });
53808
53942
  PROJECT_INFO_SECTIONS2 = dictionary({
53809
53943
  autoloads: { operation: "list_autoloads", params: () => ({}) },
53944
+ classes: { operation: "list_script_classes", params: () => ({}) },
53810
53945
  input_actions: { operation: "list_input_actions", params: () => ({}) },
53811
53946
  plugins: { operation: "list_plugins", params: () => ({}) },
53812
53947
  export_presets: { operation: "list_export_presets", params: () => ({}) },
@@ -54819,10 +54954,10 @@ init_setup();
54819
54954
  init_setup();
54820
54955
  init_tool_definitions();
54821
54956
  init_tool_reference();
54822
- import { existsSync as existsSync14, mkdirSync as mkdirSync5, readdirSync as readdirSync9, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
54823
- import { basename as basename7, dirname as dirname9, join as join16 } from "node:path";
54957
+ import { existsSync as existsSync15, mkdirSync as mkdirSync5, readdirSync as readdirSync10, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
54958
+ import { basename as basename7, dirname as dirname9, join as join17 } from "node:path";
54824
54959
  var SKILL_NAME = "gdharness";
54825
- var SHARED_SKILLS = join16(".agents", "skills");
54960
+ var SHARED_SKILLS = join17(".agents", "skills");
54826
54961
  function skillMarkdown(version) {
54827
54962
  return `---
54828
54963
  name: ${SKILL_NAME}
@@ -55059,27 +55194,27 @@ what they report while starting comes back under \`engine_messages_before_operat
55059
55194
  function skillFiles(version) {
55060
55195
  return new Map([
55061
55196
  ["SKILL.md", skillMarkdown(version)],
55062
- [join16("references", "tools.md"), renderToolsMarkdown()]
55197
+ [join17("references", "tools.md"), renderToolsMarkdown()]
55063
55198
  ]);
55064
55199
  }
55065
55200
  function skillDirectories(harnesses, projectPath, exists) {
55066
- const directories = [join16(projectPath, SHARED_SKILLS, SKILL_NAME)];
55201
+ const directories = [join17(projectPath, SHARED_SKILLS, SKILL_NAME)];
55067
55202
  for (const harness of harnesses) {
55068
55203
  if (harness.skills === undefined) {
55069
55204
  continue;
55070
55205
  }
55071
- const own = join16(projectPath, harness.skills.dir);
55072
- if ((!harness.skills.shared || exists(own)) && !directories.includes(join16(own, SKILL_NAME))) {
55073
- directories.push(join16(own, SKILL_NAME));
55206
+ const own = join17(projectPath, harness.skills.dir);
55207
+ if ((!harness.skills.shared || exists(own)) && !directories.includes(join17(own, SKILL_NAME))) {
55208
+ directories.push(join17(own, SKILL_NAME));
55074
55209
  }
55075
55210
  }
55076
55211
  return directories;
55077
55212
  }
55078
55213
  function everySkillDirectory(projectPath, harnesses) {
55079
- const directories = new Set([join16(projectPath, SHARED_SKILLS, SKILL_NAME)]);
55214
+ const directories = new Set([join17(projectPath, SHARED_SKILLS, SKILL_NAME)]);
55080
55215
  for (const harness of harnesses) {
55081
55216
  if (harness.skills !== undefined) {
55082
- directories.add(join16(projectPath, harness.skills.dir, SKILL_NAME));
55217
+ directories.add(join17(projectPath, harness.skills.dir, SKILL_NAME));
55083
55218
  }
55084
55219
  }
55085
55220
  return [...directories];
@@ -55087,7 +55222,7 @@ function everySkillDirectory(projectPath, harnesses) {
55087
55222
  function removeSkill(directories, projectPath) {
55088
55223
  const removed = [];
55089
55224
  for (const directory of directories) {
55090
- if (existsSync14(directory)) {
55225
+ if (existsSync15(directory)) {
55091
55226
  rmSync6(directory, { recursive: true, force: true });
55092
55227
  pruneEmpty(dirname9(directory), projectPath);
55093
55228
  removed.push(directory);
@@ -55096,10 +55231,10 @@ function removeSkill(directories, projectPath) {
55096
55231
  return removed;
55097
55232
  }
55098
55233
  function pruneEmpty(directory, projectPath) {
55099
- const shared = join16(projectPath, ".agents");
55234
+ const shared = join17(projectPath, ".agents");
55100
55235
  let path = directory;
55101
55236
  while (path !== projectPath && (basename7(path) === "skills" || path === shared)) {
55102
- if (readdirSync9(path).length > 0) {
55237
+ if (readdirSync10(path).length > 0) {
55103
55238
  return;
55104
55239
  }
55105
55240
  rmSync6(path, { recursive: true, force: true });
@@ -55109,10 +55244,10 @@ function pruneEmpty(directory, projectPath) {
55109
55244
  function writeSkill(directories, version) {
55110
55245
  const files = skillFiles(version);
55111
55246
  return directories.map((directory) => {
55112
- const replaced = existsSync14(directory);
55247
+ const replaced = existsSync15(directory);
55113
55248
  rmSync6(directory, { recursive: true, force: true });
55114
55249
  for (const [name, contents] of files) {
55115
- const path = join16(directory, name);
55250
+ const path = join17(directory, name);
55116
55251
  mkdirSync5(dirname9(path), { recursive: true });
55117
55252
  writeFileSync7(path, contents, "utf8");
55118
55253
  }
@@ -55125,27 +55260,27 @@ init_alive();
55125
55260
  init_desktop();
55126
55261
  import {
55127
55262
  closeSync as closeSync3,
55128
- existsSync as existsSync18,
55263
+ existsSync as existsSync19,
55129
55264
  fstatSync,
55130
55265
  mkdirSync as mkdirSync8,
55131
55266
  openSync as openSync3,
55132
- readdirSync as readdirSync11,
55133
- readFileSync as readFileSync18,
55267
+ readdirSync as readdirSync12,
55268
+ readFileSync as readFileSync19,
55134
55269
  readSync,
55135
55270
  rmSync as rmSync10,
55136
- statSync as statSync11,
55271
+ statSync as statSync12,
55137
55272
  writeFileSync as writeFileSync10
55138
55273
  } from "node:fs";
55139
55274
  import { availableParallelism, freemem, totalmem } from "node:os";
55140
- import { isAbsolute as isAbsolute4, join as join21, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
55275
+ import { isAbsolute as isAbsolute4, join as join22, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
55141
55276
  import process12 from "node:process";
55142
55277
  import { setTimeout as delay3 } from "node:timers/promises";
55143
55278
 
55144
55279
  // src/engine-slots.ts
55145
55280
  init_alive();
55146
55281
  init_process_children();
55147
- import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as readFileSync13, rmSync as rmSync8, statSync as statSync9, writeSync as writeSync2 } from "node:fs";
55148
- import { join as join18 } from "node:path";
55282
+ import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as readFileSync14, rmSync as rmSync8, statSync as statSync10, writeSync as writeSync2 } from "node:fs";
55283
+ import { join as join19 } from "node:path";
55149
55284
  import process11 from "node:process";
55150
55285
  import { setTimeout as delay2 } from "node:timers/promises";
55151
55286
  var START_TOLERANCE_MS = 2000;
@@ -55196,7 +55331,7 @@ class EngineSlots {
55196
55331
  for (;; ) {
55197
55332
  signal?.throwIfAborted();
55198
55333
  for (let slot = 0;slot < this.count; slot += 1) {
55199
- const lock = join18(this.directory, `${String(slot)}.lock`);
55334
+ const lock = join19(this.directory, `${String(slot)}.lock`);
55200
55335
  if (this.made(lock) || await this.takenOver(lock)) {
55201
55336
  this.held.add(lock);
55202
55337
  return lock;
@@ -55213,7 +55348,7 @@ class EngineSlots {
55213
55348
  release(lock) {
55214
55349
  this.held.delete(lock);
55215
55350
  try {
55216
- if (holderOf(readFileSync13(lock, "utf8"))?.pid === this.holder.pid) {
55351
+ if (holderOf(readFileSync14(lock, "utf8"))?.pid === this.holder.pid) {
55217
55352
  rmSync8(lock, { force: true });
55218
55353
  }
55219
55354
  } catch {}
@@ -55239,7 +55374,7 @@ class EngineSlots {
55239
55374
  const steal = `${lock}.steal`;
55240
55375
  if (!this.made(steal)) {
55241
55376
  try {
55242
- if (Date.now() - statSync9(steal).mtimeMs > UNWRITTEN_MS) {
55377
+ if (Date.now() - statSync10(steal).mtimeMs > UNWRITTEN_MS) {
55243
55378
  rmSync8(steal, { force: true });
55244
55379
  }
55245
55380
  } catch {}
@@ -55263,8 +55398,8 @@ class EngineSlots {
55263
55398
  let content;
55264
55399
  let writtenAt;
55265
55400
  try {
55266
- content = readFileSync13(lock, "utf8");
55267
- writtenAt = statSync9(lock).mtimeMs;
55401
+ content = readFileSync14(lock, "utf8");
55402
+ writtenAt = statSync10(lock).mtimeMs;
55268
55403
  } catch {
55269
55404
  return false;
55270
55405
  }
@@ -55291,9 +55426,9 @@ init_spawn();
55291
55426
 
55292
55427
  // src/suite-fingerprint.ts
55293
55428
  init_class_cache();
55294
- import { createHash as createHash5 } from "node:crypto";
55295
- import { existsSync as existsSync16, readdirSync as readdirSync10, readFileSync as readFileSync14, statSync as statSync10 } from "node:fs";
55296
- import { join as join19, posix as posix2 } from "node:path";
55429
+ import { createHash as createHash6 } from "node:crypto";
55430
+ import { existsSync as existsSync17, readdirSync as readdirSync11, readFileSync as readFileSync15, statSync as statSync11 } from "node:fs";
55431
+ import { join as join20, posix as posix2 } from "node:path";
55297
55432
  var RES_LITERAL = /res:\/\/[^"'\s)\]<>]*/g;
55298
55433
  var RELATIVE_LOAD = /\b(?:pre)?load\(\s*["']((?!res:\/\/|uid:\/\/)[^"']+)["']/g;
55299
55434
  var REACHES_AT_RUN_TIME = [
@@ -55313,7 +55448,7 @@ class SuiteFingerprints {
55313
55448
  constructor(scripts, always) {
55314
55449
  this.scripts = scripts;
55315
55450
  this.everywhere = ["res://project.godot", ...extensionsOf(scripts.projectPath)];
55316
- this.alwaysDigest = createHash5("sha256").update(always.join(`
55451
+ this.alwaysDigest = createHash6("sha256").update(always.join(`
55317
55452
  `)).digest("hex");
55318
55453
  }
55319
55454
  of(suite) {
@@ -55330,7 +55465,7 @@ class SuiteFingerprints {
55330
55465
  }
55331
55466
  pending.push(...reached.reaches);
55332
55467
  }
55333
- const hash = createHash5("sha256").update(this.alwaysDigest);
55468
+ const hash = createHash6("sha256").update(this.alwaysDigest);
55334
55469
  for (const path of [...seen].sort()) {
55335
55470
  hash.update(`${path}
55336
55471
  ${this.read(path).digest}
@@ -55349,20 +55484,20 @@ ${this.read(path).digest}
55349
55484
  return reached;
55350
55485
  }
55351
55486
  addon(root) {
55352
- const hash = createHash5("sha256");
55487
+ const hash = createHash6("sha256");
55353
55488
  const walk = (directory, under) => {
55354
- for (const entry of readdirSync10(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
55355
- const file = join19(directory, entry.name);
55489
+ for (const entry of readdirSync11(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
55490
+ const file = join20(directory, entry.name);
55356
55491
  if (entry.isDirectory()) {
55357
55492
  walk(file, `${under}${entry.name}/`);
55358
55493
  } else if (entry.isFile()) {
55359
55494
  hash.update(`${under}${entry.name}
55360
- `).update(readFileSync14(file));
55495
+ `).update(readFileSync15(file));
55361
55496
  }
55362
55497
  }
55363
55498
  };
55364
- const directory = join19(this.scripts.projectPath, root.slice("res://".length));
55365
- if (existsSync16(directory)) {
55499
+ const directory = join20(this.scripts.projectPath, root.slice("res://".length));
55500
+ if (existsSync17(directory)) {
55366
55501
  walk(directory, "");
55367
55502
  }
55368
55503
  return { digest: hash.digest("hex"), reaches: [], unreadable: null };
@@ -55371,26 +55506,26 @@ ${this.read(path).digest}
55371
55506
  if (PLACEHOLDER.test(path)) {
55372
55507
  return { digest: "", reaches: [], unreadable: `${path} is filled in at run time` };
55373
55508
  }
55374
- const file = join19(this.scripts.projectPath, path.slice("res://".length));
55375
- if (!existsSync16(file)) {
55509
+ const file = join20(this.scripts.projectPath, path.slice("res://".length));
55510
+ if (!existsSync17(file)) {
55376
55511
  return { digest: "missing", reaches: [], unreadable: null };
55377
55512
  }
55378
- if (statSync10(file).isDirectory()) {
55379
- const inside = readdirSync10(file, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".")).map((entry) => `${path.endsWith("/") ? path : `${path}/`}${entry.name}`).sort();
55513
+ if (statSync11(file).isDirectory()) {
55514
+ const inside = readdirSync11(file, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".")).map((entry) => `${path.endsWith("/") ? path : `${path}/`}${entry.name}`).sort();
55380
55515
  return { digest: inside.join(`
55381
55516
  `), reaches: inside, unreadable: null };
55382
55517
  }
55383
55518
  if (BINARY_RESOURCE.test(path)) {
55384
55519
  return { digest: "", reaches: [], unreadable: `${path} is a binary resource` };
55385
55520
  }
55386
- const bytes = readFileSync14(file);
55387
- const digest = createHash5("sha256").update(bytes).digest("hex");
55521
+ const bytes = readFileSync15(file);
55522
+ const digest = createHash6("sha256").update(bytes).digest("hex");
55388
55523
  const text = bytes.toString("latin1");
55389
55524
  const reaches = new Set;
55390
55525
  for (const [literal] of text.matchAll(RES_LITERAL)) {
55391
55526
  reaches.add(literal);
55392
55527
  }
55393
- if (existsSync16(`${file}.import`)) {
55528
+ if (existsSync17(`${file}.import`)) {
55394
55529
  reaches.add(`${path}.import`);
55395
55530
  }
55396
55531
  if (path.endsWith(".gd")) {
@@ -55420,8 +55555,8 @@ ${this.read(path).digest}
55420
55555
  }
55421
55556
  }
55422
55557
  function extensionsOf(projectPath) {
55423
- const list = join19(projectPath, ".godot", "extension_list.cfg");
55424
- return existsSync16(list) ? readFileSync14(list, "utf8").split(`
55558
+ const list = join20(projectPath, ".godot", "extension_list.cfg");
55559
+ return existsSync17(list) ? readFileSync15(list, "utf8").split(`
55425
55560
  `).map((line) => line.trim()).filter((line) => line.startsWith("res://")) : [];
55426
55561
  }
55427
55562
 
@@ -55430,18 +55565,18 @@ init_suite_judge();
55430
55565
  init_suite_plan();
55431
55566
 
55432
55567
  // src/version.ts
55433
- import { readFileSync as readFileSync17 } from "node:fs";
55434
- import { dirname as dirname11, join as join20 } from "node:path";
55568
+ import { readFileSync as readFileSync18 } from "node:fs";
55569
+ import { dirname as dirname11, join as join21 } from "node:path";
55435
55570
  import { fileURLToPath as fileURLToPath2 } from "node:url";
55436
55571
  function getLocalVersion() {
55437
55572
  const currentDirectory = dirname11(fileURLToPath2(import.meta.url));
55438
55573
  const packageCandidates = [
55439
- join20(currentDirectory, "..", "package.json"),
55440
- join20(currentDirectory, "..", "..", "package.json")
55574
+ join21(currentDirectory, "..", "package.json"),
55575
+ join21(currentDirectory, "..", "..", "package.json")
55441
55576
  ];
55442
55577
  for (const packagePath of packageCandidates) {
55443
55578
  try {
55444
- const value = JSON.parse(readFileSync17(packagePath, "utf-8"));
55579
+ const value = JSON.parse(readFileSync18(packagePath, "utf-8"));
55445
55580
  if (typeof value === "object" && value !== null && "name" in value && value.name === "gdharness" && "version" in value && typeof value.version === "string") {
55446
55581
  return value.version;
55447
55582
  }
@@ -55493,9 +55628,9 @@ function testOptionsOf(args, cwd) {
55493
55628
  }
55494
55629
  const first = positional[0];
55495
55630
  const named = first === undefined ? null : resolve4(cwd, first);
55496
- const projectNamed = named !== null && existsSync18(join21(named, "project.godot"));
55631
+ const projectNamed = named !== null && existsSync19(join22(named, "project.godot"));
55497
55632
  const projectPath = projectNamed ? named : resolve4(cwd);
55498
- if (!existsSync18(join21(projectPath, "project.godot"))) {
55633
+ if (!existsSync19(join22(projectPath, "project.godot"))) {
55499
55634
  throw new TestUsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
55500
55635
  }
55501
55636
  const last = (flag) => valued.get(flag)?.at(-1);
@@ -55539,7 +55674,7 @@ function resPathOf(entry, projectPath, cwd) {
55539
55674
  const file = caseAt?.[1] ?? entry;
55540
55675
  const fromProject = resolve4(projectPath, file);
55541
55676
  const fromHere = resolve4(cwd, file);
55542
- const absolute = isAbsolute4(file) || !existsSync18(fromProject) && existsSync18(fromHere) ? fromHere : fromProject;
55677
+ const absolute = isAbsolute4(file) || !existsSync19(fromProject) && existsSync19(fromHere) ? fromHere : fromProject;
55543
55678
  const inside = relative3(projectPath, absolute);
55544
55679
  if (inside.startsWith("..") || isAbsolute4(inside)) {
55545
55680
  throw new TestUsageError(`${entry} is not inside the project at ${projectPath}.`);
@@ -55551,7 +55686,7 @@ function homeOf(projectPath) {
55551
55686
  return mainCheckoutOf(projectPath) ?? projectPath;
55552
55687
  }
55553
55688
  function selectedSuites(scripts, options) {
55554
- const roots = options.roots.length > 0 ? options.roots : TEST_DIRECTORIES.filter((root) => existsSync18(join21(options.projectPath, root))).map((root) => `res://${root}`);
55689
+ const roots = options.roots.length > 0 ? options.roots : TEST_DIRECTORIES.filter((root) => existsSync19(join22(options.projectPath, root))).map((root) => `res://${root}`);
55555
55690
  if (options.selected.length === 0) {
55556
55691
  const found = scripts.suitesUnder(roots);
55557
55692
  if (found.length === 0) {
@@ -55563,10 +55698,10 @@ function selectedSuites(scripts, options) {
55563
55698
  for (const entry of options.selected) {
55564
55699
  const caseAt = /^(res:\/\/.*\.gd):(\w+)$/.exec(entry);
55565
55700
  const path = caseAt?.[1] ?? entry;
55566
- const directory = join21(options.projectPath, path.slice(6));
55567
- const suites = existsSync18(directory) && statSync11(directory).isDirectory() ? scripts.suitesUnder([path.replace(/\/$/, "")]) : [scripts.suite(path)].filter((suite) => suite !== null);
55701
+ const directory = join22(options.projectPath, path.slice(6));
55702
+ const suites = existsSync19(directory) && statSync12(directory).isDirectory() ? scripts.suitesUnder([path.replace(/\/$/, "")]) : [scripts.suite(path)].filter((suite) => suite !== null);
55568
55703
  if (suites.length === 0) {
55569
- throw new TestUsageError(existsSync18(directory) ? `${path} holds no gdUnit4 suite: nothing there extends GdUnitTestSuite and declares a test_ function.` : `Nothing at ${path} in the project at ${options.projectPath}.`);
55704
+ throw new TestUsageError(existsSync19(directory) ? `${path} holds no gdUnit4 suite: nothing there extends GdUnitTestSuite and declares a test_ function.` : `Nothing at ${path} in the project at ${options.projectPath}.`);
55570
55705
  }
55571
55706
  for (const suite of suites) {
55572
55707
  const picked = chosen.get(suite.path) ?? { suite, cases: new Set };
@@ -55643,17 +55778,17 @@ async function runTestCommand(options, godotPath) {
55643
55778
  const startedAt = Date.now();
55644
55779
  const scripts = new ProjectScripts(options.projectPath);
55645
55780
  const home = homeOf(options.projectPath);
55646
- const state = join21(home, ".godot", "gdharness");
55647
- const timesFile = join21(state, "suite-times.json");
55648
- const passedFile = join21(state, "suite-passed.json");
55781
+ const state = join22(home, ".godot", "gdharness");
55782
+ const timesFile = join22(state, "suite-times.json");
55783
+ const passedFile = join22(state, "suite-passed.json");
55649
55784
  let suites = selectedSuites(scripts, options);
55650
55785
  const wholeSuites = suites.map((suite) => scripts.suite(suite.path) ?? suite);
55651
55786
  const declared = new Map(wholeSuites.map((suite) => [suite.path, suite.cases]));
55652
- const testRuns = join21(options.projectPath, ".godot", "gdharness", "test-runs");
55787
+ const testRuns = join22(options.projectPath, ".godot", "gdharness", "test-runs");
55653
55788
  sweepRunDirectories(testRuns);
55654
- const runDirectory = join21(testRuns, `${String(startedAt)}-${String(process12.pid)}`);
55789
+ const runDirectory = join22(testRuns, `${String(startedAt)}-${String(process12.pid)}`);
55655
55790
  mkdirSync8(runDirectory, { recursive: true });
55656
- const slots = new EngineSlots(options.slotsDirectory ?? join21(home, ".godot", "gate", "slots"), options.slotCount ?? Math.min(MOST_ENGINES, availableParallelism()), { pid: process12.pid, started: await ownStart() });
55791
+ const slots = new EngineSlots(options.slotsDirectory ?? join22(home, ".godot", "gate", "slots"), options.slotCount ?? Math.min(MOST_ENGINES, availableParallelism()), { pid: process12.pid, started: await ownStart() });
55657
55792
  const running = new Set;
55658
55793
  const interrupted = () => {
55659
55794
  for (const end of running) {
@@ -55682,9 +55817,9 @@ async function runTestCommand(options, godotPath) {
55682
55817
  }
55683
55818
  const fingerprints = new SuiteFingerprints(scripts, [
55684
55819
  getLocalVersion(),
55685
- readFileSync18(shippedListedRunner(), "utf8"),
55820
+ readFileSync19(shippedListedRunner(), "utf8"),
55686
55821
  ...options.reachAlso.map((path) => `${path}
55687
- ${existsSync18(path) ? readFileSync18(path, "latin1") : "missing"}`)
55822
+ ${existsSync19(path) ? readFileSync19(path, "latin1") : "missing"}`)
55688
55823
  ]);
55689
55824
  const printsOf = new Map(suites.map((suite) => [suite.path, fingerprints.of(suite)]));
55690
55825
  if (options.changedOnly) {
@@ -55709,7 +55844,7 @@ ${existsSync18(path) ? readFileSync18(path, "latin1") : "missing"}`)
55709
55844
  const plans = options.order === null ? dealt(units, declared, room.engines) : [inOneEngine(ordered(units, options.order), declared)];
55710
55845
  const functions = suites.reduce((sum, suite) => sum + suite.cases.length, 0);
55711
55846
  const held = options.order === null && room.held !== undefined && plans.length === room.engines ? `, of the ${String(options.engines)} asked for, since ${room.held}` : "";
55712
- const how = options.order === null ? `${plans.some((plan) => plan.windowed) ? ", one of them windowed" : ""}${held}` : options.order === "reversed" ? ", in reverse order of path" : `, shuffled by ${String(options.order)}`;
55847
+ const how = options.order === null ? `${windowedSaid(plans)}${held}` : options.order === "reversed" ? ", in reverse order of path" : `, shuffled by ${String(options.order)}`;
55713
55848
  console.log(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${how}.`);
55714
55849
  const split = [...new Set(units.map((unit) => unit.suite))].filter((path) => units.filter((unit) => unit.suite === path).length > 1);
55715
55850
  if (plans.length > 1 && split.length > 0) {
@@ -55737,7 +55872,7 @@ function plural(count, noun) {
55737
55872
  }
55738
55873
  function readRecord(file) {
55739
55874
  try {
55740
- const parsed = JSON.parse(readFileSync18(file, "utf8"));
55875
+ const parsed = JSON.parse(readFileSync19(file, "utf8"));
55741
55876
  return typeof parsed === "object" && parsed !== null ? parsed : {};
55742
55877
  } catch {
55743
55878
  return {};
@@ -55764,20 +55899,20 @@ function recordPassed(file, ran, printsOf) {
55764
55899
  writeJson(file, passed);
55765
55900
  }
55766
55901
  function sweepRunDirectories(testRuns) {
55767
- if (!existsSync18(testRuns)) {
55902
+ if (!existsSync19(testRuns)) {
55768
55903
  return;
55769
55904
  }
55770
- for (const entry of readdirSync11(testRuns)) {
55905
+ for (const entry of readdirSync12(testRuns)) {
55771
55906
  const pid = Number(/^\d+-(\d+)$/.exec(entry)?.[1]);
55772
55907
  if (!Number.isInteger(pid) || !alive(pid)) {
55773
- rmSync10(join21(testRuns, entry), { recursive: true, force: true });
55908
+ rmSync10(join22(testRuns, entry), { recursive: true, force: true });
55774
55909
  }
55775
55910
  }
55776
55911
  }
55777
55912
  async function importProject(launch) {
55778
- const directory = join21(launch.runDirectory, "import");
55779
- mkdirSync8(join21(directory, "user"), { recursive: true });
55780
- const log = join21(directory, "import.log");
55913
+ const directory = join22(launch.runDirectory, "import");
55914
+ mkdirSync8(join22(directory, "user"), { recursive: true });
55915
+ const log = join22(directory, "import.log");
55781
55916
  const lock = await launch.slots.take();
55782
55917
  try {
55783
55918
  const ended = await watched(launch, ["--headless", "--audio-driver", "Dummy", "--path", launch.projectPath, "--import"], directory, log, false, launch.silenceFloorSeconds);
@@ -55790,11 +55925,11 @@ async function importProject(launch) {
55790
55925
  }
55791
55926
  }
55792
55927
  async function runEngine2(launch, plan, index, units, again = "") {
55793
- const directory = join21(launch.runDirectory, `engine-${String(index)}${again === "" ? "" : `-${again}`}`);
55794
- mkdirSync8(join21(directory, "user"), { recursive: true });
55795
- const suitesFile = join21(directory, "suites.txt");
55796
- const skippingFile = join21(directory, "skipping.txt");
55797
- const expectedFile = join21(directory, "expected.json");
55928
+ const directory = join22(launch.runDirectory, `engine-${String(index)}${again === "" ? "" : `-${again}`}`);
55929
+ mkdirSync8(join22(directory, "user"), { recursive: true });
55930
+ const suitesFile = join22(directory, "suites.txt");
55931
+ const skippingFile = join22(directory, "skipping.txt");
55932
+ const expectedFile = join22(directory, "expected.json");
55798
55933
  writeFileSync10(suitesFile, `${plan.suites.map((suite) => suite.path).join(`
55799
55934
  `)}
55800
55935
  `);
@@ -55814,7 +55949,7 @@ async function runEngine2(launch, plan, index, units, again = "") {
55814
55949
  ...plan.windowed ? [] : ["--ignoreHeadlessMode"],
55815
55950
  "-c",
55816
55951
  "-rd",
55817
- `res://${relative3(launch.projectPath, join21(directory, "reports")).split(sep2).join("/")}`,
55952
+ `res://${relative3(launch.projectPath, join22(directory, "reports")).split(sep2).join("/")}`,
55818
55953
  "-rc",
55819
55954
  "1",
55820
55955
  "--",
@@ -55831,7 +55966,7 @@ async function runEngine2(launch, plan, index, units, again = "") {
55831
55966
  const held = new Set(plan.suites.map((suite) => suite.path));
55832
55967
  const slowest = Math.max(0, ...units.filter((unit) => held.has(unit.suite)).map((unit) => unit.cost));
55833
55968
  const silence = Math.max(launch.silenceFloorSeconds, SILENCE_TIMES_SLOWEST * slowest);
55834
- const log = join21(directory, "engine.log");
55969
+ const log = join22(directory, "engine.log");
55835
55970
  const lock = await launch.slots.take();
55836
55971
  const began = Date.now();
55837
55972
  let end;
@@ -55840,7 +55975,7 @@ async function runEngine2(launch, plan, index, units, again = "") {
55840
55975
  } finally {
55841
55976
  launch.slots.release(lock);
55842
55977
  }
55843
- const printed = readRun(readFileSync18(log, "utf8"));
55978
+ const printed = readRun(readFileSync19(log, "utf8"));
55844
55979
  const assignment = { suites: plan.suites, expected: readExpected(expectedFile) };
55845
55980
  return {
55846
55981
  index,
@@ -55852,7 +55987,7 @@ async function runEngine2(launch, plan, index, units, again = "") {
55852
55987
  };
55853
55988
  }
55854
55989
  async function watched(launch, args, directory, log, windowed, silenceSeconds) {
55855
- const user = join21(directory, "user");
55990
+ const user = join22(directory, "user");
55856
55991
  const env = { ...process12.env, APPDATA: user, XDG_DATA_HOME: user };
55857
55992
  const output = openSync3(log, "w");
55858
55993
  let child;
@@ -55864,12 +55999,12 @@ async function watched(launch, args, directory, log, windowed, silenceSeconds) {
55864
55999
  const how = windowed ? "on a desktop of its own" : "through the desktop helper";
55865
56000
  return { code: null, signal: null, ended: `the engine could not be started ${how}: ${compiled}` };
55866
56001
  }
55867
- const stopFile = join21(directory, "stop");
56002
+ const stopFile = join22(directory, "stop");
55868
56003
  const how = windowed ? { desktop: HIDDEN_DESKTOP, directory: launch.projectPath } : { directory: launch.projectPath };
55869
56004
  const wrapper = throughHelper(how, launch.godotPath, args, {
55870
- spec: join21(directory, "spec.json"),
55871
- pid: join21(directory, "pid"),
55872
- error: join21(directory, "error"),
56005
+ spec: join22(directory, "spec.json"),
56006
+ pid: join22(directory, "pid"),
56007
+ error: join22(directory, "error"),
55873
56008
  stop: stopFile
55874
56009
  });
55875
56010
  child = spawnHidden(wrapper.command, wrapper.args, {