gdharness 0.13.27 → 0.13.29
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 +51 -16
- package/build/index.js +51 -16
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7626,7 +7626,14 @@ function staleAnalysisNote(contradicted, dependsOn) {
|
|
|
7626
7626
|
const types = new Set(contradicted.map((one) => one.type));
|
|
7627
7627
|
const declaring = [...new Set(contradicted.map((one) => one.declaredIn))].sort();
|
|
7628
7628
|
const lever = dependsOn.length === 0 ? ", and the named types depend on no other global class, so that lever is not available here. " + "That is the common case rather than the awkward one: an edit near a type that depends on " + "something refreshes it before the fault is noticed, which is why a type with no " + "dependencies is where this turns up" : `, so changing ${[...dependsOn].sort().join(", ")} and rescanning is worth a try`;
|
|
7629
|
-
return `The editor is reporting against an older copy of ${types.size === 1 ? "a type" : "some types"} ` + "named under contradictedByTheFile. Each member listed is declared in the file the class cache " + "points at, so those diagnostics are wrong however the code is written. Run editor_rescan first: " + "it costs about half a second, it cleared this in
|
|
7629
|
+
return `The editor is reporting against an older copy of ${types.size === 1 ? "a type" : "some types"} ` + "named under contradictedByTheFile. Each member listed is declared in the file the class cache " + "points at, so those diagnostics are wrong however the code is written. Run editor_rescan first: " + "it costs about half a second, and it has cleared this in every reproduction measured since the " + "scan timing was fixed: both in one project, the scan returning in 243ms and 275ms, and one in " + "another where the same call also rebuilt the copy. " + "The fault behind it is that a script the editor has loaded keeps the copy it built, and nothing " + `rebuilds that copy when the file changes; ${declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]} recompiles that copy` : `reloadScript takes one script, so a call each for ${declaring.join(" and ")} recompiles those copies`} from the file into the same ` + "object and answers with the members it has afterwards under reloadedMethods. Read that and the " + "next diagnostics as two readings, because they are of two things: measured in one window on " + "one editor, the analyser resolved a call to a newly added method while the built copy of that " + "same script did not have it. So the reload is worth doing and is not what clears these; the " + "scan is. The other lever measured is that a held type is refreshed when something it " + `depends on changes rather than when it changes itself${lever}. editor_launch restart has always ` + "worked and costs a window. project_import refresh_classes does not, and answers added: [] while " + "this is happening.";
|
|
7630
|
+
}
|
|
7631
|
+
function uncachedClassNote(types) {
|
|
7632
|
+
if (types.length === 0) {
|
|
7633
|
+
return "";
|
|
7634
|
+
}
|
|
7635
|
+
const one = types.length === 1;
|
|
7636
|
+
return `${types.join(", ")} ${one ? "is declared" : "are declared"} in this project and missing from ` + `.godot/global_script_class_cache.cfg, so a game launched now would not resolve ${one ? "it" : "them"} ` + "either. Try editor_rescan first: it asks the editor already running and starts nothing, and it " + "cleared this in 314ms in a second project with a 31-worker fan-out still importing. It is not " + "always enough, because a file another engine has already imported reads as settled and the " + "walk does not look inside it. project_import refresh_classes rewrites the cache from the " + "declarations on disk and does reach that case, at the cost of a short headless engine, which " + "is worth knowing when something else is already running against this project.";
|
|
7630
7637
|
}
|
|
7631
7638
|
function cacheWrittenAt(projectPath) {
|
|
7632
7639
|
const cache = join6(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
@@ -15037,6 +15044,16 @@ function settingKeys(content) {
|
|
|
15037
15044
|
}
|
|
15038
15045
|
return named;
|
|
15039
15046
|
}
|
|
15047
|
+
function settingsDroppedReport(before, after) {
|
|
15048
|
+
const dropped = [...before].filter(([key]) => !after.has(key));
|
|
15049
|
+
if (dropped.length === 0) {
|
|
15050
|
+
return {};
|
|
15051
|
+
}
|
|
15052
|
+
return {
|
|
15053
|
+
settingsDropped: dropped.map(([setting, was]) => ({ setting, was })),
|
|
15054
|
+
settingsNote: `The editor saved project.godot and these keys are no longer in it, with the value each one held. Godot writes only what differs from its own defaults, so a key named deliberately at its default value is redundant to the editor and is dropped on save; it will be dropped again the next time one opens. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, project_settings set puts one back: ${dropped.map(([setting, was]) => `setting "${setting}" value ${JSON.stringify(was)}`).join(", ")}. Nothing else will say they have gone until something depends on one.`
|
|
15055
|
+
};
|
|
15056
|
+
}
|
|
15040
15057
|
function parseProjectGodot(content) {
|
|
15041
15058
|
const result = emptyRecord();
|
|
15042
15059
|
let section = emptyRecord();
|
|
@@ -15978,7 +15995,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15978
15995
|
},
|
|
15979
15996
|
{
|
|
15980
15997
|
name: "editor_status",
|
|
15981
|
-
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. 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. 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.',
|
|
15998
|
+
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. 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. 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.',
|
|
15982
15999
|
parameters: {},
|
|
15983
16000
|
requires: []
|
|
15984
16001
|
},
|
|
@@ -31401,7 +31418,7 @@ class GodotDAPClient {
|
|
|
31401
31418
|
socket.once("error", handleError);
|
|
31402
31419
|
});
|
|
31403
31420
|
}
|
|
31404
|
-
async
|
|
31421
|
+
async abandon() {
|
|
31405
31422
|
if (!this.socket) {
|
|
31406
31423
|
this.connected = false;
|
|
31407
31424
|
this.initialized = false;
|
|
@@ -31409,11 +31426,6 @@ class GodotDAPClient {
|
|
|
31409
31426
|
this.halt = null;
|
|
31410
31427
|
return;
|
|
31411
31428
|
}
|
|
31412
|
-
if (this.connected) {
|
|
31413
|
-
try {
|
|
31414
|
-
await this.sendRequest("disconnect", { restart: false });
|
|
31415
|
-
} catch {}
|
|
31416
|
-
}
|
|
31417
31429
|
await new Promise((resolve) => {
|
|
31418
31430
|
const socket = this.socket;
|
|
31419
31431
|
if (!socket) {
|
|
@@ -34715,6 +34727,9 @@ function servedPort(said) {
|
|
|
34715
34727
|
function mayYetConnect(listeningSince, now = Date.now()) {
|
|
34716
34728
|
return listeningSince === undefined || now - listeningSince.getTime() < CONNECT_WINDOW_MS;
|
|
34717
34729
|
}
|
|
34730
|
+
function anEditorIsStillComing(listeningSince, launchedEditorIsAlive, now = Date.now()) {
|
|
34731
|
+
return mayYetConnect(listeningSince, now) || launchedEditorIsAlive;
|
|
34732
|
+
}
|
|
34718
34733
|
function getDefaultBridge(ownProject = null) {
|
|
34719
34734
|
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
|
|
34720
34735
|
return defaultBridge;
|
|
@@ -36998,6 +37013,8 @@ class GodotServer {
|
|
|
36998
37013
|
successorWatch = null;
|
|
36999
37014
|
lastProjectPath = null;
|
|
37000
37015
|
shutdownInitiated = false;
|
|
37016
|
+
launchedEditor = null;
|
|
37017
|
+
launchedFrom = null;
|
|
37001
37018
|
ownProject;
|
|
37002
37019
|
announcedAt = null;
|
|
37003
37020
|
constructor() {
|
|
@@ -37151,9 +37168,9 @@ class GodotServer {
|
|
|
37151
37168
|
}
|
|
37152
37169
|
if (this.dapClient) {
|
|
37153
37170
|
try {
|
|
37154
|
-
await this.dapClient.
|
|
37171
|
+
await this.dapClient.abandon();
|
|
37155
37172
|
} catch (error) {
|
|
37156
|
-
this.logDebug(`DAP client did not
|
|
37173
|
+
this.logDebug(`DAP client did not let go cleanly: ${errorMessage(error)}`);
|
|
37157
37174
|
}
|
|
37158
37175
|
this.dapClient = null;
|
|
37159
37176
|
}
|
|
@@ -38157,7 +38174,7 @@ class GodotServer {
|
|
|
38157
38174
|
}
|
|
38158
38175
|
const uncached = unloaded.filter((type) => !type.inTheClassCache);
|
|
38159
38176
|
if (uncached.length > 0) {
|
|
38160
|
-
notes.push(
|
|
38177
|
+
notes.push(uncachedClassNote(uncached.map((type) => type.type)));
|
|
38161
38178
|
}
|
|
38162
38179
|
const stale = unloaded.filter((type) => type.inTheClassCache);
|
|
38163
38180
|
if (stale.length > 0) {
|
|
@@ -38205,6 +38222,9 @@ class GodotServer {
|
|
|
38205
38222
|
}
|
|
38206
38223
|
return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
|
|
38207
38224
|
}
|
|
38225
|
+
anEditorIsStillComing(listeningSince) {
|
|
38226
|
+
return anEditorIsStillComing(listeningSince, this.launchedEditor !== null && alive(this.launchedEditor));
|
|
38227
|
+
}
|
|
38208
38228
|
dap() {
|
|
38209
38229
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
38210
38230
|
if (this.dapClient !== null && this.dapClient.port !== port) {
|
|
@@ -38248,7 +38268,9 @@ class GodotServer {
|
|
|
38248
38268
|
serverVersion: SERVER_VERSION,
|
|
38249
38269
|
addonIsStale: status.connected ? stale : undefined,
|
|
38250
38270
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
38251
|
-
mayYetConnect: status.connected ? undefined :
|
|
38271
|
+
mayYetConnect: status.connected ? undefined : this.anEditorIsStillComing(status.listeningSince),
|
|
38272
|
+
awaitingLaunchedEditor: status.connected || this.launchedEditor === null || !alive(this.launchedEditor) ? undefined : this.launchedEditor,
|
|
38273
|
+
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
38252
38274
|
startupError: this.bridgeStartupError,
|
|
38253
38275
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
38254
38276
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
@@ -38345,7 +38367,6 @@ class GodotServer {
|
|
|
38345
38367
|
}
|
|
38346
38368
|
const now = this.godotBridge.getStatus();
|
|
38347
38369
|
const after = this.settingKeysOf(before.projectPath);
|
|
38348
|
-
const dropped = [...settingsBefore].filter(([key]) => !after.has(key));
|
|
38349
38370
|
return this.jsonTextResponse({
|
|
38350
38371
|
restarted: true,
|
|
38351
38372
|
editorPid: now.editorPid,
|
|
@@ -38353,11 +38374,21 @@ class GodotServer {
|
|
|
38353
38374
|
serverVersion: SERVER_VERSION,
|
|
38354
38375
|
addonIsStale: now.addonVersion !== SERVER_VERSION,
|
|
38355
38376
|
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
|
|
38356
|
-
|
|
38357
|
-
settingsNote: dropped.length > 0 ? `The editor saved project.godot on its way out and these keys are no longer in it, with the value each one held. Godot writes only what differs from its own defaults, so a key named deliberately at its default value is redundant to the editor and is dropped on save; it will be dropped again on the next restart. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, project_settings set puts one back: ${dropped.map(([setting, was]) => `setting "${setting}" value ${JSON.stringify(was)}`).join(", ")}. Nothing else will say they have gone until something depends on one.` : undefined,
|
|
38377
|
+
...this.whatTheEditorDropped(settingsBefore, after),
|
|
38358
38378
|
tookMs: Date.now() - began
|
|
38359
38379
|
});
|
|
38360
38380
|
}
|
|
38381
|
+
whatTheLaunchedEditorDropped() {
|
|
38382
|
+
const watched = this.launchedFrom;
|
|
38383
|
+
if (watched === null) {
|
|
38384
|
+
return {};
|
|
38385
|
+
}
|
|
38386
|
+
this.launchedFrom = null;
|
|
38387
|
+
return this.whatTheEditorDropped(watched.before, this.settingKeysOf(watched.projectPath));
|
|
38388
|
+
}
|
|
38389
|
+
whatTheEditorDropped(before, after) {
|
|
38390
|
+
return settingsDroppedReport(before, after);
|
|
38391
|
+
}
|
|
38361
38392
|
async startItAgain(projectPath, editorPid) {
|
|
38362
38393
|
const engine = await this.engine();
|
|
38363
38394
|
if (!engine.ok) {
|
|
@@ -38407,16 +38438,19 @@ class GodotServer {
|
|
|
38407
38438
|
}
|
|
38408
38439
|
this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
|
|
38409
38440
|
const ports = await this.portsForAnEditor();
|
|
38441
|
+
const before = this.settingKeysOf(project.value.path);
|
|
38410
38442
|
const opened = await this.openAnEditor(engine.value, project.value.path, ports);
|
|
38411
38443
|
if (opened.error !== null) {
|
|
38412
38444
|
return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
|
|
38413
38445
|
}
|
|
38446
|
+
this.launchedFrom = { projectPath: project.value.path, before };
|
|
38414
38447
|
return this.jsonTextResponse({
|
|
38415
38448
|
launched: true,
|
|
38416
38449
|
pid: opened.pid,
|
|
38417
38450
|
projectPath: project.value.path,
|
|
38418
38451
|
lspPort: ports.lsp,
|
|
38419
|
-
dapPort: ports.dap
|
|
38452
|
+
dapPort: ports.dap,
|
|
38453
|
+
settingsNote: 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."
|
|
38420
38454
|
});
|
|
38421
38455
|
}
|
|
38422
38456
|
async openAnEditor(engine, projectPath, ports) {
|
|
@@ -38443,6 +38477,7 @@ class GodotServer {
|
|
|
38443
38477
|
return { pid: null, error: started };
|
|
38444
38478
|
}
|
|
38445
38479
|
editor.unref();
|
|
38480
|
+
this.launchedEditor = editor.pid ?? null;
|
|
38446
38481
|
return { pid: editor.pid ?? null, error: null };
|
|
38447
38482
|
}
|
|
38448
38483
|
async portsForAnEditor() {
|
package/build/index.js
CHANGED
|
@@ -24886,7 +24886,14 @@ function staleAnalysisNote(contradicted, dependsOn) {
|
|
|
24886
24886
|
const types = new Set(contradicted.map((one) => one.type));
|
|
24887
24887
|
const declaring = [...new Set(contradicted.map((one) => one.declaredIn))].sort();
|
|
24888
24888
|
const lever = dependsOn.length === 0 ? ", and the named types depend on no other global class, so that lever is not available here. " + "That is the common case rather than the awkward one: an edit near a type that depends on " + "something refreshes it before the fault is noticed, which is why a type with no " + "dependencies is where this turns up" : `, so changing ${[...dependsOn].sort().join(", ")} and rescanning is worth a try`;
|
|
24889
|
-
return `The editor is reporting against an older copy of ${types.size === 1 ? "a type" : "some types"} ` + "named under contradictedByTheFile. Each member listed is declared in the file the class cache " + "points at, so those diagnostics are wrong however the code is written. Run editor_rescan first: " + "it costs about half a second, it cleared this in
|
|
24889
|
+
return `The editor is reporting against an older copy of ${types.size === 1 ? "a type" : "some types"} ` + "named under contradictedByTheFile. Each member listed is declared in the file the class cache " + "points at, so those diagnostics are wrong however the code is written. Run editor_rescan first: " + "it costs about half a second, and it has cleared this in every reproduction measured since the " + "scan timing was fixed: both in one project, the scan returning in 243ms and 275ms, and one in " + "another where the same call also rebuilt the copy. " + "The fault behind it is that a script the editor has loaded keeps the copy it built, and nothing " + `rebuilds that copy when the file changes; ${declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]} recompiles that copy` : `reloadScript takes one script, so a call each for ${declaring.join(" and ")} recompiles those copies`} from the file into the same ` + "object and answers with the members it has afterwards under reloadedMethods. Read that and the " + "next diagnostics as two readings, because they are of two things: measured in one window on " + "one editor, the analyser resolved a call to a newly added method while the built copy of that " + "same script did not have it. So the reload is worth doing and is not what clears these; the " + "scan is. The other lever measured is that a held type is refreshed when something it " + `depends on changes rather than when it changes itself${lever}. editor_launch restart has always ` + "worked and costs a window. project_import refresh_classes does not, and answers added: [] while " + "this is happening.";
|
|
24890
|
+
}
|
|
24891
|
+
function uncachedClassNote(types) {
|
|
24892
|
+
if (types.length === 0) {
|
|
24893
|
+
return "";
|
|
24894
|
+
}
|
|
24895
|
+
const one = types.length === 1;
|
|
24896
|
+
return `${types.join(", ")} ${one ? "is declared" : "are declared"} in this project and missing from ` + `.godot/global_script_class_cache.cfg, so a game launched now would not resolve ${one ? "it" : "them"} ` + "either. Try editor_rescan first: it asks the editor already running and starts nothing, and it " + "cleared this in 314ms in a second project with a 31-worker fan-out still importing. It is not " + "always enough, because a file another engine has already imported reads as settled and the " + "walk does not look inside it. project_import refresh_classes rewrites the cache from the " + "declarations on disk and does reach that case, at the cost of a short headless engine, which " + "is worth knowing when something else is already running against this project.";
|
|
24890
24897
|
}
|
|
24891
24898
|
function cacheWrittenAt(projectPath) {
|
|
24892
24899
|
const cache = join3(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
@@ -25153,7 +25160,7 @@ class GodotDAPClient {
|
|
|
25153
25160
|
socket.once("error", handleError);
|
|
25154
25161
|
});
|
|
25155
25162
|
}
|
|
25156
|
-
async
|
|
25163
|
+
async abandon() {
|
|
25157
25164
|
if (!this.socket) {
|
|
25158
25165
|
this.connected = false;
|
|
25159
25166
|
this.initialized = false;
|
|
@@ -25161,11 +25168,6 @@ class GodotDAPClient {
|
|
|
25161
25168
|
this.halt = null;
|
|
25162
25169
|
return;
|
|
25163
25170
|
}
|
|
25164
|
-
if (this.connected) {
|
|
25165
|
-
try {
|
|
25166
|
-
await this.sendRequest("disconnect", { restart: false });
|
|
25167
|
-
} catch {}
|
|
25168
|
-
}
|
|
25169
25171
|
await new Promise((resolve) => {
|
|
25170
25172
|
const socket = this.socket;
|
|
25171
25173
|
if (!socket) {
|
|
@@ -25754,6 +25756,9 @@ var CONNECT_WINDOW_MS = 35000;
|
|
|
25754
25756
|
function mayYetConnect(listeningSince, now = Date.now()) {
|
|
25755
25757
|
return listeningSince === undefined || now - listeningSince.getTime() < CONNECT_WINDOW_MS;
|
|
25756
25758
|
}
|
|
25759
|
+
function anEditorIsStillComing(listeningSince, launchedEditorIsAlive, now = Date.now()) {
|
|
25760
|
+
return mayYetConnect(listeningSince, now) || launchedEditorIsAlive;
|
|
25761
|
+
}
|
|
25757
25762
|
|
|
25758
25763
|
class GodotBridge extends EventEmitter {
|
|
25759
25764
|
listeningSince = null;
|
|
@@ -27725,6 +27730,16 @@ function settingKeys(content) {
|
|
|
27725
27730
|
}
|
|
27726
27731
|
return named;
|
|
27727
27732
|
}
|
|
27733
|
+
function settingsDroppedReport(before, after) {
|
|
27734
|
+
const dropped = [...before].filter(([key]) => !after.has(key));
|
|
27735
|
+
if (dropped.length === 0) {
|
|
27736
|
+
return {};
|
|
27737
|
+
}
|
|
27738
|
+
return {
|
|
27739
|
+
settingsDropped: dropped.map(([setting, was]) => ({ setting, was })),
|
|
27740
|
+
settingsNote: `The editor saved project.godot and these keys are no longer in it, with the value each one held. Godot writes only what differs from its own defaults, so a key named deliberately at its default value is redundant to the editor and is dropped on save; it will be dropped again the next time one opens. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, project_settings set puts one back: ${dropped.map(([setting, was]) => `setting "${setting}" value ${JSON.stringify(was)}`).join(", ")}. Nothing else will say they have gone until something depends on one.`
|
|
27741
|
+
};
|
|
27742
|
+
}
|
|
27728
27743
|
function parseProjectGodot(content) {
|
|
27729
27744
|
const result = emptyRecord();
|
|
27730
27745
|
let section = emptyRecord();
|
|
@@ -29085,7 +29100,7 @@ var TOOL_SPECS = [
|
|
|
29085
29100
|
},
|
|
29086
29101
|
{
|
|
29087
29102
|
name: "editor_status",
|
|
29088
|
-
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. 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. 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.',
|
|
29103
|
+
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. 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. 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.',
|
|
29089
29104
|
parameters: {},
|
|
29090
29105
|
requires: []
|
|
29091
29106
|
},
|
|
@@ -29740,6 +29755,8 @@ class GodotServer {
|
|
|
29740
29755
|
successorWatch = null;
|
|
29741
29756
|
lastProjectPath = null;
|
|
29742
29757
|
shutdownInitiated = false;
|
|
29758
|
+
launchedEditor = null;
|
|
29759
|
+
launchedFrom = null;
|
|
29743
29760
|
ownProject;
|
|
29744
29761
|
announcedAt = null;
|
|
29745
29762
|
constructor() {
|
|
@@ -29893,9 +29910,9 @@ class GodotServer {
|
|
|
29893
29910
|
}
|
|
29894
29911
|
if (this.dapClient) {
|
|
29895
29912
|
try {
|
|
29896
|
-
await this.dapClient.
|
|
29913
|
+
await this.dapClient.abandon();
|
|
29897
29914
|
} catch (error) {
|
|
29898
|
-
this.logDebug(`DAP client did not
|
|
29915
|
+
this.logDebug(`DAP client did not let go cleanly: ${errorMessage(error)}`);
|
|
29899
29916
|
}
|
|
29900
29917
|
this.dapClient = null;
|
|
29901
29918
|
}
|
|
@@ -30899,7 +30916,7 @@ class GodotServer {
|
|
|
30899
30916
|
}
|
|
30900
30917
|
const uncached = unloaded.filter((type) => !type.inTheClassCache);
|
|
30901
30918
|
if (uncached.length > 0) {
|
|
30902
|
-
notes.push(
|
|
30919
|
+
notes.push(uncachedClassNote(uncached.map((type) => type.type)));
|
|
30903
30920
|
}
|
|
30904
30921
|
const stale = unloaded.filter((type) => type.inTheClassCache);
|
|
30905
30922
|
if (stale.length > 0) {
|
|
@@ -30947,6 +30964,9 @@ class GodotServer {
|
|
|
30947
30964
|
}
|
|
30948
30965
|
return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
|
|
30949
30966
|
}
|
|
30967
|
+
anEditorIsStillComing(listeningSince) {
|
|
30968
|
+
return anEditorIsStillComing(listeningSince, this.launchedEditor !== null && alive(this.launchedEditor));
|
|
30969
|
+
}
|
|
30950
30970
|
dap() {
|
|
30951
30971
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
30952
30972
|
if (this.dapClient !== null && this.dapClient.port !== port) {
|
|
@@ -30990,7 +31010,9 @@ class GodotServer {
|
|
|
30990
31010
|
serverVersion: SERVER_VERSION,
|
|
30991
31011
|
addonIsStale: status.connected ? stale : undefined,
|
|
30992
31012
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
30993
|
-
mayYetConnect: status.connected ? undefined :
|
|
31013
|
+
mayYetConnect: status.connected ? undefined : this.anEditorIsStillComing(status.listeningSince),
|
|
31014
|
+
awaitingLaunchedEditor: status.connected || this.launchedEditor === null || !alive(this.launchedEditor) ? undefined : this.launchedEditor,
|
|
31015
|
+
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
30994
31016
|
startupError: this.bridgeStartupError,
|
|
30995
31017
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
30996
31018
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
@@ -31087,7 +31109,6 @@ class GodotServer {
|
|
|
31087
31109
|
}
|
|
31088
31110
|
const now = this.godotBridge.getStatus();
|
|
31089
31111
|
const after = this.settingKeysOf(before.projectPath);
|
|
31090
|
-
const dropped = [...settingsBefore].filter(([key]) => !after.has(key));
|
|
31091
31112
|
return this.jsonTextResponse({
|
|
31092
31113
|
restarted: true,
|
|
31093
31114
|
editorPid: now.editorPid,
|
|
@@ -31095,11 +31116,21 @@ class GodotServer {
|
|
|
31095
31116
|
serverVersion: SERVER_VERSION,
|
|
31096
31117
|
addonIsStale: now.addonVersion !== SERVER_VERSION,
|
|
31097
31118
|
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
|
|
31098
|
-
|
|
31099
|
-
settingsNote: dropped.length > 0 ? `The editor saved project.godot on its way out and these keys are no longer in it, with the value each one held. Godot writes only what differs from its own defaults, so a key named deliberately at its default value is redundant to the editor and is dropped on save; it will be dropped again on the next restart. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, project_settings set puts one back: ${dropped.map(([setting, was]) => `setting "${setting}" value ${JSON.stringify(was)}`).join(", ")}. Nothing else will say they have gone until something depends on one.` : undefined,
|
|
31119
|
+
...this.whatTheEditorDropped(settingsBefore, after),
|
|
31100
31120
|
tookMs: Date.now() - began
|
|
31101
31121
|
});
|
|
31102
31122
|
}
|
|
31123
|
+
whatTheLaunchedEditorDropped() {
|
|
31124
|
+
const watched = this.launchedFrom;
|
|
31125
|
+
if (watched === null) {
|
|
31126
|
+
return {};
|
|
31127
|
+
}
|
|
31128
|
+
this.launchedFrom = null;
|
|
31129
|
+
return this.whatTheEditorDropped(watched.before, this.settingKeysOf(watched.projectPath));
|
|
31130
|
+
}
|
|
31131
|
+
whatTheEditorDropped(before, after) {
|
|
31132
|
+
return settingsDroppedReport(before, after);
|
|
31133
|
+
}
|
|
31103
31134
|
async startItAgain(projectPath, editorPid) {
|
|
31104
31135
|
const engine = await this.engine();
|
|
31105
31136
|
if (!engine.ok) {
|
|
@@ -31149,16 +31180,19 @@ class GodotServer {
|
|
|
31149
31180
|
}
|
|
31150
31181
|
this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
|
|
31151
31182
|
const ports = await this.portsForAnEditor();
|
|
31183
|
+
const before = this.settingKeysOf(project.value.path);
|
|
31152
31184
|
const opened = await this.openAnEditor(engine.value, project.value.path, ports);
|
|
31153
31185
|
if (opened.error !== null) {
|
|
31154
31186
|
return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
|
|
31155
31187
|
}
|
|
31188
|
+
this.launchedFrom = { projectPath: project.value.path, before };
|
|
31156
31189
|
return this.jsonTextResponse({
|
|
31157
31190
|
launched: true,
|
|
31158
31191
|
pid: opened.pid,
|
|
31159
31192
|
projectPath: project.value.path,
|
|
31160
31193
|
lspPort: ports.lsp,
|
|
31161
|
-
dapPort: ports.dap
|
|
31194
|
+
dapPort: ports.dap,
|
|
31195
|
+
settingsNote: 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."
|
|
31162
31196
|
});
|
|
31163
31197
|
}
|
|
31164
31198
|
async openAnEditor(engine, projectPath, ports) {
|
|
@@ -31185,6 +31219,7 @@ class GodotServer {
|
|
|
31185
31219
|
return { pid: null, error: started };
|
|
31186
31220
|
}
|
|
31187
31221
|
editor.unref();
|
|
31222
|
+
this.launchedEditor = editor.pid ?? null;
|
|
31188
31223
|
return { pid: editor.pid ?? null, error: null };
|
|
31189
31224
|
}
|
|
31190
31225
|
async portsForAnEditor() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.29",
|
|
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",
|