gdharness 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -10183,7 +10183,7 @@ import {
10183
10183
  readSync
10184
10184
  } from "node:fs";
10185
10185
  import { tmpdir as tmpdir4 } from "node:os";
10186
- import { basename as basename3, dirname as dirname6, join as join13, normalize as normalize2, relative as relative3 } from "node:path";
10186
+ import { basename as basename3, dirname as dirname7, join as join13, normalize as normalize2, relative as relative3 } from "node:path";
10187
10187
  import { setTimeout as delay3 } from "node:timers/promises";
10188
10188
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10189
10189
  import { promisify as promisify5 } from "node:util";
@@ -25052,6 +25052,9 @@ function cacheWrittenAt(projectPath) {
25052
25052
  return null;
25053
25053
  }
25054
25054
  }
25055
+ function cachedAtMissingPaths(cached, projectPath) {
25056
+ return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync2(join4(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
25057
+ }
25055
25058
  function staleAgainst(cached, projectPath) {
25056
25059
  return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
25057
25060
  }
@@ -27710,20 +27713,61 @@ async function handleLSPTool(client, toolName, args) {
27710
27713
  }
27711
27714
 
27712
27715
  // src/process-children.ts
27713
- import { execFile as execFile3 } from "node:child_process";
27716
+ import { execFile as execFile3, execFileSync, spawnSync } from "node:child_process";
27714
27717
  import { basename } from "node:path";
27715
27718
  import { promisify as promisify3 } from "node:util";
27716
27719
  var run3 = promisify3(execFile3);
27720
+ function startTimesOf(pids) {
27721
+ const began = new Map;
27722
+ if (pids.length === 0) {
27723
+ return began;
27724
+ }
27725
+ try {
27726
+ if (process.platform === "win32") {
27727
+ const filter = pids.map((pid) => `ProcessId=${pid}`).join(" OR ");
27728
+ const said = execFileSync("powershell", [
27729
+ "-NoProfile",
27730
+ "-NonInteractive",
27731
+ "-Command",
27732
+ `Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`
27733
+ ], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, windowsHide: true });
27734
+ for (const line of said.split(`
27735
+ `)) {
27736
+ const found = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
27737
+ if (found?.[1] !== undefined && found[2] !== undefined) {
27738
+ began.set(Number(found[1]), Number(found[2]));
27739
+ }
27740
+ }
27741
+ return began;
27742
+ }
27743
+ const said = spawnSync("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
27744
+ encoding: "utf8",
27745
+ timeout: ASK_TIMEOUT_MS,
27746
+ env: { ...process.env, LC_ALL: "C" }
27747
+ }).stdout;
27748
+ for (const line of said.split(`
27749
+ `)) {
27750
+ const found = /^\s*(\d+)\s+(.+)$/.exec(line);
27751
+ const at = Date.parse(found?.[2]?.trim() ?? "");
27752
+ if (found?.[1] !== undefined && !Number.isNaN(at)) {
27753
+ began.set(Number(found[1]), at);
27754
+ }
27755
+ }
27756
+ } catch {}
27757
+ return began;
27758
+ }
27717
27759
  var ASK_TIMEOUT_MS = 15000;
27718
27760
  async function processTree() {
27719
27761
  try {
27720
- const printed = process.platform === "win32" ? await askWindows() : await askPosix();
27721
- return parseProcessTable(printed);
27762
+ if (process.platform === "win32") {
27763
+ return parseProcessTable(await askWindows(), true);
27764
+ }
27765
+ return parseProcessTable(await askPosix());
27722
27766
  } catch {
27723
27767
  return;
27724
27768
  }
27725
27769
  }
27726
- function parseProcessTable(printed) {
27770
+ function parseProcessTable(printed, started = false) {
27727
27771
  const tree = new Map;
27728
27772
  for (const line of printed.split(/\r?\n/)) {
27729
27773
  const fields = /^\s*(\d+)\s+(\d+)(?:\s+(.*))?$/.exec(line);
@@ -27732,16 +27776,38 @@ function parseProcessTable(printed) {
27732
27776
  }
27733
27777
  const pid = Number(fields[1]);
27734
27778
  const parent = Number(fields[2]);
27735
- if (pid > 0) {
27736
- tree.set(pid, { parent, command: (fields[3] ?? "").trim() });
27779
+ const rest = (fields[3] ?? "").trim();
27780
+ if (pid <= 0) {
27781
+ continue;
27782
+ }
27783
+ if (!started) {
27784
+ tree.set(pid, { parent, command: rest });
27785
+ continue;
27737
27786
  }
27787
+ const timed = /^(\d+)(?:\s+(.*))?$/.exec(rest);
27788
+ if (timed === null) {
27789
+ continue;
27790
+ }
27791
+ const began = Number(timed[1]);
27792
+ tree.set(pid, { parent, command: (timed[2] ?? "").trim(), ...began > 0 ? { startedAt: began } : {} });
27738
27793
  }
27739
27794
  return tree;
27740
27795
  }
27796
+ function linked(tree, child, parent) {
27797
+ const from = tree.get(child);
27798
+ const to = tree.get(parent);
27799
+ if (from === undefined || from.parent !== parent) {
27800
+ return false;
27801
+ }
27802
+ if (from.startedAt === undefined || to?.startedAt === undefined) {
27803
+ return true;
27804
+ }
27805
+ return from.startedAt >= to.startedAt;
27806
+ }
27741
27807
  function childrenIn(tree, pid) {
27742
27808
  const children = [];
27743
- for (const [child, listed] of tree) {
27744
- if (listed.parent === pid) {
27809
+ for (const child of tree.keys()) {
27810
+ if (linked(tree, child, pid)) {
27745
27811
  children.push(child);
27746
27812
  }
27747
27813
  }
@@ -27769,13 +27835,15 @@ function descendantsIn(tree, pid) {
27769
27835
  function ancestorsIn(tree, pid, root) {
27770
27836
  const chain = [];
27771
27837
  const seen = new Set([pid]);
27838
+ let child = pid;
27772
27839
  let parent = tree.get(pid)?.parent;
27773
- while (parent !== undefined && !seen.has(parent)) {
27840
+ while (parent !== undefined && !seen.has(parent) && linked(tree, child, parent)) {
27774
27841
  chain.push(parent);
27775
27842
  if (parent === root) {
27776
27843
  break;
27777
27844
  }
27778
27845
  seen.add(parent);
27846
+ child = parent;
27779
27847
  parent = tree.get(parent)?.parent;
27780
27848
  }
27781
27849
  return chain;
@@ -27831,7 +27899,7 @@ async function askWindows() {
27831
27899
  "-NoProfile",
27832
27900
  "-NonInteractive",
27833
27901
  "-Command",
27834
- 'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId) $($_.CommandLine)" }'
27902
+ 'Get-CimInstance Win32_Process | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }'
27835
27903
  ], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
27836
27904
  return stdout;
27837
27905
  }
@@ -28289,7 +28357,7 @@ function restartSettled(projectPath) {
28289
28357
  }
28290
28358
 
28291
28359
  // src/run-record.ts
28292
- import { execFileSync } from "node:child_process";
28360
+ import { execFileSync as execFileSync2 } from "node:child_process";
28293
28361
  import { mkdirSync as mkdirSync5, openSync, readdirSync as readdirSync5, readFileSync as readFileSync11, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync6 } from "node:fs";
28294
28362
  import { basename as basename2, join as join11 } from "node:path";
28295
28363
 
@@ -28297,7 +28365,7 @@ import { basename as basename2, join as join11 } from "node:path";
28297
28365
  import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync3, unlinkSync } from "node:fs";
28298
28366
  import { createConnection as createConnection3 } from "node:net";
28299
28367
  import { tmpdir as tmpdir3 } from "node:os";
28300
- import { join as join10, resolve as resolve4 } from "node:path";
28368
+ import { dirname as dirname5, join as join10, resolve as resolve4 } from "node:path";
28301
28369
  import { setTimeout as delay2 } from "node:timers/promises";
28302
28370
 
28303
28371
  // src/tool-args.ts
@@ -28471,11 +28539,35 @@ function announcedAndGone(projectPath) {
28471
28539
  const wanted = resolve4(projectPath);
28472
28540
  return gone.filter((one) => one.noticedAt >= since && resolve4(one.project) === wanted);
28473
28541
  }
28542
+ var STARTED_AFTER_ANNOUNCING_MS = 2000;
28543
+ var CONFIRMED_FOR_MS = 60000;
28544
+ var JUDGED_AFTER_MS = 60000;
28545
+ var verdicts = new Map;
28546
+ function strangersAmong(candidates, now, startTimes = startTimesOf) {
28547
+ const toJudge = candidates.filter((one) => {
28548
+ if (now - one.writtenAt < JUDGED_AFTER_MS) {
28549
+ return false;
28550
+ }
28551
+ const known = verdicts.get(one.file);
28552
+ return known === undefined || known.writtenAt !== one.writtenAt || !known.stranger && now - known.judgedAt > CONFIRMED_FOR_MS;
28553
+ });
28554
+ if (toJudge.length > 0) {
28555
+ const began = startTimes(toJudge.map((one) => one.pid));
28556
+ for (const one of toJudge) {
28557
+ const at = began.get(one.pid);
28558
+ const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
28559
+ verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
28560
+ }
28561
+ }
28562
+ return new Set(candidates.filter((one) => verdicts.get(one.file)?.stranger === true).map((one) => one.file));
28563
+ }
28474
28564
  function announcedIn(directory) {
28475
28565
  if (!existsSync7(directory)) {
28476
28566
  return [];
28477
28567
  }
28478
28568
  const found = [];
28569
+ const candidates = [];
28570
+ const entries = [];
28479
28571
  for (const entry of readdirSync4(directory)) {
28480
28572
  const report = ERROR_REPORT_PATTERN.exec(entry);
28481
28573
  if (report) {
@@ -28489,20 +28581,60 @@ function announcedIn(directory) {
28489
28581
  const file = join10(directory, entry);
28490
28582
  const pid = Number.parseInt(match[1] ?? "", 10);
28491
28583
  const alive = processAlive(pid);
28492
- const announced = alive ? parseAnnouncement(file, pid) : { kind: "rubbish" };
28584
+ entries.push({ file, pid, alive });
28585
+ if (alive) {
28586
+ try {
28587
+ candidates.push({ file, pid, writtenAt: statSync3(file).mtimeMs });
28588
+ } catch {}
28589
+ }
28590
+ }
28591
+ const strangers = strangersAmong(candidates, Date.now());
28592
+ for (const { file, pid, alive } of entries) {
28593
+ const announced = alive && !strangers.has(file) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
28493
28594
  if (announced.kind === "rubbish") {
28494
- if (!alive) {
28595
+ if (!alive || strangers.has(file)) {
28495
28596
  wentAway(file, pid);
28496
28597
  }
28497
28598
  try {
28498
28599
  unlinkSync(file);
28499
28600
  } catch {}
28601
+ verdicts.delete(file);
28500
28602
  continue;
28501
28603
  }
28502
28604
  found.push(announced);
28503
28605
  }
28606
+ const held = new Set(entries.map((one) => one.file));
28607
+ const here = join10(directory, ".");
28608
+ for (const file of verdicts.keys()) {
28609
+ if (dirname5(file) === here && !held.has(file)) {
28610
+ verdicts.delete(file);
28611
+ }
28612
+ }
28504
28613
  return found;
28505
28614
  }
28615
+ var ENDED_WITHIN_MS = 3000;
28616
+ async function announcementEnded(pid, directories = runtimeDirectories()) {
28617
+ const files = directories.map((directory) => join10(directory, `runtime-${pid}.json`)).filter((file) => existsSync7(file));
28618
+ const first = files[0];
28619
+ if (first === undefined) {
28620
+ return;
28621
+ }
28622
+ const until = Date.now() + ENDED_WITHIN_MS;
28623
+ while (processAlive(pid)) {
28624
+ const left = until - Date.now();
28625
+ if (left <= 0) {
28626
+ return;
28627
+ }
28628
+ await delay2(Math.min(LOOK_EVERY_MS, left));
28629
+ }
28630
+ wentAway(first, pid);
28631
+ for (const file of files) {
28632
+ try {
28633
+ unlinkSync(file);
28634
+ } catch {}
28635
+ verdicts.delete(file);
28636
+ }
28637
+ }
28506
28638
  function sweepErrorReport(path, pid, now = Date.now()) {
28507
28639
  if (processAlive(pid)) {
28508
28640
  return;
@@ -28811,7 +28943,7 @@ function runningAs(pid) {
28811
28943
  const raw = readFileSync11(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
28812
28944
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
28813
28945
  }
28814
- const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
28946
+ const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
28815
28947
  encoding: "utf8",
28816
28948
  timeout: 15000
28817
28949
  }).trim();
@@ -28823,20 +28955,14 @@ function runningAs(pid) {
28823
28955
  function listeningPid(port) {
28824
28956
  try {
28825
28957
  if (process.platform === "win32") {
28826
- const said = execFileSync("powershell", [
28827
- "-NoProfile",
28828
- "-NonInteractive",
28829
- "-Command",
28830
- `(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
28831
- ], { encoding: "utf8", timeout: 15000 }).trim();
28832
- return /^\d+$/.test(said) ? Number(said) : null;
28958
+ return listeningPidInNetstat(execFileSync2("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 }), port);
28833
28959
  }
28834
28960
  for (const [command, args, pattern] of [
28835
28961
  ["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
28836
28962
  ["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
28837
28963
  ]) {
28838
28964
  try {
28839
- const said = execFileSync(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
28965
+ const said = execFileSync2(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
28840
28966
  const found = pattern.exec(said);
28841
28967
  if (found?.[1] !== undefined) {
28842
28968
  return Number(found[1]);
@@ -28848,9 +28974,22 @@ function listeningPid(port) {
28848
28974
  return null;
28849
28975
  }
28850
28976
  }
28977
+ function listeningPidInNetstat(printed, port) {
28978
+ for (const line of printed.split(/\r?\n/)) {
28979
+ const fields = /^\s*TCP\s+(\S+)\s+\S+\s+LISTENING\s+(\d+)\s*$/i.exec(line);
28980
+ if (fields === null) {
28981
+ continue;
28982
+ }
28983
+ const local = fields[1] ?? "";
28984
+ if (Number(local.slice(local.lastIndexOf(":") + 1)) === port) {
28985
+ return Number(fields[2]);
28986
+ }
28987
+ }
28988
+ return null;
28989
+ }
28851
28990
  function windowsCommandLine(pid) {
28852
28991
  try {
28853
- const answer = execFileSync("powershell", [
28992
+ const answer = execFileSync2("powershell", [
28854
28993
  "-NoProfile",
28855
28994
  "-NonInteractive",
28856
28995
  "-Command",
@@ -28868,7 +29007,7 @@ function startedAt(pid, windowsAnswer) {
28868
29007
  return Number.isNaN(when) ? null : when;
28869
29008
  }
28870
29009
  try {
28871
- const said = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
29010
+ const said = execFileSync2("ps", ["-p", String(pid), "-o", "lstart="], {
28872
29011
  encoding: "utf8",
28873
29012
  timeout: 15000
28874
29013
  }).trim();
@@ -28880,7 +29019,7 @@ function startedAt(pid, windowsAnswer) {
28880
29019
  }
28881
29020
  function windowsImage(pid) {
28882
29021
  try {
28883
- const csv = execFileSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
29022
+ const csv = execFileSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
28884
29023
  encoding: "utf8",
28885
29024
  timeout: 15000,
28886
29025
  windowsHide: true
@@ -28966,7 +29105,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
28966
29105
 
28967
29106
  // src/setup.ts
28968
29107
  import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync12, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
28969
- import { dirname as dirname5, join as join12 } from "node:path";
29108
+ import { dirname as dirname6, join as join12 } from "node:path";
28970
29109
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
28971
29110
  var RUNTIME_AUTOLOAD = {
28972
29111
  name: "GdharnessRuntime",
@@ -29687,7 +29826,7 @@ var TOOL_SPECS = [
29687
29826
  },
29688
29827
  {
29689
29828
  name: "editor_rescan",
29690
- description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. 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. 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. Needs the editor connected.",
29829
+ description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. 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. 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.",
29691
29830
  parameters: {
29692
29831
  projectPath: PROJECT_PATH,
29693
29832
  timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." },
@@ -29723,12 +29862,20 @@ var TOOL_SPECS = [
29723
29862
  },
29724
29863
  instantiableOnly: { type: "boolean", description: "query: leave out abstract classes. Default false." },
29725
29864
  className: { type: "string", description: "info, inheritance: the class." },
29726
- includeInherited: { type: "boolean", description: "info: include inherited members. Default false." }
29865
+ includeInherited: { type: "boolean", description: "info: include inherited members. Default false." },
29866
+ member: {
29867
+ type: "string",
29868
+ ops: ["info"],
29869
+ description: 'info: one member by name, a method, property, signal, enum or constant, with its signature and which class declares it, looked for on the class and every ancestor. The whole of Control is seventy thousand characters; the question "does this engine have it, and what does it take" is this. A name nothing declares is refused with the members whose names contain it.'
29870
+ }
29727
29871
  },
29728
29872
  requires: ["projectPath"],
29729
29873
  operations: {
29730
29874
  query: { summary: "classes matching a filter or category", requires: [] },
29731
- info: { summary: "methods, properties, signals and enums of one class", requires: ["className"] },
29875
+ info: {
29876
+ summary: "methods, properties, signals and enums of one class, or one member of it by name",
29877
+ requires: ["className"]
29878
+ },
29732
29879
  inheritance: { summary: "ancestors and descendants of one class", requires: ["className"] }
29733
29880
  },
29734
29881
  defaultOperation: "query"
@@ -29747,13 +29894,19 @@ var TOOL_SPECS = [
29747
29894
  property: {
29748
29895
  type: "string",
29749
29896
  ops: ["property", "find"],
29750
- description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step that is not there is named, and says what was there instead.'
29897
+ description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A step that is not there is named, and says what was there instead.'
29751
29898
  },
29752
29899
  depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
29753
29900
  includeProperties: {
29754
29901
  type: "boolean",
29755
29902
  ops: ["tree"],
29756
- description: "tree: include each node's properties. Default false."
29903
+ description: "tree: include every stored property of each node. Default false, and one row of a form answered seventy-six thousand characters with it; properties names the few that are wanted."
29904
+ },
29905
+ properties: {
29906
+ type: "array",
29907
+ items: { type: "string" },
29908
+ ops: ["tree"],
29909
+ description: "tree: the properties to read off each node, by name, so the text of every label and button under a panel is one call: a node that has not got one leaves it out. Colons read through what a node holds, as property does for find."
29757
29910
  },
29758
29911
  className: {
29759
29912
  type: "string",
@@ -29822,7 +29975,7 @@ var TOOL_SPECS = [
29822
29975
  property: {
29823
29976
  type: "string",
29824
29977
  ops: ["set"],
29825
- description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
29978
+ description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
29826
29979
  },
29827
29980
  value: {
29828
29981
  ops: ["set"],
@@ -29832,7 +29985,7 @@ var TOOL_SPECS = [
29832
29985
  method: {
29833
29986
  type: "string",
29834
29987
  ops: ["call"],
29835
- description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, and a list or a map is stepped into by index or key, "_game:run:roster:0:retire".'
29988
+ description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a method taking no arguments returned, "get_viewport():gui_get_focus_owner".'
29836
29989
  },
29837
29990
  args: {
29838
29991
  type: "array",
@@ -29892,7 +30045,7 @@ var TOOL_SPECS = [
29892
30045
  pressed: {
29893
30046
  type: "boolean",
29894
30047
  ops: ["action", "key", "mouse_click"],
29895
- description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false."
30048
+ description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false; a wheel button down is a whole step, since a wheel is never held."
29896
30049
  },
29897
30050
  strength: { type: "number", ops: ["action"], description: "action: 0 to 1. Default 1." },
29898
30051
  keycode: {
@@ -29942,7 +30095,7 @@ var TOOL_SPECS = [
29942
30095
  relativeX: {
29943
30096
  type: "number",
29944
30097
  ops: ["mouse_motion"],
29945
- description: "mouse_motion: the movement the event carries. Left out, it is the distance from where the pointer last was, which is what a control that drags reads; give it to send a motion the position does not show."
30098
+ description: "mouse_motion: the movement the event carries. Left out, it is the distance from where the last injected pointer event put the pointer, and none for the first; that is what a control that drags reads. Give it to send a motion the position does not show."
29946
30099
  },
29947
30100
  relativeY: {
29948
30101
  type: "number",
@@ -29962,7 +30115,7 @@ var TOOL_SPECS = [
29962
30115
  },
29963
30116
  action: { summary: "press an action, or hold it", requires: ["action"] },
29964
30117
  key: {
29965
- summary: "press a key, or hold it. Not for a menu a click has opened: an OptionButton or PopupMenu pops up as a window of its own that a key sent to the game never reaches, so an arrow moves nothing in it and Enter closes it with nothing chosen. choose is what selects there",
30118
+ summary: "press a key, or hold it. Not for a menu a click has opened: an OptionButton or PopupMenu pops up as a window of its own that a key sent to the game never reaches, so an arrow moves nothing in it and Enter closes it with nothing chosen. choose is what selects there. The limit is that menu's own navigation, not windows in general: an embedded dialog whose owner reads keys in _input gets them, and a binding catcher up in one takes the key first time",
29966
30119
  requires: ["keycode"]
29967
30120
  },
29968
30121
  text: {
@@ -29970,11 +30123,11 @@ var TOOL_SPECS = [
29970
30123
  requires: ["text"]
29971
30124
  },
29972
30125
  mouse_click: {
29973
- summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false",
30126
+ summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false. A wheel button is a whole step, press and release together as a mouse sends one, and the answer says released: a lone wheel press holds the viewport on the control that took it, so every click after it lands there",
29974
30127
  requires: ["x", "y"]
29975
30128
  },
29976
30129
  mouse_motion: {
29977
- summary: "move the mouse to a position, carrying the distance from where it last was unless relativeX and relativeY say otherwise, and the buttons held, so a control that drags moves under a run of these between a held mouse_click and its release",
30130
+ summary: "move the mouse to a position, carrying the distance from where the last injected event put it unless relativeX and relativeY say otherwise, and the buttons held, so a control that drags moves under a run of these between a held mouse_click and its release",
29978
30131
  requires: ["x", "y"]
29979
30132
  }
29980
30133
  }
@@ -29992,12 +30145,21 @@ var TOOL_SPECS = [
29992
30145
  },
29993
30146
  nodePath: { type: "string", ops: ["signal", "until"], description: "signal, until: the node." },
29994
30147
  signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
29995
- property: { type: "string", ops: ["until"], description: "until: the property name." },
30148
+ property: {
30149
+ type: "string",
30150
+ ops: ["until"],
30151
+ description: 'until: the property name, or a colon path through what the node holds, "_game:run:day", read the way runtime_inspect property reads one, a step written as a call included. Walked again on every frame, so a holder the game replaces while the wait is on is followed.'
30152
+ },
29996
30153
  value: { ops: ["until"], description: "until: the value to wait for, fitted to the property's type." },
29997
30154
  says: {
29998
30155
  type: "string",
29999
30156
  ops: ["until"],
30000
- description: "until: wait for these words to appear anywhere under nodePath instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, hidden nodes included; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property."
30157
+ description: "until: wait for these words to appear anywhere under nodePath instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property."
30158
+ },
30159
+ includeHidden: {
30160
+ type: "boolean",
30161
+ ops: ["until"],
30162
+ description: "until, with says: count words on a hidden node too. Default false, since a wait for words is a wait for them to be shown: a button that exists hidden through a whole animation carries its words the whole time and satisfied the wait at once, and every wait on that screen fell back to counting frames."
30001
30163
  },
30002
30164
  timeoutMs: {
30003
30165
  type: "number",
@@ -30112,7 +30274,7 @@ function buildToolDefinitions() {
30112
30274
  var UPDATE_NOTICE_EVERY = 500;
30113
30275
  var FEEDBACK_NOTICE_EVERY = 250;
30114
30276
  var run5 = promisify5(execFile5);
30115
- var __dirname2 = dirname6(fileURLToPath2(import.meta.url));
30277
+ var __dirname2 = dirname7(fileURLToPath2(import.meta.url));
30116
30278
  var EDITOR_RESTART_TIMEOUT_MS = 90000;
30117
30279
  var HOLD_PING_MS = 2000;
30118
30280
  var SLOWEST_FRAME_RATE = 20;
@@ -30151,13 +30313,17 @@ function stillRunning(run) {
30151
30313
  }
30152
30314
  return true;
30153
30315
  }
30154
- function runIsUp(run, editorSays) {
30316
+ var PLAY_STARTS_WITHIN_MS = 30000;
30317
+ function runIsUp(run, editorSays, now = Date.now()) {
30155
30318
  if (run?.throughEditor !== true) {
30156
30319
  return stillRunning(run);
30157
30320
  }
30158
30321
  if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
30159
30322
  return false;
30160
30323
  }
30324
+ if (editorSays === false && run.seenPlaying !== true && now - run.startedAt < PLAY_STARTS_WITHIN_MS) {
30325
+ return true;
30326
+ }
30161
30327
  return editorSays ?? stillRunning(run);
30162
30328
  }
30163
30329
  function endedPreviousRun(ended) {
@@ -30232,10 +30398,11 @@ function runtimeVerdict(endpoint, after) {
30232
30398
  heldAt: after.heldAt
30233
30399
  };
30234
30400
  }
30401
+ const state = after.playingSeen === false ? "the editor has not yet reported the game as playing, which a restarted editor does not until its scan is over" : "the game is still running";
30235
30402
  return {
30236
30403
  listening: false,
30237
30404
  mayYetAnnounce: after.running,
30238
- note: after.running ? `nothing announced itself within ${after.budgetMs}ms and the game is still running, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}` : `nothing announced itself within ${after.budgetMs}ms and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
30405
+ note: after.running ? `nothing announced itself within ${after.budgetMs}ms and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}` : `nothing announced itself within ${after.budgetMs}ms and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
30239
30406
  heldAt: null
30240
30407
  };
30241
30408
  }
@@ -30966,7 +31133,8 @@ class GodotServer {
30966
31133
  ...whichGame(args),
30967
31134
  root: readNonEmptyString(args, "nodePath") ?? "/root",
30968
31135
  depth: readPositiveNumber(args, "depth") ?? 3,
30969
- include_properties: readBoolean(args, "includeProperties") ?? false
31136
+ include_properties: readBoolean(args, "includeProperties") ?? false,
31137
+ properties: readArray(args, "properties") ?? []
30970
31138
  });
30971
31139
  case "find":
30972
31140
  return await this.handleFindRuntimeNodes(args);
@@ -31368,11 +31536,11 @@ class GodotServer {
31368
31536
  if (!engine.ok) {
31369
31537
  return engine.response;
31370
31538
  }
31371
- const folder = dirname6(output.absolutePath);
31539
+ const folder = dirname7(output.absolutePath);
31372
31540
  try {
31373
31541
  mkdirSync7(folder, { recursive: true });
31374
31542
  } catch (error) {
31375
- return this.createErrorResponse(`Cannot create ${dirname6(output.relativePath)} to export into: ${errorMessage(error)}`);
31543
+ return this.createErrorResponse(`Cannot create ${dirname7(output.relativePath)} to export into: ${errorMessage(error)}`);
31376
31544
  }
31377
31545
  const debug = readBoolean(args, "debug") ?? false;
31378
31546
  const exportLogs = mkdtempSync2(join13(tmpdir4(), "gdharness-export-"));
@@ -31826,11 +31994,14 @@ class GodotServer {
31826
31994
  return null;
31827
31995
  }
31828
31996
  try {
31829
- const answer = await Promise.race([
31830
- this.godotBridge.invokeTool("playing_status", {}),
31831
- delay3(PLAYING_STATUS_MS).then(() => null)
31832
- ]);
31997
+ const askedAt = Date.now();
31998
+ const asked = this.godotBridge.invokeTool("playing_status", {});
31999
+ const answer = await Promise.race([asked, delay3(PLAYING_STATUS_MS).then(() => null)]);
31833
32000
  if (answer === null) {
32001
+ this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
32002
+ asked.then(() => {
32003
+ this.logDebug(`playing_status answered after ${Date.now() - askedAt}ms`);
32004
+ }).catch(() => {});
31834
32005
  return null;
31835
32006
  }
31836
32007
  return {
@@ -32150,6 +32321,9 @@ class GodotServer {
32150
32321
  let asking = null;
32151
32322
  let editorAskedAt = 0;
32152
32323
  let editorSaysGoing = true;
32324
+ let editorAsks = 0;
32325
+ const waitBegan = Date.now();
32326
+ const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
32153
32327
  const endpoint = await announcedSince(projectPath, before, {
32154
32328
  budgetMs,
32155
32329
  accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
@@ -32163,6 +32337,7 @@ class GodotServer {
32163
32337
  }
32164
32338
  if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
32165
32339
  editorAskedAt = Date.now();
32340
+ editorAsks += 1;
32166
32341
  asking = this.runStillGoing(run).then((going) => {
32167
32342
  editorSaysGoing = going;
32168
32343
  }).finally(() => {
@@ -32172,6 +32347,7 @@ class GodotServer {
32172
32347
  return !editorSaysGoing;
32173
32348
  }
32174
32349
  });
32350
+ this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, editor asked ${editorAsks} times, last said ${lastSaid()}`);
32175
32351
  const going = this.currentRun();
32176
32352
  if (endpoint !== null && going !== null && going.announcedPid === undefined) {
32177
32353
  going.announcedPid = endpoint.pid;
@@ -32181,7 +32357,8 @@ class GodotServer {
32181
32357
  budgetMs,
32182
32358
  heldAt: this.dapClient?.whereItStopped() ?? null,
32183
32359
  running: endpoint !== null || going !== null && await this.runStillGoing(going),
32184
- withArgs
32360
+ withArgs,
32361
+ ...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
32185
32362
  });
32186
32363
  }
32187
32364
  async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
@@ -32211,6 +32388,7 @@ class GodotServer {
32211
32388
  const startedAt = Date.now();
32212
32389
  const transcript = openTranscript(startedAt);
32213
32390
  closeSync(transcript.fd);
32391
+ const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
32214
32392
  const played = {
32215
32393
  process: null,
32216
32394
  pid: null,
@@ -32222,7 +32400,8 @@ class GodotServer {
32222
32400
  exitCode: null,
32223
32401
  throughEditor: true,
32224
32402
  brokeOn: null,
32225
- announcedBefore: alreadyPlaying
32403
+ announcedBefore: alreadyPlaying,
32404
+ seenPlaying: readBoolean(playAnswer, "playing") === true
32226
32405
  };
32227
32406
  this.activeProcess = played;
32228
32407
  writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
@@ -32232,7 +32411,7 @@ class GodotServer {
32232
32411
  through: "editor",
32233
32412
  scene: scene === null ? "the main scene" : `res://${scene}`,
32234
32413
  refreshedClasses,
32235
- debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
32414
+ debugPort: readNumber(playAnswer, "debugPort"),
32236
32415
  endedPreviousRun: endedPreviousRun(ended),
32237
32416
  breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
32238
32417
  breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
@@ -32758,7 +32937,11 @@ class GodotServer {
32758
32937
  if (announced !== undefined && editorSays === undefined) {
32759
32938
  return stillRunning(run);
32760
32939
  }
32761
- return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
32940
+ const said = editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays;
32941
+ if (said === true) {
32942
+ run.seenPlaying = true;
32943
+ }
32944
+ return runIsUp(run, said);
32762
32945
  }
32763
32946
  async handleGetDebugOutput(args, waitedMs) {
32764
32947
  await this.pickUpWhatTheEditorIsPlaying();
@@ -32841,11 +33024,19 @@ class GodotServer {
32841
33024
  this.drainEditorOutput(stopped);
32842
33025
  this.drainTranscript(stopped);
32843
33026
  const wasRunning = await this.runStillGoing(stopped);
32844
- const endedPid = stopped.pid ?? this.announcedPidOf(stopped) ?? null;
33027
+ const announcedPid = this.announcedPidOf(stopped) ?? null;
33028
+ const endedPid = stopped.pid ?? announcedPid;
32845
33029
  const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
32846
33030
  this.logDebug("Stopping the running game");
32847
33031
  await this.endActiveGame("editor_run stop");
32848
33032
  const ended = children === null ? null : endChildrenAmong(children);
33033
+ if (wasRunning) {
33034
+ for (const pid of new Set([endedPid, announcedPid])) {
33035
+ if (pid !== null) {
33036
+ await announcementEnded(pid);
33037
+ }
33038
+ }
33039
+ }
32849
33040
  return this.jsonTextResponse({
32850
33041
  stopped: true,
32851
33042
  through: stopped.throughEditor ? "editor" : "gdharness",
@@ -32941,6 +33132,7 @@ class GodotServer {
32941
33132
  const writtenBefore = projectPath === "" ? null : cacheWrittenAt(projectPath);
32942
33133
  const blind = await this.classesTheEditorCannotSee(args);
32943
33134
  const atRisk = before === null ? [] : blind.unseen.filter((one) => before.has(one.className));
33135
+ const ghosts = blind.stillHeld ?? [];
32944
33136
  const first = await this.handleViaBridge("rescan_filesystem", args);
32945
33137
  if (first.isError) {
32946
33138
  return first;
@@ -32952,7 +33144,7 @@ class GodotServer {
32952
33144
  const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
32953
33145
  busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
32954
33146
  }
32955
- if (!busy && projectPath !== "" && (atRisk.length > 0 || blind.unchecked !== undefined)) {
33147
+ if (!busy && projectPath !== "" && (atRisk.length > 0 || ghosts.length > 0 || blind.unchecked !== undefined)) {
32956
33148
  const until = Date.now() + CACHE_WRITE_MS;
32957
33149
  while (Date.now() < until && cacheWrittenAt(projectPath) === writtenBefore) {
32958
33150
  await new Promise((settle) => setTimeout(settle, 100));
@@ -32960,7 +33152,11 @@ class GodotServer {
32960
33152
  }
32961
33153
  const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
32962
33154
  const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
32963
- const restored = lost.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
33155
+ const atMissingPaths = after === null ? [] : cachedAtMissingPaths(after, projectPath);
33156
+ const restored = lost.length > 0 || atMissingPaths.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
33157
+ const rebuilt = atMissingPaths.length > 0 ? cachedClasses(projectPath) : null;
33158
+ const dropped = rebuilt === null ? [] : atMissingPaths.filter((name) => !cachedAtMissingPaths(rebuilt, projectPath).includes(name));
33159
+ const stillAtMissingPaths = atMissingPaths.filter((name) => !dropped.includes(name));
32964
33160
  const reloading = readNonEmptyString(args, "reloadScript");
32965
33161
  let reloaded = {};
32966
33162
  if (reloading !== undefined) {
@@ -32976,8 +33172,23 @@ class GodotServer {
32976
33172
  const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
32977
33173
  const unseen = checked.unseen;
32978
33174
  const stillGone = lost.filter((name) => !restored.includes(name));
33175
+ const notes = [];
33176
+ if (busy) {
33177
+ notes.push("The editor was still scanning or importing when the wait ran out, so new files may not be visible yet.");
33178
+ } else if (stillGone.length > 0) {
33179
+ notes.push("The scan wrote the class cache from the list this editor is holding, that list is shorter than the file was, and rebuilding the cache from the files did not bring these back. Every engine that reads the cache next, including a test run, will report them as unknown identifiers. Restart the editor with editor_launch restart before scanning again.");
33180
+ } else if (lost.length > 0) {
33181
+ notes.push("The scan wrote the class cache from the list this editor is holding, which is shorter than the file was, so the cache was rebuilt from the files and these classes are back in it. Nothing was lost, but this editor is still holding the short list: do not rescan again until it has been restarted with editor_launch restart, because it will write the same list over the file each time.");
33182
+ } else if (unseen.length > 0) {
33183
+ notes.push("The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Rescan again on its own, which is the measured cure and needs no change to the declaring script, or restart the editor with editor_launch restart.");
33184
+ }
33185
+ if (stillAtMissingPaths.length > 0) {
33186
+ notes.push(`The scan wrote the class cache with ${stillAtMissingPaths.join(", ")} at a path that is not on disk, and rebuilding the cache from the files did not take it out. The next engine to read the cache fails on "Could not parse global class" in whichever correct script shares the bare name. Restart the editor with editor_launch restart, then run project_import refresh_classes.`);
33187
+ } else if (dropped.length > 0) {
33188
+ notes.push(`The scan wrote the class cache from the list this editor is holding, and that list still has ${dropped.join(", ")} for a script that is gone, so the cache was rebuilt from the files without it. The editor goes on holding it and writes it back on every scan until it is restarted with editor_launch restart; until then a scan is followed by this same rebuild.`);
33189
+ }
32979
33190
  return this.jsonTextResponse({
32980
- ok: !busy && unseen.length === 0 && stillGone.length === 0 && checked.unchecked === undefined,
33191
+ ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && checked.unchecked === undefined,
32981
33192
  stillWorking: busy,
32982
33193
  waitedMs: Date.now() - started,
32983
33194
  heldBeforeReload: reloaded.heldBefore,
@@ -32986,8 +33197,10 @@ class GodotServer {
32986
33197
  unseenByEditor: unseen.length > 0 ? unseen : undefined,
32987
33198
  cacheLost: lost.length > 0 ? lost : undefined,
32988
33199
  cacheRestored: restored.length > 0 ? restored : undefined,
33200
+ cacheDropped: dropped.length > 0 ? dropped : undefined,
33201
+ cacheAtMissingPaths: stillAtMissingPaths.length > 0 ? stillAtMissingPaths : undefined,
32989
33202
  classesUnchecked: checked.unchecked,
32990
- note: busy ? "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet." : stillGone.length > 0 ? "The scan wrote the class cache from the list this editor is holding, that list is shorter than the file was, and rebuilding the cache from the files did not bring these back. Every engine that reads the cache next, including a test run, will report them as unknown identifiers. Restart the editor with editor_launch restart before scanning again." : lost.length > 0 ? "The scan wrote the class cache from the list this editor is holding, which is shorter than the file was, so the cache was rebuilt from the files and these classes are back in it. Nothing was lost, but this editor is still holding the short list: do not rescan again until it has been restarted with editor_launch restart, because it will write the same list over the file each time." : unseen.length > 0 ? "The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Rescan again on its own, which is the measured cure and needs no change to the declaring script, or restart the editor with editor_launch restart." : undefined
33203
+ note: notes.length > 0 ? notes.join(" ") : undefined
32991
33204
  });
32992
33205
  }
32993
33206
  async rebuildCacheAfterLoss(projectPath, lost) {
@@ -33228,6 +33441,7 @@ class GodotServer {
33228
33441
  property: readString(args, "property") ?? "",
33229
33442
  value: args["value"],
33230
33443
  ...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
33444
+ ...readBoolean(args, "includeHidden") === undefined ? {} : { include_hidden: readBoolean(args, "includeHidden") },
33231
33445
  timeout_ms: timeoutMs
33232
33446
  }, patience);
33233
33447
  }