gdharness 1.1.49 → 1.1.51
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 +194 -29
- package/build/index.js +194 -27
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -17843,10 +17843,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
17843
17843
|
},
|
|
17844
17844
|
{
|
|
17845
17845
|
name: "script_diagnostics",
|
|
17846
|
-
description: "Errors and warnings for a script from the editor's language server, and whether
|
|
17846
|
+
description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
17847
17847
|
parameters: {
|
|
17848
17848
|
projectPath: PROJECT_PATH,
|
|
17849
|
-
scriptPath:
|
|
17849
|
+
scriptPath: {
|
|
17850
|
+
type: ["string", "array"],
|
|
17851
|
+
items: { type: "string" },
|
|
17852
|
+
description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered as it always was; several are answered together under scripts, each in that same shape, with clean, errors and warnings for all of them beside it, and a script that could not be read is named there with why while the rest are still asked about. At most 300 scripts a call.'
|
|
17853
|
+
}
|
|
17850
17854
|
},
|
|
17851
17855
|
requires: ["projectPath", "scriptPath"]
|
|
17852
17856
|
},
|
|
@@ -17893,7 +17897,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17893
17897
|
},
|
|
17894
17898
|
{
|
|
17895
17899
|
name: "editor_launch",
|
|
17896
|
-
description: "Opens the Godot editor on a project, or restarts the one already connected. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
17900
|
+
description: "Opens the Godot editor on a project, or restarts the one already connected. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
17897
17901
|
parameters: {
|
|
17898
17902
|
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
17899
17903
|
hidden: {
|
|
@@ -18048,7 +18052,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18048
18052
|
},
|
|
18049
18053
|
{
|
|
18050
18054
|
name: "editor_status",
|
|
18051
|
-
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. 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.",
|
|
18055
|
+
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.",
|
|
18052
18056
|
parameters: {},
|
|
18053
18057
|
requires: []
|
|
18054
18058
|
},
|
|
@@ -39488,7 +39492,7 @@ class GodotLSPClient {
|
|
|
39488
39492
|
const diagnosticsPromise = new Promise((resolveDiagnostics, rejectDiagnostics) => {
|
|
39489
39493
|
const timer = setTimeout(() => {
|
|
39490
39494
|
this.diagnosticsWaiters.delete(key);
|
|
39491
|
-
rejectDiagnostics(new
|
|
39495
|
+
rejectDiagnostics(new NoDiagnosticsPublished(`Godot published no diagnostics for ${key} within ${DIAGNOSTICS_TIMEOUT_MS}ms. ` + "The language server may not be running, or may not have this file in its workspace."));
|
|
39492
39496
|
}, DIAGNOSTICS_TIMEOUT_MS);
|
|
39493
39497
|
this.diagnosticsWaiters.set(key, {
|
|
39494
39498
|
resolve: resolveDiagnostics,
|
|
@@ -39718,11 +39722,12 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
39718
39722
|
return asToolResponse({
|
|
39719
39723
|
error: normalizeLSPError(error, client.port),
|
|
39720
39724
|
...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
|
|
39721
|
-
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {}
|
|
39725
|
+
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {},
|
|
39726
|
+
...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {}
|
|
39722
39727
|
});
|
|
39723
39728
|
}
|
|
39724
39729
|
}
|
|
39725
|
-
var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, REQUEST_TIMEOUT_MS2 = 1e4, REFERENCES_TIMEOUT_MS = 180000, LSPTimeout, ArgumentRefusal, AnotherProjectsServer;
|
|
39730
|
+
var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, REQUEST_TIMEOUT_MS2 = 1e4, REFERENCES_TIMEOUT_MS = 180000, LSPTimeout, ArgumentRefusal, NoDiagnosticsPublished, AnotherProjectsServer;
|
|
39726
39731
|
var init_lsp_client = __esm(() => {
|
|
39727
39732
|
init_answer_json();
|
|
39728
39733
|
init_errors();
|
|
@@ -39740,6 +39745,8 @@ var init_lsp_client = __esm(() => {
|
|
|
39740
39745
|
};
|
|
39741
39746
|
ArgumentRefusal = class ArgumentRefusal extends Refusal {
|
|
39742
39747
|
};
|
|
39748
|
+
NoDiagnosticsPublished = class NoDiagnosticsPublished extends Error {
|
|
39749
|
+
};
|
|
39743
39750
|
AnotherProjectsServer = class AnotherProjectsServer extends Refusal {
|
|
39744
39751
|
serves;
|
|
39745
39752
|
constructor(port, serves, asked) {
|
|
@@ -42084,6 +42091,16 @@ function endedPreviousRun(ended) {
|
|
|
42084
42091
|
function previousRunLeft(ended) {
|
|
42085
42092
|
return (ended?.ending === "refused" || ended?.ending === "lingering") && ended.pid !== null ? ended.pid : undefined;
|
|
42086
42093
|
}
|
|
42094
|
+
function editorsOfProjectIn(tree, projectPath) {
|
|
42095
|
+
const found = [];
|
|
42096
|
+
for (const [pid, listed] of tree) {
|
|
42097
|
+
const read = readCommandLine(listed.command);
|
|
42098
|
+
if (read.editor && read.oneShot === null && read.projectPath !== null && /godot/i.test(read.executable) && !CONSOLE_WRAPPER.test(read.executable) && isSameDirectory(read.projectPath, projectPath)) {
|
|
42099
|
+
found.push(pid);
|
|
42100
|
+
}
|
|
42101
|
+
}
|
|
42102
|
+
return found.sort((a, b) => a - b);
|
|
42103
|
+
}
|
|
42087
42104
|
function engineNamesOf(executable) {
|
|
42088
42105
|
return CONSOLE_WRAPPER.test(executable) ? [executable, executable.replace(CONSOLE_WRAPPER, "$1")] : [executable];
|
|
42089
42106
|
}
|
|
@@ -42568,6 +42585,82 @@ function testPathsIn(projectPath, given) {
|
|
|
42568
42585
|
value: unique.filter((path) => !unique.some((other) => other !== path && within(path, other))).map((path) => `res://${path}`)
|
|
42569
42586
|
};
|
|
42570
42587
|
}
|
|
42588
|
+
function scriptsToDiagnose(projectPath, given) {
|
|
42589
|
+
const listed = Array.isArray(given);
|
|
42590
|
+
if (!listed) {
|
|
42591
|
+
const one = typeof given === "string" ? resolveWithinProject(projectPath, given) : null;
|
|
42592
|
+
if (one === null || !one.ok || !isDirectory(one.absolutePath)) {
|
|
42593
|
+
return { ok: true, value: null };
|
|
42594
|
+
}
|
|
42595
|
+
}
|
|
42596
|
+
const entries = listed ? given : [given];
|
|
42597
|
+
if (entries.length === 0) {
|
|
42598
|
+
return {
|
|
42599
|
+
ok: false,
|
|
42600
|
+
reason: "scriptPath is an empty list: name a script, a folder, or a list of either."
|
|
42601
|
+
};
|
|
42602
|
+
}
|
|
42603
|
+
const found = [];
|
|
42604
|
+
const empty = [];
|
|
42605
|
+
for (const [index, entry] of entries.entries()) {
|
|
42606
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
42607
|
+
return {
|
|
42608
|
+
ok: false,
|
|
42609
|
+
reason: `scriptPath${listed ? `[${index}]` : ""} is ${typeof entry === "string" ? "empty" : describeValue(entry)}, not a script or a folder in the project.`
|
|
42610
|
+
};
|
|
42611
|
+
}
|
|
42612
|
+
const location = resolveWithinProject(projectPath, entry);
|
|
42613
|
+
if (!location.ok) {
|
|
42614
|
+
return { ok: false, reason: location.reason };
|
|
42615
|
+
}
|
|
42616
|
+
const relative = location.relativePath.replace(/\/+$/, "");
|
|
42617
|
+
if (!isDirectory(location.absolutePath)) {
|
|
42618
|
+
found.push(relative);
|
|
42619
|
+
continue;
|
|
42620
|
+
}
|
|
42621
|
+
const inside = scriptsUnder(location.absolutePath, relative);
|
|
42622
|
+
if (inside.length === 0) {
|
|
42623
|
+
empty.push(entry);
|
|
42624
|
+
}
|
|
42625
|
+
found.push(...inside);
|
|
42626
|
+
}
|
|
42627
|
+
const unique = [...new Set(found)];
|
|
42628
|
+
if (unique.length === 0) {
|
|
42629
|
+
return { ok: false, reason: `No .gd script is under ${empty.join(" or ")}.` };
|
|
42630
|
+
}
|
|
42631
|
+
if (unique.length > MOST_DIAGNOSED) {
|
|
42632
|
+
return {
|
|
42633
|
+
ok: false,
|
|
42634
|
+
reason: `scriptPath names ${unique.length} scripts, and one call asks about at most ${MOST_DIAGNOSED}, each a round trip through the language server: name narrower folders, or call again for the rest.`
|
|
42635
|
+
};
|
|
42636
|
+
}
|
|
42637
|
+
return { ok: true, value: unique };
|
|
42638
|
+
}
|
|
42639
|
+
function isDirectory(path) {
|
|
42640
|
+
try {
|
|
42641
|
+
return statSync9(path).isDirectory();
|
|
42642
|
+
} catch {
|
|
42643
|
+
return false;
|
|
42644
|
+
}
|
|
42645
|
+
}
|
|
42646
|
+
function scriptsUnder(directory, prefix) {
|
|
42647
|
+
if (existsSync20(join26(directory, ".gdignore"))) {
|
|
42648
|
+
return [];
|
|
42649
|
+
}
|
|
42650
|
+
const found = [];
|
|
42651
|
+
for (const entry of readdirSync10(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
|
|
42652
|
+
if (entry.name.startsWith(".")) {
|
|
42653
|
+
continue;
|
|
42654
|
+
}
|
|
42655
|
+
const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
42656
|
+
if (entry.isDirectory()) {
|
|
42657
|
+
found.push(...scriptsUnder(join26(directory, entry.name), path));
|
|
42658
|
+
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
42659
|
+
found.push(path);
|
|
42660
|
+
}
|
|
42661
|
+
}
|
|
42662
|
+
return found;
|
|
42663
|
+
}
|
|
42571
42664
|
function hasMainScene(projectFile) {
|
|
42572
42665
|
const scene = parseProjectGodot(readFileSync22(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
42573
42666
|
return typeof scene === "string" && scene !== "";
|
|
@@ -44396,39 +44489,88 @@ class GodotServer {
|
|
|
44396
44489
|
}
|
|
44397
44490
|
}
|
|
44398
44491
|
async handleScriptDiagnostics(args) {
|
|
44492
|
+
const project = this.project(args);
|
|
44493
|
+
const scripts = project.ok ? scriptsToDiagnose(project.value.path, args["scriptPath"]) : null;
|
|
44494
|
+
if (scripts !== null && !scripts.ok) {
|
|
44495
|
+
return this.createErrorResponse(scripts.reason);
|
|
44496
|
+
}
|
|
44497
|
+
if (scripts?.value == null) {
|
|
44498
|
+
const one = await this.diagnosticsOf(args);
|
|
44499
|
+
return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
|
|
44500
|
+
}
|
|
44501
|
+
const progress = callProgress();
|
|
44502
|
+
const answers = [];
|
|
44503
|
+
let errors = 0;
|
|
44504
|
+
let warnings = 0;
|
|
44505
|
+
let unread = 0;
|
|
44506
|
+
for (const [at, scriptPath] of scripts.value.entries()) {
|
|
44507
|
+
progress?.(at + 1, `${at + 1} of ${scripts.value.length}: ${scriptPath}`);
|
|
44508
|
+
const one = await this.diagnosticsOf({ ...args, scriptPath });
|
|
44509
|
+
if ("refusal" in one) {
|
|
44510
|
+
if (!one.perScript) {
|
|
44511
|
+
return one.refusal;
|
|
44512
|
+
}
|
|
44513
|
+
unread += 1;
|
|
44514
|
+
answers.push({ scriptPath, error: one.refusal.content[0]?.text ?? "not read" });
|
|
44515
|
+
continue;
|
|
44516
|
+
}
|
|
44517
|
+
errors += Number(one.answer["errors"]);
|
|
44518
|
+
warnings += Number(one.answer["warnings"]);
|
|
44519
|
+
answers.push(one.answer);
|
|
44520
|
+
}
|
|
44521
|
+
return this.jsonTextResponse(this.markedIfStale({
|
|
44522
|
+
clean: errors === 0 && unread === 0,
|
|
44523
|
+
scriptsChecked: answers.length,
|
|
44524
|
+
errors,
|
|
44525
|
+
warnings,
|
|
44526
|
+
...unread > 0 ? { scriptsNotRead: unread } : {},
|
|
44527
|
+
scripts: answers
|
|
44528
|
+
}));
|
|
44529
|
+
}
|
|
44530
|
+
markedIfStale(answer) {
|
|
44531
|
+
return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
|
|
44532
|
+
}
|
|
44533
|
+
async diagnosticsOf(args) {
|
|
44399
44534
|
const answer = await this.handleLSP("lsp_get_diagnostics", args);
|
|
44400
44535
|
const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
44401
44536
|
if (payload["error"] !== undefined) {
|
|
44402
44537
|
const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
|
|
44403
44538
|
if (payload["refusedArguments"] === true) {
|
|
44404
|
-
return this.createErrorResponse(reason);
|
|
44539
|
+
return { refusal: this.createErrorResponse(reason), perScript: true };
|
|
44405
44540
|
}
|
|
44406
44541
|
if (typeof payload["serves"] === "string") {
|
|
44407
|
-
return
|
|
44408
|
-
|
|
44409
|
-
|
|
44410
|
-
|
|
44411
|
-
|
|
44542
|
+
return {
|
|
44543
|
+
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
44544
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
44545
|
+
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
44546
|
+
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
44547
|
+
]),
|
|
44548
|
+
perScript: false
|
|
44549
|
+
};
|
|
44412
44550
|
}
|
|
44413
|
-
return
|
|
44414
|
-
|
|
44415
|
-
|
|
44416
|
-
|
|
44551
|
+
return {
|
|
44552
|
+
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
44553
|
+
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
44554
|
+
"GDHARNESS_LSP_PORT points this server at another one"
|
|
44555
|
+
]),
|
|
44556
|
+
perScript: payload["aboutThisFile"] === true
|
|
44557
|
+
};
|
|
44417
44558
|
}
|
|
44418
44559
|
const diagnostics = readArray(payload, "diagnostics") ?? [];
|
|
44419
44560
|
const errors = diagnostics.filter((entry) => {
|
|
44420
44561
|
const severity = asParams(entry)["severity"];
|
|
44421
44562
|
return severity === 1 || severity === "error" || severity === "ERROR";
|
|
44422
44563
|
}).length;
|
|
44423
|
-
|
|
44424
|
-
|
|
44425
|
-
|
|
44426
|
-
|
|
44427
|
-
|
|
44428
|
-
|
|
44429
|
-
|
|
44430
|
-
|
|
44431
|
-
|
|
44564
|
+
return {
|
|
44565
|
+
answer: {
|
|
44566
|
+
scriptPath: readString(args, "scriptPath"),
|
|
44567
|
+
clean: errors === 0,
|
|
44568
|
+
errors,
|
|
44569
|
+
warnings: diagnostics.length - errors,
|
|
44570
|
+
diagnostics,
|
|
44571
|
+
...this.whereTheFileDisagrees(diagnostics, args)
|
|
44572
|
+
}
|
|
44573
|
+
};
|
|
44432
44574
|
}
|
|
44433
44575
|
whereTheFileDisagrees(diagnostics, args) {
|
|
44434
44576
|
const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
|
|
@@ -44646,14 +44788,16 @@ class GodotServer {
|
|
|
44646
44788
|
const greeted = hasSaidWhoItIs(status);
|
|
44647
44789
|
const stale = greeted && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
|
|
44648
44790
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
44791
|
+
const arriving = status.connected || this.ownProject === null ? [] : await this.editorsNotYetConnected(this.ownProject) ?? [];
|
|
44649
44792
|
return {
|
|
44650
44793
|
...status,
|
|
44651
44794
|
serverVersion: SERVER_VERSION,
|
|
44652
44795
|
addonIsStale: greeted ? stale : undefined,
|
|
44653
44796
|
greeting: status.connected && !greeted ? "The editor has connected and has not yet said who it is: its project, pid, addon version and ports arrive with its greeting a moment after the socket opens, later while it imports. Ask again for them." : undefined,
|
|
44654
44797
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
44655
|
-
mayYetConnect: status.connected ? undefined : unfinished !== null ? unfinished.underway : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
44656
|
-
|
|
44798
|
+
mayYetConnect: status.connected ? undefined : arriving.length > 0 ? true : unfinished !== null ? unfinished.underway : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
44799
|
+
editorsNotYetConnected: arriving.length > 0 ? arriving : undefined,
|
|
44800
|
+
restartInterrupted: unfinished === null || unfinished.underway || arriving.length > 0 ? undefined : {
|
|
44657
44801
|
quitEditorPid: unfinished.note.editorPid,
|
|
44658
44802
|
quitAt: unfinished.note.quitAt,
|
|
44659
44803
|
byServerPid: unfinished.note.byPid,
|
|
@@ -44950,6 +45094,14 @@ class GodotServer {
|
|
|
44950
45094
|
if (refused !== null) {
|
|
44951
45095
|
return refused;
|
|
44952
45096
|
}
|
|
45097
|
+
const arriving = await this.editorsNotYetConnected(project.value.path);
|
|
45098
|
+
if (arriving !== undefined && arriving.length > 0) {
|
|
45099
|
+
const named = arriving.map((one) => `pid ${one.pid}${one.startedAt === null ? "" : `, started ${one.startedAt}`}`).join("; ");
|
|
45100
|
+
return this.createErrorResponse(`An editor of this project is already running (${named}) and has not reached this server yet: an editor dials in once it is up, later while it imports, so opening another would put two editors on one project and one language server port.`, [
|
|
45101
|
+
"editor_status names it under editorsNotYetConnected and says when it has connected",
|
|
45102
|
+
`If it never connects it is not reaching this server: end ${arriving.length === 1 ? "that pid" : "those pids"} and open again`
|
|
45103
|
+
]);
|
|
45104
|
+
}
|
|
44953
45105
|
const engine = await this.engine();
|
|
44954
45106
|
if (!engine.ok) {
|
|
44955
45107
|
return engine.response;
|
|
@@ -44974,6 +45126,19 @@ class GodotServer {
|
|
|
44974
45126
|
settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
|
|
44975
45127
|
});
|
|
44976
45128
|
}
|
|
45129
|
+
async editorsNotYetConnected(projectPath) {
|
|
45130
|
+
const tree = await processTree();
|
|
45131
|
+
if (tree === undefined) {
|
|
45132
|
+
return;
|
|
45133
|
+
}
|
|
45134
|
+
const connected = this.godotBridge.isConnected() ? this.godotBridge.getStatus().editorPid : undefined;
|
|
45135
|
+
const pids = editorsOfProjectIn(tree, projectPath).filter((pid) => pid !== connected);
|
|
45136
|
+
const started = await startTimesOf(pids);
|
|
45137
|
+
return pids.map((pid) => {
|
|
45138
|
+
const at = started.get(pid) ?? tree.get(pid)?.startedAt;
|
|
45139
|
+
return { pid, startedAt: at === undefined ? null : new Date(at).toISOString() };
|
|
45140
|
+
});
|
|
45141
|
+
}
|
|
44977
45142
|
anEditorThatCannotConnect(projectPath) {
|
|
44978
45143
|
if (this.ownProject !== null && !isSameDirectory(this.ownProject, projectPath)) {
|
|
44979
45144
|
return this.createErrorResponse(`This server serves ${this.ownProject}, and an editor of ${projectPath} would dial that project's own server rather than this one.`, [
|
|
@@ -47238,7 +47403,7 @@ async function runGodotServer() {
|
|
|
47238
47403
|
const server = new GodotServer;
|
|
47239
47404
|
await server.run();
|
|
47240
47405
|
}
|
|
47241
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, 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, SCAN_WAIT_MS = 30000, 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/";
|
|
47406
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, 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, SCAN_WAIT_MS = 30000, 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/", MOST_DIAGNOSED = 300;
|
|
47242
47407
|
var init_server3 = __esm(() => {
|
|
47243
47408
|
init_mcp();
|
|
47244
47409
|
init_stdio2();
|
package/build/index.js
CHANGED
|
@@ -29729,6 +29729,9 @@ class LSPTimeout extends Error {
|
|
|
29729
29729
|
class ArgumentRefusal extends Refusal {
|
|
29730
29730
|
}
|
|
29731
29731
|
|
|
29732
|
+
class NoDiagnosticsPublished extends Error {
|
|
29733
|
+
}
|
|
29734
|
+
|
|
29732
29735
|
class AnotherProjectsServer extends Refusal {
|
|
29733
29736
|
serves;
|
|
29734
29737
|
constructor(port, serves, asked) {
|
|
@@ -30116,7 +30119,7 @@ class GodotLSPClient {
|
|
|
30116
30119
|
const diagnosticsPromise = new Promise((resolveDiagnostics, rejectDiagnostics) => {
|
|
30117
30120
|
const timer = setTimeout(() => {
|
|
30118
30121
|
this.diagnosticsWaiters.delete(key);
|
|
30119
|
-
rejectDiagnostics(new
|
|
30122
|
+
rejectDiagnostics(new NoDiagnosticsPublished(`Godot published no diagnostics for ${key} within ${DIAGNOSTICS_TIMEOUT_MS}ms. ` + "The language server may not be running, or may not have this file in its workspace."));
|
|
30120
30123
|
}, DIAGNOSTICS_TIMEOUT_MS);
|
|
30121
30124
|
this.diagnosticsWaiters.set(key, {
|
|
30122
30125
|
resolve: resolveDiagnostics,
|
|
@@ -30346,7 +30349,8 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
30346
30349
|
return asToolResponse({
|
|
30347
30350
|
error: normalizeLSPError(error, client.port),
|
|
30348
30351
|
...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
|
|
30349
|
-
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {}
|
|
30352
|
+
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {},
|
|
30353
|
+
...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {}
|
|
30350
30354
|
});
|
|
30351
30355
|
}
|
|
30352
30356
|
}
|
|
@@ -33520,10 +33524,14 @@ var TOOL_SPECS = [
|
|
|
33520
33524
|
},
|
|
33521
33525
|
{
|
|
33522
33526
|
name: "script_diagnostics",
|
|
33523
|
-
description: "Errors and warnings for a script from the editor's language server, and whether
|
|
33527
|
+
description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
33524
33528
|
parameters: {
|
|
33525
33529
|
projectPath: PROJECT_PATH,
|
|
33526
|
-
scriptPath:
|
|
33530
|
+
scriptPath: {
|
|
33531
|
+
type: ["string", "array"],
|
|
33532
|
+
items: { type: "string" },
|
|
33533
|
+
description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered as it always was; several are answered together under scripts, each in that same shape, with clean, errors and warnings for all of them beside it, and a script that could not be read is named there with why while the rest are still asked about. At most 300 scripts a call.'
|
|
33534
|
+
}
|
|
33527
33535
|
},
|
|
33528
33536
|
requires: ["projectPath", "scriptPath"]
|
|
33529
33537
|
},
|
|
@@ -33570,7 +33578,7 @@ var TOOL_SPECS = [
|
|
|
33570
33578
|
},
|
|
33571
33579
|
{
|
|
33572
33580
|
name: "editor_launch",
|
|
33573
|
-
description: "Opens the Godot editor on a project, or restarts the one already connected. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
33581
|
+
description: "Opens the Godot editor on a project, or restarts the one already connected. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
33574
33582
|
parameters: {
|
|
33575
33583
|
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
33576
33584
|
hidden: {
|
|
@@ -33725,7 +33733,7 @@ var TOOL_SPECS = [
|
|
|
33725
33733
|
},
|
|
33726
33734
|
{
|
|
33727
33735
|
name: "editor_status",
|
|
33728
|
-
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. 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.",
|
|
33736
|
+
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.",
|
|
33729
33737
|
parameters: {},
|
|
33730
33738
|
requires: []
|
|
33731
33739
|
},
|
|
@@ -34509,6 +34517,16 @@ function previousRunLeft(ended) {
|
|
|
34509
34517
|
return (ended?.ending === "refused" || ended?.ending === "lingering") && ended.pid !== null ? ended.pid : undefined;
|
|
34510
34518
|
}
|
|
34511
34519
|
var CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
34520
|
+
function editorsOfProjectIn(tree, projectPath) {
|
|
34521
|
+
const found = [];
|
|
34522
|
+
for (const [pid, listed] of tree) {
|
|
34523
|
+
const read = readCommandLine(listed.command);
|
|
34524
|
+
if (read.editor && read.oneShot === null && read.projectPath !== null && /godot/i.test(read.executable) && !CONSOLE_WRAPPER.test(read.executable) && isSameDirectory(read.projectPath, projectPath)) {
|
|
34525
|
+
found.push(pid);
|
|
34526
|
+
}
|
|
34527
|
+
}
|
|
34528
|
+
return found.sort((a, b) => a - b);
|
|
34529
|
+
}
|
|
34512
34530
|
function engineNamesOf(executable) {
|
|
34513
34531
|
return CONSOLE_WRAPPER.test(executable) ? [executable, executable.replace(CONSOLE_WRAPPER, "$1")] : [executable];
|
|
34514
34532
|
}
|
|
@@ -35054,6 +35072,83 @@ function testPathsIn(projectPath, given) {
|
|
|
35054
35072
|
value: unique.filter((path) => !unique.some((other) => other !== path && within(path, other))).map((path) => `res://${path}`)
|
|
35055
35073
|
};
|
|
35056
35074
|
}
|
|
35075
|
+
var MOST_DIAGNOSED = 300;
|
|
35076
|
+
function scriptsToDiagnose(projectPath, given) {
|
|
35077
|
+
const listed = Array.isArray(given);
|
|
35078
|
+
if (!listed) {
|
|
35079
|
+
const one = typeof given === "string" ? resolveWithinProject(projectPath, given) : null;
|
|
35080
|
+
if (one === null || !one.ok || !isDirectory(one.absolutePath)) {
|
|
35081
|
+
return { ok: true, value: null };
|
|
35082
|
+
}
|
|
35083
|
+
}
|
|
35084
|
+
const entries = listed ? given : [given];
|
|
35085
|
+
if (entries.length === 0) {
|
|
35086
|
+
return {
|
|
35087
|
+
ok: false,
|
|
35088
|
+
reason: "scriptPath is an empty list: name a script, a folder, or a list of either."
|
|
35089
|
+
};
|
|
35090
|
+
}
|
|
35091
|
+
const found = [];
|
|
35092
|
+
const empty = [];
|
|
35093
|
+
for (const [index, entry] of entries.entries()) {
|
|
35094
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
35095
|
+
return {
|
|
35096
|
+
ok: false,
|
|
35097
|
+
reason: `scriptPath${listed ? `[${index}]` : ""} is ${typeof entry === "string" ? "empty" : describeValue(entry)}, not a script or a folder in the project.`
|
|
35098
|
+
};
|
|
35099
|
+
}
|
|
35100
|
+
const location = resolveWithinProject(projectPath, entry);
|
|
35101
|
+
if (!location.ok) {
|
|
35102
|
+
return { ok: false, reason: location.reason };
|
|
35103
|
+
}
|
|
35104
|
+
const relative = location.relativePath.replace(/\/+$/, "");
|
|
35105
|
+
if (!isDirectory(location.absolutePath)) {
|
|
35106
|
+
found.push(relative);
|
|
35107
|
+
continue;
|
|
35108
|
+
}
|
|
35109
|
+
const inside = scriptsUnder(location.absolutePath, relative);
|
|
35110
|
+
if (inside.length === 0) {
|
|
35111
|
+
empty.push(entry);
|
|
35112
|
+
}
|
|
35113
|
+
found.push(...inside);
|
|
35114
|
+
}
|
|
35115
|
+
const unique = [...new Set(found)];
|
|
35116
|
+
if (unique.length === 0) {
|
|
35117
|
+
return { ok: false, reason: `No .gd script is under ${empty.join(" or ")}.` };
|
|
35118
|
+
}
|
|
35119
|
+
if (unique.length > MOST_DIAGNOSED) {
|
|
35120
|
+
return {
|
|
35121
|
+
ok: false,
|
|
35122
|
+
reason: `scriptPath names ${unique.length} scripts, and one call asks about at most ${MOST_DIAGNOSED}, each a round trip through the language server: name narrower folders, or call again for the rest.`
|
|
35123
|
+
};
|
|
35124
|
+
}
|
|
35125
|
+
return { ok: true, value: unique };
|
|
35126
|
+
}
|
|
35127
|
+
function isDirectory(path) {
|
|
35128
|
+
try {
|
|
35129
|
+
return statSync8(path).isDirectory();
|
|
35130
|
+
} catch {
|
|
35131
|
+
return false;
|
|
35132
|
+
}
|
|
35133
|
+
}
|
|
35134
|
+
function scriptsUnder(directory, prefix) {
|
|
35135
|
+
if (existsSync17(join22(directory, ".gdignore"))) {
|
|
35136
|
+
return [];
|
|
35137
|
+
}
|
|
35138
|
+
const found = [];
|
|
35139
|
+
for (const entry of readdirSync9(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
|
|
35140
|
+
if (entry.name.startsWith(".")) {
|
|
35141
|
+
continue;
|
|
35142
|
+
}
|
|
35143
|
+
const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
35144
|
+
if (entry.isDirectory()) {
|
|
35145
|
+
found.push(...scriptsUnder(join22(directory, entry.name), path));
|
|
35146
|
+
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
35147
|
+
found.push(path);
|
|
35148
|
+
}
|
|
35149
|
+
}
|
|
35150
|
+
return found;
|
|
35151
|
+
}
|
|
35057
35152
|
function hasMainScene(projectFile) {
|
|
35058
35153
|
const scene = parseProjectGodot(readFileSync20(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
35059
35154
|
return typeof scene === "string" && scene !== "";
|
|
@@ -36882,39 +36977,88 @@ class GodotServer {
|
|
|
36882
36977
|
}
|
|
36883
36978
|
}
|
|
36884
36979
|
async handleScriptDiagnostics(args) {
|
|
36980
|
+
const project = this.project(args);
|
|
36981
|
+
const scripts = project.ok ? scriptsToDiagnose(project.value.path, args["scriptPath"]) : null;
|
|
36982
|
+
if (scripts !== null && !scripts.ok) {
|
|
36983
|
+
return this.createErrorResponse(scripts.reason);
|
|
36984
|
+
}
|
|
36985
|
+
if (scripts?.value == null) {
|
|
36986
|
+
const one = await this.diagnosticsOf(args);
|
|
36987
|
+
return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
|
|
36988
|
+
}
|
|
36989
|
+
const progress = callProgress();
|
|
36990
|
+
const answers = [];
|
|
36991
|
+
let errors = 0;
|
|
36992
|
+
let warnings = 0;
|
|
36993
|
+
let unread = 0;
|
|
36994
|
+
for (const [at, scriptPath] of scripts.value.entries()) {
|
|
36995
|
+
progress?.(at + 1, `${at + 1} of ${scripts.value.length}: ${scriptPath}`);
|
|
36996
|
+
const one = await this.diagnosticsOf({ ...args, scriptPath });
|
|
36997
|
+
if ("refusal" in one) {
|
|
36998
|
+
if (!one.perScript) {
|
|
36999
|
+
return one.refusal;
|
|
37000
|
+
}
|
|
37001
|
+
unread += 1;
|
|
37002
|
+
answers.push({ scriptPath, error: one.refusal.content[0]?.text ?? "not read" });
|
|
37003
|
+
continue;
|
|
37004
|
+
}
|
|
37005
|
+
errors += Number(one.answer["errors"]);
|
|
37006
|
+
warnings += Number(one.answer["warnings"]);
|
|
37007
|
+
answers.push(one.answer);
|
|
37008
|
+
}
|
|
37009
|
+
return this.jsonTextResponse(this.markedIfStale({
|
|
37010
|
+
clean: errors === 0 && unread === 0,
|
|
37011
|
+
scriptsChecked: answers.length,
|
|
37012
|
+
errors,
|
|
37013
|
+
warnings,
|
|
37014
|
+
...unread > 0 ? { scriptsNotRead: unread } : {},
|
|
37015
|
+
scripts: answers
|
|
37016
|
+
}));
|
|
37017
|
+
}
|
|
37018
|
+
markedIfStale(answer) {
|
|
37019
|
+
return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
|
|
37020
|
+
}
|
|
37021
|
+
async diagnosticsOf(args) {
|
|
36885
37022
|
const answer = await this.handleLSP("lsp_get_diagnostics", args);
|
|
36886
37023
|
const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
36887
37024
|
if (payload["error"] !== undefined) {
|
|
36888
37025
|
const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
|
|
36889
37026
|
if (payload["refusedArguments"] === true) {
|
|
36890
|
-
return this.createErrorResponse(reason);
|
|
37027
|
+
return { refusal: this.createErrorResponse(reason), perScript: true };
|
|
36891
37028
|
}
|
|
36892
37029
|
if (typeof payload["serves"] === "string") {
|
|
36893
|
-
return
|
|
36894
|
-
|
|
36895
|
-
|
|
36896
|
-
|
|
36897
|
-
|
|
37030
|
+
return {
|
|
37031
|
+
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
37032
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
37033
|
+
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
37034
|
+
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
37035
|
+
]),
|
|
37036
|
+
perScript: false
|
|
37037
|
+
};
|
|
36898
37038
|
}
|
|
36899
|
-
return
|
|
36900
|
-
|
|
36901
|
-
|
|
36902
|
-
|
|
37039
|
+
return {
|
|
37040
|
+
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
37041
|
+
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
37042
|
+
"GDHARNESS_LSP_PORT points this server at another one"
|
|
37043
|
+
]),
|
|
37044
|
+
perScript: payload["aboutThisFile"] === true
|
|
37045
|
+
};
|
|
36903
37046
|
}
|
|
36904
37047
|
const diagnostics = readArray(payload, "diagnostics") ?? [];
|
|
36905
37048
|
const errors = diagnostics.filter((entry) => {
|
|
36906
37049
|
const severity = asParams(entry)["severity"];
|
|
36907
37050
|
return severity === 1 || severity === "error" || severity === "ERROR";
|
|
36908
37051
|
}).length;
|
|
36909
|
-
|
|
36910
|
-
|
|
36911
|
-
|
|
36912
|
-
|
|
36913
|
-
|
|
36914
|
-
|
|
36915
|
-
|
|
36916
|
-
|
|
36917
|
-
|
|
37052
|
+
return {
|
|
37053
|
+
answer: {
|
|
37054
|
+
scriptPath: readString(args, "scriptPath"),
|
|
37055
|
+
clean: errors === 0,
|
|
37056
|
+
errors,
|
|
37057
|
+
warnings: diagnostics.length - errors,
|
|
37058
|
+
diagnostics,
|
|
37059
|
+
...this.whereTheFileDisagrees(diagnostics, args)
|
|
37060
|
+
}
|
|
37061
|
+
};
|
|
36918
37062
|
}
|
|
36919
37063
|
whereTheFileDisagrees(diagnostics, args) {
|
|
36920
37064
|
const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
|
|
@@ -37132,14 +37276,16 @@ class GodotServer {
|
|
|
37132
37276
|
const greeted = hasSaidWhoItIs(status);
|
|
37133
37277
|
const stale = greeted && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
|
|
37134
37278
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
37279
|
+
const arriving = status.connected || this.ownProject === null ? [] : await this.editorsNotYetConnected(this.ownProject) ?? [];
|
|
37135
37280
|
return {
|
|
37136
37281
|
...status,
|
|
37137
37282
|
serverVersion: SERVER_VERSION,
|
|
37138
37283
|
addonIsStale: greeted ? stale : undefined,
|
|
37139
37284
|
greeting: status.connected && !greeted ? "The editor has connected and has not yet said who it is: its project, pid, addon version and ports arrive with its greeting a moment after the socket opens, later while it imports. Ask again for them." : undefined,
|
|
37140
37285
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
37141
|
-
mayYetConnect: status.connected ? undefined : unfinished !== null ? unfinished.underway : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
37142
|
-
|
|
37286
|
+
mayYetConnect: status.connected ? undefined : arriving.length > 0 ? true : unfinished !== null ? unfinished.underway : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
37287
|
+
editorsNotYetConnected: arriving.length > 0 ? arriving : undefined,
|
|
37288
|
+
restartInterrupted: unfinished === null || unfinished.underway || arriving.length > 0 ? undefined : {
|
|
37143
37289
|
quitEditorPid: unfinished.note.editorPid,
|
|
37144
37290
|
quitAt: unfinished.note.quitAt,
|
|
37145
37291
|
byServerPid: unfinished.note.byPid,
|
|
@@ -37436,6 +37582,14 @@ class GodotServer {
|
|
|
37436
37582
|
if (refused !== null) {
|
|
37437
37583
|
return refused;
|
|
37438
37584
|
}
|
|
37585
|
+
const arriving = await this.editorsNotYetConnected(project.value.path);
|
|
37586
|
+
if (arriving !== undefined && arriving.length > 0) {
|
|
37587
|
+
const named = arriving.map((one) => `pid ${one.pid}${one.startedAt === null ? "" : `, started ${one.startedAt}`}`).join("; ");
|
|
37588
|
+
return this.createErrorResponse(`An editor of this project is already running (${named}) and has not reached this server yet: an editor dials in once it is up, later while it imports, so opening another would put two editors on one project and one language server port.`, [
|
|
37589
|
+
"editor_status names it under editorsNotYetConnected and says when it has connected",
|
|
37590
|
+
`If it never connects it is not reaching this server: end ${arriving.length === 1 ? "that pid" : "those pids"} and open again`
|
|
37591
|
+
]);
|
|
37592
|
+
}
|
|
37439
37593
|
const engine = await this.engine();
|
|
37440
37594
|
if (!engine.ok) {
|
|
37441
37595
|
return engine.response;
|
|
@@ -37460,6 +37614,19 @@ class GodotServer {
|
|
|
37460
37614
|
settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
|
|
37461
37615
|
});
|
|
37462
37616
|
}
|
|
37617
|
+
async editorsNotYetConnected(projectPath) {
|
|
37618
|
+
const tree = await processTree();
|
|
37619
|
+
if (tree === undefined) {
|
|
37620
|
+
return;
|
|
37621
|
+
}
|
|
37622
|
+
const connected = this.godotBridge.isConnected() ? this.godotBridge.getStatus().editorPid : undefined;
|
|
37623
|
+
const pids = editorsOfProjectIn(tree, projectPath).filter((pid) => pid !== connected);
|
|
37624
|
+
const started = await startTimesOf(pids);
|
|
37625
|
+
return pids.map((pid) => {
|
|
37626
|
+
const at = started.get(pid) ?? tree.get(pid)?.startedAt;
|
|
37627
|
+
return { pid, startedAt: at === undefined ? null : new Date(at).toISOString() };
|
|
37628
|
+
});
|
|
37629
|
+
}
|
|
37463
37630
|
anEditorThatCannotConnect(projectPath) {
|
|
37464
37631
|
if (this.ownProject !== null && !isSameDirectory(this.ownProject, projectPath)) {
|
|
37465
37632
|
return this.createErrorResponse(`This server serves ${this.ownProject}, and an editor of ${projectPath} would dial that project's own server rather than this one.`, [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.51",
|
|
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",
|