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 +198 -10
- package/build/index.js +196 -9
- package/build/keeper.js +1 -0
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -9242,11 +9242,116 @@ function bodyCallsSuper(code, line, lambda) {
|
|
|
9242
9242
|
}
|
|
9243
9243
|
return false;
|
|
9244
9244
|
}
|
|
9245
|
-
function
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|