gdharness 1.1.60 → 1.1.61

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/cli.js CHANGED
@@ -9242,11 +9242,116 @@ function bodyCallsSuper(code, line, lambda) {
9242
9242
  }
9243
9243
  return false;
9244
9244
  }
9245
- function callSitesOf(source, name) {
9245
+ function parametersOf(text, from) {
9246
+ const open = text.indexOf("(", from);
9247
+ if (open === -1) {
9248
+ return [];
9249
+ }
9250
+ const names = [];
9251
+ let depth = 0;
9252
+ let start = open + 1;
9253
+ for (let at = open;at < text.length; at += 1) {
9254
+ const here = text[at];
9255
+ if (here === "(" || here === "[" || here === "{") {
9256
+ depth += 1;
9257
+ } else if (here === ")" || here === "]" || here === "}") {
9258
+ depth -= 1;
9259
+ }
9260
+ if (here === "," && depth === 1 || depth === 0) {
9261
+ const named = new RegExp(`^\\s*(${NAME})`, "u").exec(text.slice(start, at));
9262
+ if (named?.[1] !== undefined) {
9263
+ names.push(named[1]);
9264
+ }
9265
+ start = at + 1;
9266
+ if (depth === 0) {
9267
+ break;
9268
+ }
9269
+ }
9270
+ }
9271
+ return names;
9272
+ }
9273
+ function isLocalAt(lines, index, name) {
9274
+ const word = escapedForPattern(name);
9275
+ const declares = new RegExp(`(?<![\\p{L}\\p{N}_.])(?:var|const)\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
9276
+ const iterates = new RegExp(`^\\s*for\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
9277
+ const own = lines[index] ?? "";
9278
+ const headerOnOwnLine = FUNC_LINE.exec(own) ?? LAMBDA_HEADER.exec(own);
9279
+ if (headerOnOwnLine !== null && parametersOf(own, headerOnOwnLine.index).includes(name)) {
9280
+ return true;
9281
+ }
9282
+ if (FUNC_LINE.test(own)) {
9283
+ return false;
9284
+ }
9285
+ let depth = indentOf(own);
9286
+ for (let at = index - 1;at >= 0; at -= 1) {
9287
+ const text = lines[at] ?? "";
9288
+ if (text.trim() === "") {
9289
+ continue;
9290
+ }
9291
+ const indent = indentOf(text);
9292
+ if (indent > depth) {
9293
+ continue;
9294
+ }
9295
+ const named = FUNC_LINE.exec(text);
9296
+ if (indent < depth) {
9297
+ depth = indent;
9298
+ if (named !== null) {
9299
+ return parametersOf(text, named.index).includes(name);
9300
+ }
9301
+ const lambda = LAMBDA_HEADER.exec(text);
9302
+ if (lambda !== null && parametersOf(text, lambda.index).includes(name) || iterates.test(text)) {
9303
+ return true;
9304
+ }
9305
+ }
9306
+ if (named !== null || depth === 0) {
9307
+ return false;
9308
+ }
9309
+ if (declares.test(text)) {
9310
+ return true;
9311
+ }
9312
+ }
9313
+ return false;
9314
+ }
9315
+ function receiverBefore(code, offset) {
9316
+ let at = offset - 1;
9317
+ while (at >= 0 && /\s/.test(code[at] ?? "")) {
9318
+ at -= 1;
9319
+ }
9320
+ if (code[at] !== ".") {
9321
+ return null;
9322
+ }
9323
+ at -= 1;
9324
+ while (at >= 0 && /\s/.test(code[at] ?? "")) {
9325
+ at -= 1;
9326
+ }
9327
+ let start = at + 1;
9328
+ while (start > 0 && isIdentifierPart(code[start - 1])) {
9329
+ start -= 1;
9330
+ }
9331
+ return start <= at ? code.slice(start, at + 1) : null;
9332
+ }
9333
+ function bodyHolding(body, offset) {
9334
+ const inner = body.inner.find((one) => one.start <= offset && offset <= one.end);
9335
+ return inner === undefined ? body : bodyHolding(inner, offset);
9336
+ }
9337
+ function callSitesOf(source, name, otherClasses = new Set) {
9246
9338
  const regions = regionsOf(source);
9247
9339
  const code = codeOf(source, regions);
9340
+ const codeLines = code.split(`
9341
+ `);
9248
9342
  const lines = new Lines(source);
9249
- return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).map((one) => {
9343
+ const shape = shapeOf(source);
9344
+ return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).filter((one) => {
9345
+ if (one.afterDot) {
9346
+ const receiver = receiverBefore(code, one.offset);
9347
+ return receiver === null || !otherClasses.has(receiver);
9348
+ }
9349
+ const index = lines.positionOf(one.offset).line;
9350
+ if (isLocalAt(codeLines, index, name)) {
9351
+ return false;
9352
+ }
9353
+ return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind));
9354
+ }).map((one) => {
9250
9355
  const line = lines.positionOf(one.offset).line + 1;
9251
9356
  const after = code.slice(one.offset + name.length).trimStart();
9252
9357
  return { line, within: enclosingFunction(source, line, code), called: after.startsWith("(") };
@@ -9267,7 +9372,7 @@ function collectEnumValues(text, from, into) {
9267
9372
  function isIdentifier(name) {
9268
9373
  return new RegExp(`^${NAME}$`, "u").test(name) && !KEYWORDS.has(name);
9269
9374
  }
9270
- var IDENTIFIER_START, IDENTIFIER_PART, DECLARING, NAME = "[\\p{L}_][\\p{L}\\p{N}_]*", ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*", DECLARATION_LINE, CLASS_NAME_LINE, EXTENDS_LINE, EXTENDS_CLAUSE, FUNC_LINE, SUPER_CALL, LAMBDA_HEADER, KEYWORDS;
9375
+ var IDENTIFIER_START, IDENTIFIER_PART, DECLARING, NAME = "[\\p{L}_][\\p{L}\\p{N}_]*", ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*", DECLARATION_LINE, CLASS_NAME_LINE, EXTENDS_LINE, EXTENDS_CLAUSE, FUNC_LINE, SUPER_CALL, LAMBDA_HEADER, MEMBER_KINDS, KEYWORDS;
9271
9376
  var init_gdscript_source = __esm(() => {
9272
9377
  IDENTIFIER_START = /[\p{L}_]/u;
9273
9378
  IDENTIFIER_PART = /[\p{L}\p{N}_]/u;
@@ -9279,6 +9384,7 @@ var init_gdscript_source = __esm(() => {
9279
9384
  FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
9280
9385
  SUPER_CALL = /(?<![\p{L}\p{N}_.])super\s*[.(]/u;
9281
9386
  LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
9387
+ MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
9282
9388
  KEYWORDS = new Set([
9283
9389
  "and",
9284
9390
  "as",
@@ -18243,7 +18349,7 @@ var init_tool_definitions = __esm(() => {
18243
18349
  callersOf: {
18244
18350
  type: "string",
18245
18351
  ops: ["profile"],
18246
- description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too and a call made through a string is not."
18352
+ description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except one called through another class of the project by name when Class.name was asked, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed."
18247
18353
  },
18248
18354
  limit: {
18249
18355
  type: "integer",
@@ -18335,7 +18441,7 @@ var init_tool_definitions = __esm(() => {
18335
18441
  },
18336
18442
  {
18337
18443
  name: "editor_status",
18338
- description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
18444
+ description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
18339
18445
  parameters: {},
18340
18446
  requires: []
18341
18447
  },
@@ -40311,6 +40417,42 @@ var init_outside = __esm(() => {
40311
40417
  KEEPER_SCRIPT = fileURLToPath4(new URL("./keeper.js", import.meta.url));
40312
40418
  });
40313
40419
 
40420
+ // src/port-probe.ts
40421
+ import { connect as connect3 } from "node:net";
40422
+ function acceptsConnections(port, withinMs = 2000) {
40423
+ return new Promise((resolve) => {
40424
+ const socket = connect3({ host: "127.0.0.1", port });
40425
+ const settle = (answer) => {
40426
+ socket.destroy();
40427
+ resolve(answer);
40428
+ };
40429
+ socket.setTimeout(withinMs, () => {
40430
+ settle(null);
40431
+ });
40432
+ socket.once("connect", () => {
40433
+ settle(true);
40434
+ });
40435
+ socket.once("error", () => {
40436
+ settle(false);
40437
+ });
40438
+ });
40439
+ }
40440
+ function notListeningNote(servers, greetedAgoMs) {
40441
+ const down = [
40442
+ ...servers.lsp.listening === false ? [{ name: "language server", port: servers.lsp.port }] : [],
40443
+ ...servers.dap.listening === false ? [{ name: "debug adapter", port: servers.dap.port }] : []
40444
+ ];
40445
+ if (down.length === 0) {
40446
+ return;
40447
+ }
40448
+ const named = down.map((one) => `its ${one.name} on port ${one.port}`).join(" and ");
40449
+ const needs = down.map((one) => one.name === "debug adapter" ? "played runs and the debug tools need the debug adapter" : "diagnostics, symbols and renames need the language server").join(", and ");
40450
+ const starting = greetedAgoMs !== null && greetedAgoMs < STARTING_MS ? ` The editor greeted ${Math.round(greetedAgoMs / 1000)}s ago, so ${down.length === 1 ? "it" : "they"} may still be starting: ask again in a few seconds.` : "";
40451
+ return `The editor names ${named}, and nothing accepts connections there. Godot binds each once, as the editor starts, and reports a failure only in the editor's own log panel; ${needs}. editor_launch restart binds ${down.length === 1 ? "it" : "them"} again.${starting}`;
40452
+ }
40453
+ var STARTING_MS = 20000;
40454
+ var init_port_probe = () => {};
40455
+
40314
40456
  // src/process-time.ts
40315
40457
  import { execFile as execFile3 } from "node:child_process";
40316
40458
  import { promisify as promisify3 } from "node:util";
@@ -42430,15 +42572,31 @@ function placedLambda(row, sourceOf) {
42430
42572
  const within = source === null ? null : enclosingFunction(source, row.line);
42431
42573
  return within === null ? row : { ...row, within };
42432
42574
  }
42575
+ function classNamesOf(source) {
42576
+ const shape = shapeOf(source);
42577
+ const inner = (body) => body.inner.flatMap((one) => [...one.name === null ? [] : [one.name], ...inner(one)]);
42578
+ return [...shape.className === null ? [] : [shape.className.name], ...inner(shape.body)];
42579
+ }
42433
42580
  function callersOf(totals, name, scripts) {
42434
42581
  const listed = [...scripts];
42435
42582
  const sources = new Map(listed);
42436
42583
  const rows = rowsOf(totals, (script) => sources.get(script) ?? null);
42437
42584
  const bare = bareName(name);
42438
42585
  const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
42586
+ const asked = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : null;
42587
+ const otherClasses = new Set;
42588
+ if (asked !== null) {
42589
+ for (const [, source] of listed) {
42590
+ for (const declared of classNamesOf(source)) {
42591
+ if (declared !== asked) {
42592
+ otherClasses.add(declared);
42593
+ }
42594
+ }
42595
+ }
42596
+ }
42439
42597
  const sites = [];
42440
42598
  for (const [script, source] of listed) {
42441
- for (const site of callSitesOf(source, bare)) {
42599
+ for (const site of callSitesOf(source, bare, otherClasses)) {
42442
42600
  const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
42443
42601
  sites.push({
42444
42602
  script,
@@ -42548,7 +42706,7 @@ function longerWaitNote(tool, op, waitedMs) {
42548
42706
  if (spec !== undefined && argumentsOf(spec, op).includes("timeoutMs")) {
42549
42707
  return "timeoutMs waits longer next time.";
42550
42708
  }
42551
- const how = tool === "runtime_wait" && op === "frames" ? `the wait is counted from the number of frames, at ${SLOWEST_FRAME_RATE} a second on top of GDHARNESS_RUNTIME_TIMEOUT_MS, and this game fell below that rate, so ask for fewer frames at a time` : "the wait is GDHARNESS_RUNTIME_TIMEOUT_MS, which is set in the environment the server is started with";
42709
+ const how = tool === "runtime_wait" && op === "frames" ? `the wait is counted from the number of frames, at ${SLOWEST_FRAME_RATE} a second on top of GDHARNESS_RUNTIME_TIMEOUT_MS: a game drawing frames more slowly than that is waited on in fewer frames at a time, and a game inside one long frame, such as a long _ready, answers no call however few frames are asked for` : "the wait is GDHARNESS_RUNTIME_TIMEOUT_MS, which is set in the environment the server is started with";
42552
42710
  return `${tool} ${op} takes no timeoutMs: it was given ${waitedMs}ms, and ${how}.`;
42553
42711
  }
42554
42712
  function projectSpelling(projectPath, absolutePath) {
@@ -45737,15 +45895,17 @@ class GodotServer {
45737
45895
  hold: current === null ? {} : await this.holdOf(current, active)
45738
45896
  };
45739
45897
  })();
45740
- const [games, { playing, active, run, hold }, scan, editorStatus] = await Promise.all([
45898
+ const [games, { playing, active, run, hold }, scan, editorStatus, servers] = await Promise.all([
45741
45899
  pinged,
45742
45900
  asked,
45743
45901
  this.editorScanState(),
45744
- this.getEditorStatusPayload()
45902
+ this.getEditorStatusPayload(),
45903
+ this.editorServersListening()
45745
45904
  ]);
45746
45905
  return this.jsonTextResponse({
45747
45906
  editor: {
45748
45907
  ...editorStatus,
45908
+ ...servers,
45749
45909
  debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
45750
45910
  scan
45751
45911
  },
@@ -45771,6 +45931,30 @@ class GodotServer {
45771
45931
  }
45772
45932
  });
45773
45933
  }
45934
+ async editorServersListening() {
45935
+ const status = this.godotBridge.getStatus();
45936
+ const { lspPort, dapPort } = status;
45937
+ if (!status.connected || lspPort === undefined || dapPort === undefined) {
45938
+ return {};
45939
+ }
45940
+ const [lsp, dap] = await Promise.all([acceptsConnections(lspPort), acceptsConnections(dapPort)]);
45941
+ const greetedAgoMs = status.connectedAt === undefined ? null : Date.now() - status.connectedAt.getTime();
45942
+ return {
45943
+ ...lsp === null ? {} : { lspListening: lsp },
45944
+ ...dap === null ? {} : { dapListening: dap },
45945
+ serversNote: notListeningNote({ lsp: { port: lspPort, listening: lsp }, dap: { port: dapPort, listening: dap } }, greetedAgoMs)
45946
+ };
45947
+ }
45948
+ async editorServersOnceStarted() {
45949
+ const deadline = Date.now() + EDITOR_SERVERS_START_MS;
45950
+ for (;; ) {
45951
+ const answer = await this.editorServersListening();
45952
+ if (answer.serversNote === undefined || Date.now() >= deadline) {
45953
+ return answer;
45954
+ }
45955
+ await delay4(500);
45956
+ }
45957
+ }
45774
45958
  settingKeysOf(projectPath) {
45775
45959
  if (projectPath === undefined) {
45776
45960
  return null;
@@ -45814,6 +45998,7 @@ class GodotServer {
45814
45998
  "editor_status says whether anything has reached the bridge since"
45815
45999
  ]);
45816
46000
  }
46001
+ const servers = await this.editorServersOnceStarted();
45817
46002
  const now = this.godotBridge.getStatus();
45818
46003
  const after = this.settingKeysOf(before.projectPath);
45819
46004
  return this.jsonTextResponse({
@@ -45824,6 +46009,7 @@ class GodotServer {
45824
46009
  addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
45825
46010
  staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()),
45826
46011
  ...this.whatTheEditorDropped(settingsBefore, after),
46012
+ ...servers,
45827
46013
  tookMs: Date.now() - began
45828
46014
  });
45829
46015
  }
@@ -46220,6 +46406,7 @@ class GodotServer {
46220
46406
  } catch (error) {
46221
46407
  return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
46222
46408
  `This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
46409
+ "Godot binds its debug adapter once, as the editor starts, and reports a failure only in the editor's own log panel: editor_launch restart binds it again",
46223
46410
  "GDHARNESS_DAP_PORT points this server at another one"
46224
46411
  ]);
46225
46412
  }
@@ -48370,7 +48557,7 @@ async function runGodotServer() {
48370
48557
  const server = new GodotServer;
48371
48558
  await server.run();
48372
48559
  }
48373
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
48560
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless it is called through that class by name and one class was asked about. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
48374
48561
  var init_server3 = __esm(() => {
48375
48562
  init_mcp();
48376
48563
  init_stdio2();
@@ -48402,6 +48589,7 @@ var init_server3 = __esm(() => {
48402
48589
  init_lsp_client();
48403
48590
  init_outside();
48404
48591
  init_paths();
48592
+ init_port_probe();
48405
48593
  init_ports();
48406
48594
  init_process_children();
48407
48595
  init_process_time();
package/build/index.js CHANGED
@@ -26613,11 +26613,117 @@ function bodyCallsSuper(code, line, lambda) {
26613
26613
  }
26614
26614
  return false;
26615
26615
  }
26616
- function callSitesOf(source, name) {
26616
+ function parametersOf(text, from) {
26617
+ const open = text.indexOf("(", from);
26618
+ if (open === -1) {
26619
+ return [];
26620
+ }
26621
+ const names = [];
26622
+ let depth = 0;
26623
+ let start = open + 1;
26624
+ for (let at = open;at < text.length; at += 1) {
26625
+ const here = text[at];
26626
+ if (here === "(" || here === "[" || here === "{") {
26627
+ depth += 1;
26628
+ } else if (here === ")" || here === "]" || here === "}") {
26629
+ depth -= 1;
26630
+ }
26631
+ if (here === "," && depth === 1 || depth === 0) {
26632
+ const named = new RegExp(`^\\s*(${NAME})`, "u").exec(text.slice(start, at));
26633
+ if (named?.[1] !== undefined) {
26634
+ names.push(named[1]);
26635
+ }
26636
+ start = at + 1;
26637
+ if (depth === 0) {
26638
+ break;
26639
+ }
26640
+ }
26641
+ }
26642
+ return names;
26643
+ }
26644
+ function isLocalAt(lines, index, name) {
26645
+ const word = escapedForPattern(name);
26646
+ const declares = new RegExp(`(?<![\\p{L}\\p{N}_.])(?:var|const)\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
26647
+ const iterates = new RegExp(`^\\s*for\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
26648
+ const own = lines[index] ?? "";
26649
+ const headerOnOwnLine = FUNC_LINE.exec(own) ?? LAMBDA_HEADER.exec(own);
26650
+ if (headerOnOwnLine !== null && parametersOf(own, headerOnOwnLine.index).includes(name)) {
26651
+ return true;
26652
+ }
26653
+ if (FUNC_LINE.test(own)) {
26654
+ return false;
26655
+ }
26656
+ let depth = indentOf(own);
26657
+ for (let at = index - 1;at >= 0; at -= 1) {
26658
+ const text = lines[at] ?? "";
26659
+ if (text.trim() === "") {
26660
+ continue;
26661
+ }
26662
+ const indent = indentOf(text);
26663
+ if (indent > depth) {
26664
+ continue;
26665
+ }
26666
+ const named = FUNC_LINE.exec(text);
26667
+ if (indent < depth) {
26668
+ depth = indent;
26669
+ if (named !== null) {
26670
+ return parametersOf(text, named.index).includes(name);
26671
+ }
26672
+ const lambda = LAMBDA_HEADER.exec(text);
26673
+ if (lambda !== null && parametersOf(text, lambda.index).includes(name) || iterates.test(text)) {
26674
+ return true;
26675
+ }
26676
+ }
26677
+ if (named !== null || depth === 0) {
26678
+ return false;
26679
+ }
26680
+ if (declares.test(text)) {
26681
+ return true;
26682
+ }
26683
+ }
26684
+ return false;
26685
+ }
26686
+ function receiverBefore(code, offset) {
26687
+ let at = offset - 1;
26688
+ while (at >= 0 && /\s/.test(code[at] ?? "")) {
26689
+ at -= 1;
26690
+ }
26691
+ if (code[at] !== ".") {
26692
+ return null;
26693
+ }
26694
+ at -= 1;
26695
+ while (at >= 0 && /\s/.test(code[at] ?? "")) {
26696
+ at -= 1;
26697
+ }
26698
+ let start = at + 1;
26699
+ while (start > 0 && isIdentifierPart(code[start - 1])) {
26700
+ start -= 1;
26701
+ }
26702
+ return start <= at ? code.slice(start, at + 1) : null;
26703
+ }
26704
+ var MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
26705
+ function bodyHolding(body, offset) {
26706
+ const inner = body.inner.find((one) => one.start <= offset && offset <= one.end);
26707
+ return inner === undefined ? body : bodyHolding(inner, offset);
26708
+ }
26709
+ function callSitesOf(source, name, otherClasses = new Set) {
26617
26710
  const regions = regionsOf(source);
26618
26711
  const code = codeOf(source, regions);
26712
+ const codeLines = code.split(`
26713
+ `);
26619
26714
  const lines = new Lines(source);
26620
- return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).map((one) => {
26715
+ const shape = shapeOf(source);
26716
+ return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).filter((one) => {
26717
+ if (one.afterDot) {
26718
+ const receiver = receiverBefore(code, one.offset);
26719
+ return receiver === null || !otherClasses.has(receiver);
26720
+ }
26721
+ const index = lines.positionOf(one.offset).line;
26722
+ if (isLocalAt(codeLines, index, name)) {
26723
+ return false;
26724
+ }
26725
+ return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind));
26726
+ }).map((one) => {
26621
26727
  const line = lines.positionOf(one.offset).line + 1;
26622
26728
  const after = code.slice(one.offset + name.length).trimStart();
26623
26729
  return { line, within: enclosingFunction(source, line, code), called: after.startsWith("(") };
@@ -30874,6 +30980,41 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
30874
30980
  });
30875
30981
  }
30876
30982
 
30983
+ // src/port-probe.ts
30984
+ import { connect as connect2 } from "node:net";
30985
+ function acceptsConnections(port, withinMs = 2000) {
30986
+ return new Promise((resolve) => {
30987
+ const socket = connect2({ host: "127.0.0.1", port });
30988
+ const settle = (answer) => {
30989
+ socket.destroy();
30990
+ resolve(answer);
30991
+ };
30992
+ socket.setTimeout(withinMs, () => {
30993
+ settle(null);
30994
+ });
30995
+ socket.once("connect", () => {
30996
+ settle(true);
30997
+ });
30998
+ socket.once("error", () => {
30999
+ settle(false);
31000
+ });
31001
+ });
31002
+ }
31003
+ var STARTING_MS = 20000;
31004
+ function notListeningNote(servers, greetedAgoMs) {
31005
+ const down = [
31006
+ ...servers.lsp.listening === false ? [{ name: "language server", port: servers.lsp.port }] : [],
31007
+ ...servers.dap.listening === false ? [{ name: "debug adapter", port: servers.dap.port }] : []
31008
+ ];
31009
+ if (down.length === 0) {
31010
+ return;
31011
+ }
31012
+ const named = down.map((one) => `its ${one.name} on port ${one.port}`).join(" and ");
31013
+ const needs = down.map((one) => one.name === "debug adapter" ? "played runs and the debug tools need the debug adapter" : "diagnostics, symbols and renames need the language server").join(", and ");
31014
+ const starting = greetedAgoMs !== null && greetedAgoMs < STARTING_MS ? ` The editor greeted ${Math.round(greetedAgoMs / 1000)}s ago, so ${down.length === 1 ? "it" : "they"} may still be starting: ask again in a few seconds.` : "";
31015
+ return `The editor names ${named}, and nothing accepts connections there. Godot binds each once, as the editor starts, and reports a failure only in the editor's own log panel; ${needs}. editor_launch restart binds ${down.length === 1 ? "it" : "them"} again.${starting}`;
31016
+ }
31017
+
30877
31018
  // src/process-time.ts
30878
31019
  import { execFile as execFile3 } from "node:child_process";
30879
31020
  import { promisify as promisify3 } from "node:util";
@@ -33228,6 +33369,11 @@ function placedLambda(row, sourceOf) {
33228
33369
  const within = source === null ? null : enclosingFunction(source, row.line);
33229
33370
  return within === null ? row : { ...row, within };
33230
33371
  }
33372
+ function classNamesOf(source) {
33373
+ const shape = shapeOf(source);
33374
+ const inner = (body) => body.inner.flatMap((one) => [...one.name === null ? [] : [one.name], ...inner(one)]);
33375
+ return [...shape.className === null ? [] : [shape.className.name], ...inner(shape.body)];
33376
+ }
33231
33377
  var CALL_SITES = 40;
33232
33378
  function callersOf(totals, name, scripts) {
33233
33379
  const listed = [...scripts];
@@ -33235,9 +33381,20 @@ function callersOf(totals, name, scripts) {
33235
33381
  const rows = rowsOf(totals, (script) => sources.get(script) ?? null);
33236
33382
  const bare = bareName(name);
33237
33383
  const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
33384
+ const asked = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : null;
33385
+ const otherClasses = new Set;
33386
+ if (asked !== null) {
33387
+ for (const [, source] of listed) {
33388
+ for (const declared of classNamesOf(source)) {
33389
+ if (declared !== asked) {
33390
+ otherClasses.add(declared);
33391
+ }
33392
+ }
33393
+ }
33394
+ }
33238
33395
  const sites = [];
33239
33396
  for (const [script, source] of listed) {
33240
- for (const site of callSitesOf(source, bare)) {
33397
+ for (const site of callSitesOf(source, bare, otherClasses)) {
33241
33398
  const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
33242
33399
  sites.push({
33243
33400
  script,
@@ -34242,7 +34399,7 @@ var TOOL_SPECS = [
34242
34399
  callersOf: {
34243
34400
  type: "string",
34244
34401
  ops: ["profile"],
34245
- description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too and a call made through a string is not."
34402
+ description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except one called through another class of the project by name when Class.name was asked, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed."
34246
34403
  },
34247
34404
  limit: {
34248
34405
  type: "integer",
@@ -34334,7 +34491,7 @@ var TOOL_SPECS = [
34334
34491
  },
34335
34492
  {
34336
34493
  name: "editor_status",
34337
- description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
34494
+ description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
34338
34495
  parameters: {},
34339
34496
  requires: []
34340
34497
  },
@@ -34909,6 +35066,7 @@ var __dirname2 = dirname15(fileURLToPath4(import.meta.url));
34909
35066
  function editorRestartTimeoutMs() {
34910
35067
  return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
34911
35068
  }
35069
+ var EDITOR_SERVERS_START_MS = 20000;
34912
35070
  function waitFromEnv(variable, fallback) {
34913
35071
  const override = Number.parseInt(envValue(variable) ?? "", 10);
34914
35072
  return Number.isInteger(override) && override > 0 ? override : fallback;
@@ -34972,7 +35130,7 @@ function longerWaitNote(tool, op, waitedMs) {
34972
35130
  if (spec !== undefined && argumentsOf(spec, op).includes("timeoutMs")) {
34973
35131
  return "timeoutMs waits longer next time.";
34974
35132
  }
34975
- const how = tool === "runtime_wait" && op === "frames" ? `the wait is counted from the number of frames, at ${SLOWEST_FRAME_RATE} a second on top of GDHARNESS_RUNTIME_TIMEOUT_MS, and this game fell below that rate, so ask for fewer frames at a time` : "the wait is GDHARNESS_RUNTIME_TIMEOUT_MS, which is set in the environment the server is started with";
35133
+ const how = tool === "runtime_wait" && op === "frames" ? `the wait is counted from the number of frames, at ${SLOWEST_FRAME_RATE} a second on top of GDHARNESS_RUNTIME_TIMEOUT_MS: a game drawing frames more slowly than that is waited on in fewer frames at a time, and a game inside one long frame, such as a long _ready, answers no call however few frames are asked for` : "the wait is GDHARNESS_RUNTIME_TIMEOUT_MS, which is set in the environment the server is started with";
34976
35134
  return `${tool} ${op} takes no timeoutMs: it was given ${waitedMs}ms, and ${how}.`;
34977
35135
  }
34978
35136
  function projectSpelling(projectPath, absolutePath) {
@@ -35692,7 +35850,7 @@ function playedCap(held) {
35692
35850
  totalCapped: readBoolean(held, "totalCapped") === true
35693
35851
  };
35694
35852
  }
35695
- var CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too. Each place carries the profile's numbers for the function it is written in.";
35853
+ var CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless it is called through that class by name and one class was asked about. Each place carries the profile's numbers for the function it is written in.";
35696
35854
  function projectSourceOf(projectPath) {
35697
35855
  const read = new Map;
35698
35856
  return (script) => {
@@ -38231,15 +38389,17 @@ class GodotServer {
38231
38389
  hold: current === null ? {} : await this.holdOf(current, active)
38232
38390
  };
38233
38391
  })();
38234
- const [games, { playing, active, run, hold }, scan, editorStatus] = await Promise.all([
38392
+ const [games, { playing, active, run, hold }, scan, editorStatus, servers] = await Promise.all([
38235
38393
  pinged,
38236
38394
  asked,
38237
38395
  this.editorScanState(),
38238
- this.getEditorStatusPayload()
38396
+ this.getEditorStatusPayload(),
38397
+ this.editorServersListening()
38239
38398
  ]);
38240
38399
  return this.jsonTextResponse({
38241
38400
  editor: {
38242
38401
  ...editorStatus,
38402
+ ...servers,
38243
38403
  debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
38244
38404
  scan
38245
38405
  },
@@ -38265,6 +38425,30 @@ class GodotServer {
38265
38425
  }
38266
38426
  });
38267
38427
  }
38428
+ async editorServersListening() {
38429
+ const status = this.godotBridge.getStatus();
38430
+ const { lspPort, dapPort } = status;
38431
+ if (!status.connected || lspPort === undefined || dapPort === undefined) {
38432
+ return {};
38433
+ }
38434
+ const [lsp, dap] = await Promise.all([acceptsConnections(lspPort), acceptsConnections(dapPort)]);
38435
+ const greetedAgoMs = status.connectedAt === undefined ? null : Date.now() - status.connectedAt.getTime();
38436
+ return {
38437
+ ...lsp === null ? {} : { lspListening: lsp },
38438
+ ...dap === null ? {} : { dapListening: dap },
38439
+ serversNote: notListeningNote({ lsp: { port: lspPort, listening: lsp }, dap: { port: dapPort, listening: dap } }, greetedAgoMs)
38440
+ };
38441
+ }
38442
+ async editorServersOnceStarted() {
38443
+ const deadline = Date.now() + EDITOR_SERVERS_START_MS;
38444
+ for (;; ) {
38445
+ const answer = await this.editorServersListening();
38446
+ if (answer.serversNote === undefined || Date.now() >= deadline) {
38447
+ return answer;
38448
+ }
38449
+ await delay4(500);
38450
+ }
38451
+ }
38268
38452
  settingKeysOf(projectPath) {
38269
38453
  if (projectPath === undefined) {
38270
38454
  return null;
@@ -38308,6 +38492,7 @@ class GodotServer {
38308
38492
  "editor_status says whether anything has reached the bridge since"
38309
38493
  ]);
38310
38494
  }
38495
+ const servers = await this.editorServersOnceStarted();
38311
38496
  const now = this.godotBridge.getStatus();
38312
38497
  const after = this.settingKeysOf(before.projectPath);
38313
38498
  return this.jsonTextResponse({
@@ -38318,6 +38503,7 @@ class GodotServer {
38318
38503
  addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
38319
38504
  staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()),
38320
38505
  ...this.whatTheEditorDropped(settingsBefore, after),
38506
+ ...servers,
38321
38507
  tookMs: Date.now() - began
38322
38508
  });
38323
38509
  }
@@ -38714,6 +38900,7 @@ class GodotServer {
38714
38900
  } catch (error) {
38715
38901
  return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
38716
38902
  `This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
38903
+ "Godot binds its debug adapter once, as the editor starts, and reports a failure only in the editor's own log panel: editor_launch restart binds it again",
38717
38904
  "GDHARNESS_DAP_PORT points this server at another one"
38718
38905
  ]);
38719
38906
  }
package/build/keeper.js CHANGED
@@ -551,6 +551,7 @@ var CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
551
551
  var EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
552
552
  var FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
553
553
  var LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
554
+ var MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
554
555
  var KEYWORDS = new Set([
555
556
  "and",
556
557
  "as",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.60",
3
+ "version": "1.1.61",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",