gdharness 1.1.60 → 1.1.62

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,143 @@ 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 declaredClass(text, name) {
9274
+ const word = `(?<![\\p{L}\\p{N}_.])${escapedForPattern(name)}`;
9275
+ const found = new RegExp(`${word}\\s*:\\s*(${DOTTED})`, "u").exec(text)?.[1] ?? new RegExp(`${word}\\s*:=\\s*(${DOTTED})\\.new\\s*\\(`, "u").exec(text)?.[1] ?? new RegExp(`${word}\\s*:?=[^\\n]*?\\bas\\s+(${DOTTED})\\s*$`, "u").exec(text)?.[1];
9276
+ return found === undefined ? null : found.slice(found.lastIndexOf(".") + 1);
9277
+ }
9278
+ function localAt(lines, index, name) {
9279
+ const word = escapedForPattern(name);
9280
+ const declares = new RegExp(`(?<![\\p{L}\\p{N}_.])(?:var|const)\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
9281
+ const iterates = new RegExp(`^\\s*for\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
9282
+ const local = (text) => ({ type: declaredClass(text, name) });
9283
+ const own = lines[index] ?? "";
9284
+ const headerOnOwnLine = FUNC_LINE.exec(own) ?? LAMBDA_HEADER.exec(own);
9285
+ if (headerOnOwnLine !== null && parametersOf(own, headerOnOwnLine.index).includes(name)) {
9286
+ return local(own);
9287
+ }
9288
+ if (FUNC_LINE.test(own)) {
9289
+ return null;
9290
+ }
9291
+ let depth = indentOf(own);
9292
+ for (let at = index - 1;at >= 0; at -= 1) {
9293
+ const text = lines[at] ?? "";
9294
+ if (text.trim() === "") {
9295
+ continue;
9296
+ }
9297
+ const indent = indentOf(text);
9298
+ if (indent > depth) {
9299
+ continue;
9300
+ }
9301
+ const named = FUNC_LINE.exec(text);
9302
+ if (indent < depth) {
9303
+ depth = indent;
9304
+ if (named !== null) {
9305
+ return parametersOf(text, named.index).includes(name) ? local(text) : null;
9306
+ }
9307
+ const lambda = LAMBDA_HEADER.exec(text);
9308
+ if (lambda !== null && parametersOf(text, lambda.index).includes(name) || iterates.test(text)) {
9309
+ return local(text);
9310
+ }
9311
+ }
9312
+ if (named !== null || depth === 0) {
9313
+ return null;
9314
+ }
9315
+ if (declares.test(text)) {
9316
+ return local(text);
9317
+ }
9318
+ }
9319
+ return null;
9320
+ }
9321
+ function receiverBefore(code, offset) {
9322
+ let at = offset - 1;
9323
+ while (at >= 0 && /\s/.test(code[at] ?? "")) {
9324
+ at -= 1;
9325
+ }
9326
+ if (code[at] !== ".") {
9327
+ return null;
9328
+ }
9329
+ at -= 1;
9330
+ while (at >= 0 && /\s/.test(code[at] ?? "")) {
9331
+ at -= 1;
9332
+ }
9333
+ let start = at + 1;
9334
+ while (start > 0 && isIdentifierPart(code[start - 1])) {
9335
+ start -= 1;
9336
+ }
9337
+ if (start > at) {
9338
+ return null;
9339
+ }
9340
+ let before = start - 1;
9341
+ while (before >= 0 && /\s/.test(code[before] ?? "")) {
9342
+ before -= 1;
9343
+ }
9344
+ return { name: code.slice(start, at + 1), dotted: code[before] === "." };
9345
+ }
9346
+ function bodyHolding(body, offset) {
9347
+ const inner = body.inner.find((one) => one.start <= offset && offset <= one.end);
9348
+ return inner === undefined ? body : bodyHolding(inner, offset);
9349
+ }
9350
+ function callSitesOf(source, name, otherClasses = new Set) {
9246
9351
  const regions = regionsOf(source);
9247
9352
  const code = codeOf(source, regions);
9353
+ const codeLines = code.split(`
9354
+ `);
9248
9355
  const lines = new Lines(source);
9249
- return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).map((one) => {
9356
+ const shape = shapeOf(source);
9357
+ const memberClass = (member, at) => {
9358
+ const declared = bodyHolding(shape.body, at).declarations.find((one) => one.name === member && (one.kind === "var" || one.kind === "const"));
9359
+ return declared === undefined ? null : declaredClass(codeLines[lines.positionOf(declared.offset).line] ?? "", member);
9360
+ };
9361
+ return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).filter((one) => {
9362
+ const index = lines.positionOf(one.offset).line;
9363
+ if (one.afterDot) {
9364
+ const receiver = receiverBefore(code, one.offset);
9365
+ if (receiver === null || otherClasses.size === 0) {
9366
+ return true;
9367
+ }
9368
+ if (otherClasses.has(receiver.name)) {
9369
+ return false;
9370
+ }
9371
+ if (receiver.dotted) {
9372
+ return true;
9373
+ }
9374
+ const typed = localAt(codeLines, index, receiver.name)?.type ?? memberClass(receiver.name, one.offset);
9375
+ return typed === null || !otherClasses.has(typed);
9376
+ }
9377
+ if (localAt(codeLines, index, name) !== null) {
9378
+ return false;
9379
+ }
9380
+ return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind));
9381
+ }).map((one) => {
9250
9382
  const line = lines.positionOf(one.offset).line + 1;
9251
9383
  const after = code.slice(one.offset + name.length).trimStart();
9252
9384
  return { line, within: enclosingFunction(source, line, code), called: after.startsWith("(") };
@@ -9267,7 +9399,7 @@ function collectEnumValues(text, from, into) {
9267
9399
  function isIdentifier(name) {
9268
9400
  return new RegExp(`^${NAME}$`, "u").test(name) && !KEYWORDS.has(name);
9269
9401
  }
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;
9402
+ 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, DOTTED, MEMBER_KINDS, KEYWORDS;
9271
9403
  var init_gdscript_source = __esm(() => {
9272
9404
  IDENTIFIER_START = /[\p{L}_]/u;
9273
9405
  IDENTIFIER_PART = /[\p{L}\p{N}_]/u;
@@ -9279,6 +9411,8 @@ var init_gdscript_source = __esm(() => {
9279
9411
  FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
9280
9412
  SUPER_CALL = /(?<![\p{L}\p{N}_.])super\s*[.(]/u;
9281
9413
  LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
9414
+ DOTTED = `${NAME}(?:\\.${NAME})*`;
9415
+ MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
9282
9416
  KEYWORDS = new Set([
9283
9417
  "and",
9284
9418
  "as",
@@ -18243,7 +18377,7 @@ var init_tool_definitions = __esm(() => {
18243
18377
  callersOf: {
18244
18378
  type: "string",
18245
18379
  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."
18380
+ 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, when Class.name was asked, one called through another class of the project by name or through a parameter, local or class variable declared as one, 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
18381
  },
18248
18382
  limit: {
18249
18383
  type: "integer",
@@ -18335,7 +18469,7 @@ var init_tool_definitions = __esm(() => {
18335
18469
  },
18336
18470
  {
18337
18471
  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.",
18472
+ 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. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. 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
18473
  parameters: {},
18340
18474
  requires: []
18341
18475
  },
@@ -40311,6 +40445,42 @@ var init_outside = __esm(() => {
40311
40445
  KEEPER_SCRIPT = fileURLToPath4(new URL("./keeper.js", import.meta.url));
40312
40446
  });
40313
40447
 
40448
+ // src/port-probe.ts
40449
+ import { connect as connect3 } from "node:net";
40450
+ function acceptsConnections(port, withinMs = 2000) {
40451
+ return new Promise((resolve) => {
40452
+ const socket = connect3({ host: "127.0.0.1", port });
40453
+ const settle = (answer) => {
40454
+ socket.destroy();
40455
+ resolve(answer);
40456
+ };
40457
+ socket.setTimeout(withinMs, () => {
40458
+ settle(null);
40459
+ });
40460
+ socket.once("connect", () => {
40461
+ settle(true);
40462
+ });
40463
+ socket.once("error", () => {
40464
+ settle(false);
40465
+ });
40466
+ });
40467
+ }
40468
+ function notListeningNote(servers, greetedAgoMs) {
40469
+ const down = [
40470
+ ...servers.lsp.listening === false ? [{ name: "language server", port: servers.lsp.port }] : [],
40471
+ ...servers.dap.listening === false ? [{ name: "debug adapter", port: servers.dap.port }] : []
40472
+ ];
40473
+ if (down.length === 0) {
40474
+ return;
40475
+ }
40476
+ const named = down.map((one) => `its ${one.name} on port ${one.port}`).join(" and ");
40477
+ 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 ");
40478
+ 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.` : "";
40479
+ 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}`;
40480
+ }
40481
+ var STARTING_MS = 20000;
40482
+ var init_port_probe = () => {};
40483
+
40314
40484
  // src/process-time.ts
40315
40485
  import { execFile as execFile3 } from "node:child_process";
40316
40486
  import { promisify as promisify3 } from "node:util";
@@ -42430,15 +42600,31 @@ function placedLambda(row, sourceOf) {
42430
42600
  const within = source === null ? null : enclosingFunction(source, row.line);
42431
42601
  return within === null ? row : { ...row, within };
42432
42602
  }
42603
+ function classNamesOf(source) {
42604
+ const shape = shapeOf(source);
42605
+ const inner = (body) => body.inner.flatMap((one) => [...one.name === null ? [] : [one.name], ...inner(one)]);
42606
+ return [...shape.className === null ? [] : [shape.className.name], ...inner(shape.body)];
42607
+ }
42433
42608
  function callersOf(totals, name, scripts) {
42434
42609
  const listed = [...scripts];
42435
42610
  const sources = new Map(listed);
42436
42611
  const rows = rowsOf(totals, (script) => sources.get(script) ?? null);
42437
42612
  const bare = bareName(name);
42438
42613
  const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
42614
+ const asked = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : null;
42615
+ const otherClasses = new Set;
42616
+ if (asked !== null) {
42617
+ for (const [, source] of listed) {
42618
+ for (const declared of classNamesOf(source)) {
42619
+ if (declared !== asked) {
42620
+ otherClasses.add(declared);
42621
+ }
42622
+ }
42623
+ }
42624
+ }
42439
42625
  const sites = [];
42440
42626
  for (const [script, source] of listed) {
42441
- for (const site of callSitesOf(source, bare)) {
42627
+ for (const site of callSitesOf(source, bare, otherClasses)) {
42442
42628
  const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
42443
42629
  sites.push({
42444
42630
  script,
@@ -42548,7 +42734,7 @@ function longerWaitNote(tool, op, waitedMs) {
42548
42734
  if (spec !== undefined && argumentsOf(spec, op).includes("timeoutMs")) {
42549
42735
  return "timeoutMs waits longer next time.";
42550
42736
  }
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";
42737
+ 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
42738
  return `${tool} ${op} takes no timeoutMs: it was given ${waitedMs}ms, and ${how}.`;
42553
42739
  }
42554
42740
  function projectSpelling(projectPath, absolutePath) {
@@ -45737,15 +45923,17 @@ class GodotServer {
45737
45923
  hold: current === null ? {} : await this.holdOf(current, active)
45738
45924
  };
45739
45925
  })();
45740
- const [games, { playing, active, run, hold }, scan, editorStatus] = await Promise.all([
45926
+ const [games, { playing, active, run, hold }, scan, editorStatus, servers] = await Promise.all([
45741
45927
  pinged,
45742
45928
  asked,
45743
45929
  this.editorScanState(),
45744
- this.getEditorStatusPayload()
45930
+ this.getEditorStatusPayload(),
45931
+ this.editorServersListening()
45745
45932
  ]);
45746
45933
  return this.jsonTextResponse({
45747
45934
  editor: {
45748
45935
  ...editorStatus,
45936
+ ...servers,
45749
45937
  debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
45750
45938
  scan
45751
45939
  },
@@ -45771,6 +45959,30 @@ class GodotServer {
45771
45959
  }
45772
45960
  });
45773
45961
  }
45962
+ async editorServersListening() {
45963
+ const status = this.godotBridge.getStatus();
45964
+ const { lspPort, dapPort } = status;
45965
+ if (!status.connected || lspPort === undefined || dapPort === undefined) {
45966
+ return {};
45967
+ }
45968
+ const [lsp, dap] = await Promise.all([acceptsConnections(lspPort), acceptsConnections(dapPort)]);
45969
+ const greetedAgoMs = status.connectedAt === undefined ? null : Date.now() - status.connectedAt.getTime();
45970
+ return {
45971
+ ...lsp === null ? {} : { lspListening: lsp },
45972
+ ...dap === null ? {} : { dapListening: dap },
45973
+ serversNote: notListeningNote({ lsp: { port: lspPort, listening: lsp }, dap: { port: dapPort, listening: dap } }, greetedAgoMs)
45974
+ };
45975
+ }
45976
+ async editorServersOnceStarted() {
45977
+ const deadline = Date.now() + EDITOR_SERVERS_START_MS;
45978
+ for (;; ) {
45979
+ const answer = await this.editorServersListening();
45980
+ if (answer.serversNote === undefined || Date.now() >= deadline) {
45981
+ return answer;
45982
+ }
45983
+ await delay4(500);
45984
+ }
45985
+ }
45774
45986
  settingKeysOf(projectPath) {
45775
45987
  if (projectPath === undefined) {
45776
45988
  return null;
@@ -45814,6 +46026,7 @@ class GodotServer {
45814
46026
  "editor_status says whether anything has reached the bridge since"
45815
46027
  ]);
45816
46028
  }
46029
+ const servers = await this.editorServersOnceStarted();
45817
46030
  const now = this.godotBridge.getStatus();
45818
46031
  const after = this.settingKeysOf(before.projectPath);
45819
46032
  return this.jsonTextResponse({
@@ -45824,6 +46037,7 @@ class GodotServer {
45824
46037
  addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
45825
46038
  staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()),
45826
46039
  ...this.whatTheEditorDropped(settingsBefore, after),
46040
+ ...servers,
45827
46041
  tookMs: Date.now() - began
45828
46042
  });
45829
46043
  }
@@ -46220,6 +46434,7 @@ class GodotServer {
46220
46434
  } catch (error) {
46221
46435
  return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
46222
46436
  `This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
46437
+ "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
46438
  "GDHARNESS_DAP_PORT points this server at another one"
46224
46439
  ]);
46225
46440
  }
@@ -48370,7 +48585,7 @@ async function runGodotServer() {
48370
48585
  const server = new GodotServer;
48371
48586
  await server.run();
48372
48587
  }
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 "';
48588
+ 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 one class was asked about and it is called through another class by name or through a variable declared as one. 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
48589
  var init_server3 = __esm(() => {
48375
48590
  init_mcp();
48376
48591
  init_stdio2();
@@ -48402,6 +48617,7 @@ var init_server3 = __esm(() => {
48402
48617
  init_lsp_client();
48403
48618
  init_outside();
48404
48619
  init_paths();
48620
+ init_port_probe();
48405
48621
  init_ports();
48406
48622
  init_process_children();
48407
48623
  init_process_time();
package/build/index.js CHANGED
@@ -26613,11 +26613,145 @@ 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
+ var DOTTED = `${NAME}(?:\\.${NAME})*`;
26645
+ function declaredClass(text, name) {
26646
+ const word = `(?<![\\p{L}\\p{N}_.])${escapedForPattern(name)}`;
26647
+ const found = new RegExp(`${word}\\s*:\\s*(${DOTTED})`, "u").exec(text)?.[1] ?? new RegExp(`${word}\\s*:=\\s*(${DOTTED})\\.new\\s*\\(`, "u").exec(text)?.[1] ?? new RegExp(`${word}\\s*:?=[^\\n]*?\\bas\\s+(${DOTTED})\\s*$`, "u").exec(text)?.[1];
26648
+ return found === undefined ? null : found.slice(found.lastIndexOf(".") + 1);
26649
+ }
26650
+ function localAt(lines, index, name) {
26651
+ const word = escapedForPattern(name);
26652
+ const declares = new RegExp(`(?<![\\p{L}\\p{N}_.])(?:var|const)\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
26653
+ const iterates = new RegExp(`^\\s*for\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
26654
+ const local = (text) => ({ type: declaredClass(text, name) });
26655
+ const own = lines[index] ?? "";
26656
+ const headerOnOwnLine = FUNC_LINE.exec(own) ?? LAMBDA_HEADER.exec(own);
26657
+ if (headerOnOwnLine !== null && parametersOf(own, headerOnOwnLine.index).includes(name)) {
26658
+ return local(own);
26659
+ }
26660
+ if (FUNC_LINE.test(own)) {
26661
+ return null;
26662
+ }
26663
+ let depth = indentOf(own);
26664
+ for (let at = index - 1;at >= 0; at -= 1) {
26665
+ const text = lines[at] ?? "";
26666
+ if (text.trim() === "") {
26667
+ continue;
26668
+ }
26669
+ const indent = indentOf(text);
26670
+ if (indent > depth) {
26671
+ continue;
26672
+ }
26673
+ const named = FUNC_LINE.exec(text);
26674
+ if (indent < depth) {
26675
+ depth = indent;
26676
+ if (named !== null) {
26677
+ return parametersOf(text, named.index).includes(name) ? local(text) : null;
26678
+ }
26679
+ const lambda = LAMBDA_HEADER.exec(text);
26680
+ if (lambda !== null && parametersOf(text, lambda.index).includes(name) || iterates.test(text)) {
26681
+ return local(text);
26682
+ }
26683
+ }
26684
+ if (named !== null || depth === 0) {
26685
+ return null;
26686
+ }
26687
+ if (declares.test(text)) {
26688
+ return local(text);
26689
+ }
26690
+ }
26691
+ return null;
26692
+ }
26693
+ function receiverBefore(code, offset) {
26694
+ let at = offset - 1;
26695
+ while (at >= 0 && /\s/.test(code[at] ?? "")) {
26696
+ at -= 1;
26697
+ }
26698
+ if (code[at] !== ".") {
26699
+ return null;
26700
+ }
26701
+ at -= 1;
26702
+ while (at >= 0 && /\s/.test(code[at] ?? "")) {
26703
+ at -= 1;
26704
+ }
26705
+ let start = at + 1;
26706
+ while (start > 0 && isIdentifierPart(code[start - 1])) {
26707
+ start -= 1;
26708
+ }
26709
+ if (start > at) {
26710
+ return null;
26711
+ }
26712
+ let before = start - 1;
26713
+ while (before >= 0 && /\s/.test(code[before] ?? "")) {
26714
+ before -= 1;
26715
+ }
26716
+ return { name: code.slice(start, at + 1), dotted: code[before] === "." };
26717
+ }
26718
+ var MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
26719
+ function bodyHolding(body, offset) {
26720
+ const inner = body.inner.find((one) => one.start <= offset && offset <= one.end);
26721
+ return inner === undefined ? body : bodyHolding(inner, offset);
26722
+ }
26723
+ function callSitesOf(source, name, otherClasses = new Set) {
26617
26724
  const regions = regionsOf(source);
26618
26725
  const code = codeOf(source, regions);
26726
+ const codeLines = code.split(`
26727
+ `);
26619
26728
  const lines = new Lines(source);
26620
- return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).map((one) => {
26729
+ const shape = shapeOf(source);
26730
+ const memberClass = (member, at) => {
26731
+ const declared = bodyHolding(shape.body, at).declarations.find((one) => one.name === member && (one.kind === "var" || one.kind === "const"));
26732
+ return declared === undefined ? null : declaredClass(codeLines[lines.positionOf(declared.offset).line] ?? "", member);
26733
+ };
26734
+ return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).filter((one) => {
26735
+ const index = lines.positionOf(one.offset).line;
26736
+ if (one.afterDot) {
26737
+ const receiver = receiverBefore(code, one.offset);
26738
+ if (receiver === null || otherClasses.size === 0) {
26739
+ return true;
26740
+ }
26741
+ if (otherClasses.has(receiver.name)) {
26742
+ return false;
26743
+ }
26744
+ if (receiver.dotted) {
26745
+ return true;
26746
+ }
26747
+ const typed = localAt(codeLines, index, receiver.name)?.type ?? memberClass(receiver.name, one.offset);
26748
+ return typed === null || !otherClasses.has(typed);
26749
+ }
26750
+ if (localAt(codeLines, index, name) !== null) {
26751
+ return false;
26752
+ }
26753
+ return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind));
26754
+ }).map((one) => {
26621
26755
  const line = lines.positionOf(one.offset).line + 1;
26622
26756
  const after = code.slice(one.offset + name.length).trimStart();
26623
26757
  return { line, within: enclosingFunction(source, line, code), called: after.startsWith("(") };
@@ -30874,6 +31008,41 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
30874
31008
  });
30875
31009
  }
30876
31010
 
31011
+ // src/port-probe.ts
31012
+ import { connect as connect2 } from "node:net";
31013
+ function acceptsConnections(port, withinMs = 2000) {
31014
+ return new Promise((resolve) => {
31015
+ const socket = connect2({ host: "127.0.0.1", port });
31016
+ const settle = (answer) => {
31017
+ socket.destroy();
31018
+ resolve(answer);
31019
+ };
31020
+ socket.setTimeout(withinMs, () => {
31021
+ settle(null);
31022
+ });
31023
+ socket.once("connect", () => {
31024
+ settle(true);
31025
+ });
31026
+ socket.once("error", () => {
31027
+ settle(false);
31028
+ });
31029
+ });
31030
+ }
31031
+ var STARTING_MS = 20000;
31032
+ function notListeningNote(servers, greetedAgoMs) {
31033
+ const down = [
31034
+ ...servers.lsp.listening === false ? [{ name: "language server", port: servers.lsp.port }] : [],
31035
+ ...servers.dap.listening === false ? [{ name: "debug adapter", port: servers.dap.port }] : []
31036
+ ];
31037
+ if (down.length === 0) {
31038
+ return;
31039
+ }
31040
+ const named = down.map((one) => `its ${one.name} on port ${one.port}`).join(" and ");
31041
+ 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 ");
31042
+ 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.` : "";
31043
+ 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}`;
31044
+ }
31045
+
30877
31046
  // src/process-time.ts
30878
31047
  import { execFile as execFile3 } from "node:child_process";
30879
31048
  import { promisify as promisify3 } from "node:util";
@@ -33228,6 +33397,11 @@ function placedLambda(row, sourceOf) {
33228
33397
  const within = source === null ? null : enclosingFunction(source, row.line);
33229
33398
  return within === null ? row : { ...row, within };
33230
33399
  }
33400
+ function classNamesOf(source) {
33401
+ const shape = shapeOf(source);
33402
+ const inner = (body) => body.inner.flatMap((one) => [...one.name === null ? [] : [one.name], ...inner(one)]);
33403
+ return [...shape.className === null ? [] : [shape.className.name], ...inner(shape.body)];
33404
+ }
33231
33405
  var CALL_SITES = 40;
33232
33406
  function callersOf(totals, name, scripts) {
33233
33407
  const listed = [...scripts];
@@ -33235,9 +33409,20 @@ function callersOf(totals, name, scripts) {
33235
33409
  const rows = rowsOf(totals, (script) => sources.get(script) ?? null);
33236
33410
  const bare = bareName(name);
33237
33411
  const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
33412
+ const asked = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : null;
33413
+ const otherClasses = new Set;
33414
+ if (asked !== null) {
33415
+ for (const [, source] of listed) {
33416
+ for (const declared of classNamesOf(source)) {
33417
+ if (declared !== asked) {
33418
+ otherClasses.add(declared);
33419
+ }
33420
+ }
33421
+ }
33422
+ }
33238
33423
  const sites = [];
33239
33424
  for (const [script, source] of listed) {
33240
- for (const site of callSitesOf(source, bare)) {
33425
+ for (const site of callSitesOf(source, bare, otherClasses)) {
33241
33426
  const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
33242
33427
  sites.push({
33243
33428
  script,
@@ -34242,7 +34427,7 @@ var TOOL_SPECS = [
34242
34427
  callersOf: {
34243
34428
  type: "string",
34244
34429
  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."
34430
+ 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, when Class.name was asked, one called through another class of the project by name or through a parameter, local or class variable declared as one, 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
34431
  },
34247
34432
  limit: {
34248
34433
  type: "integer",
@@ -34334,7 +34519,7 @@ var TOOL_SPECS = [
34334
34519
  },
34335
34520
  {
34336
34521
  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.",
34522
+ 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. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. 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
34523
  parameters: {},
34339
34524
  requires: []
34340
34525
  },
@@ -34909,6 +35094,7 @@ var __dirname2 = dirname15(fileURLToPath4(import.meta.url));
34909
35094
  function editorRestartTimeoutMs() {
34910
35095
  return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
34911
35096
  }
35097
+ var EDITOR_SERVERS_START_MS = 20000;
34912
35098
  function waitFromEnv(variable, fallback) {
34913
35099
  const override = Number.parseInt(envValue(variable) ?? "", 10);
34914
35100
  return Number.isInteger(override) && override > 0 ? override : fallback;
@@ -34972,7 +35158,7 @@ function longerWaitNote(tool, op, waitedMs) {
34972
35158
  if (spec !== undefined && argumentsOf(spec, op).includes("timeoutMs")) {
34973
35159
  return "timeoutMs waits longer next time.";
34974
35160
  }
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";
35161
+ 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
35162
  return `${tool} ${op} takes no timeoutMs: it was given ${waitedMs}ms, and ${how}.`;
34977
35163
  }
34978
35164
  function projectSpelling(projectPath, absolutePath) {
@@ -35692,7 +35878,7 @@ function playedCap(held) {
35692
35878
  totalCapped: readBoolean(held, "totalCapped") === true
35693
35879
  };
35694
35880
  }
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.";
35881
+ 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 one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.";
35696
35882
  function projectSourceOf(projectPath) {
35697
35883
  const read = new Map;
35698
35884
  return (script) => {
@@ -38231,15 +38417,17 @@ class GodotServer {
38231
38417
  hold: current === null ? {} : await this.holdOf(current, active)
38232
38418
  };
38233
38419
  })();
38234
- const [games, { playing, active, run, hold }, scan, editorStatus] = await Promise.all([
38420
+ const [games, { playing, active, run, hold }, scan, editorStatus, servers] = await Promise.all([
38235
38421
  pinged,
38236
38422
  asked,
38237
38423
  this.editorScanState(),
38238
- this.getEditorStatusPayload()
38424
+ this.getEditorStatusPayload(),
38425
+ this.editorServersListening()
38239
38426
  ]);
38240
38427
  return this.jsonTextResponse({
38241
38428
  editor: {
38242
38429
  ...editorStatus,
38430
+ ...servers,
38243
38431
  debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
38244
38432
  scan
38245
38433
  },
@@ -38265,6 +38453,30 @@ class GodotServer {
38265
38453
  }
38266
38454
  });
38267
38455
  }
38456
+ async editorServersListening() {
38457
+ const status = this.godotBridge.getStatus();
38458
+ const { lspPort, dapPort } = status;
38459
+ if (!status.connected || lspPort === undefined || dapPort === undefined) {
38460
+ return {};
38461
+ }
38462
+ const [lsp, dap] = await Promise.all([acceptsConnections(lspPort), acceptsConnections(dapPort)]);
38463
+ const greetedAgoMs = status.connectedAt === undefined ? null : Date.now() - status.connectedAt.getTime();
38464
+ return {
38465
+ ...lsp === null ? {} : { lspListening: lsp },
38466
+ ...dap === null ? {} : { dapListening: dap },
38467
+ serversNote: notListeningNote({ lsp: { port: lspPort, listening: lsp }, dap: { port: dapPort, listening: dap } }, greetedAgoMs)
38468
+ };
38469
+ }
38470
+ async editorServersOnceStarted() {
38471
+ const deadline = Date.now() + EDITOR_SERVERS_START_MS;
38472
+ for (;; ) {
38473
+ const answer = await this.editorServersListening();
38474
+ if (answer.serversNote === undefined || Date.now() >= deadline) {
38475
+ return answer;
38476
+ }
38477
+ await delay4(500);
38478
+ }
38479
+ }
38268
38480
  settingKeysOf(projectPath) {
38269
38481
  if (projectPath === undefined) {
38270
38482
  return null;
@@ -38308,6 +38520,7 @@ class GodotServer {
38308
38520
  "editor_status says whether anything has reached the bridge since"
38309
38521
  ]);
38310
38522
  }
38523
+ const servers = await this.editorServersOnceStarted();
38311
38524
  const now = this.godotBridge.getStatus();
38312
38525
  const after = this.settingKeysOf(before.projectPath);
38313
38526
  return this.jsonTextResponse({
@@ -38318,6 +38531,7 @@ class GodotServer {
38318
38531
  addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
38319
38532
  staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()),
38320
38533
  ...this.whatTheEditorDropped(settingsBefore, after),
38534
+ ...servers,
38321
38535
  tookMs: Date.now() - began
38322
38536
  });
38323
38537
  }
@@ -38714,6 +38928,7 @@ class GodotServer {
38714
38928
  } catch (error) {
38715
38929
  return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
38716
38930
  `This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
38931
+ "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
38932
  "GDHARNESS_DAP_PORT points this server at another one"
38718
38933
  ]);
38719
38934
  }
package/build/keeper.js CHANGED
@@ -551,6 +551,8 @@ 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 DOTTED = `${NAME}(?:\\.${NAME})*`;
555
+ var MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
554
556
  var KEYWORDS = new Set([
555
557
  "and",
556
558
  "as",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.60",
3
+ "version": "1.1.62",
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",