gdharness 0.13.28 → 0.13.30
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 +48 -19
- package/build/index.js +48 -19
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7626,7 +7626,7 @@ 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
7630
|
}
|
|
7631
7631
|
function uncachedClassNote(types) {
|
|
7632
7632
|
if (types.length === 0) {
|
|
@@ -15044,6 +15044,16 @@ function settingKeys(content) {
|
|
|
15044
15044
|
}
|
|
15045
15045
|
return named;
|
|
15046
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
|
+
}
|
|
15047
15057
|
function parseProjectGodot(content) {
|
|
15048
15058
|
const result = emptyRecord();
|
|
15049
15059
|
let section = emptyRecord();
|
|
@@ -15895,7 +15905,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15895
15905
|
},
|
|
15896
15906
|
{
|
|
15897
15907
|
name: "editor_launch",
|
|
15898
|
-
description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, 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.
|
|
15908
|
+
description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, 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.",
|
|
15899
15909
|
parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
|
|
15900
15910
|
requires: [],
|
|
15901
15911
|
operations: {
|
|
@@ -15985,7 +15995,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15985
15995
|
},
|
|
15986
15996
|
{
|
|
15987
15997
|
name: "editor_status",
|
|
15988
|
-
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.',
|
|
15989
15999
|
parameters: {},
|
|
15990
16000
|
requires: []
|
|
15991
16001
|
},
|
|
@@ -31408,7 +31418,7 @@ class GodotDAPClient {
|
|
|
31408
31418
|
socket.once("error", handleError);
|
|
31409
31419
|
});
|
|
31410
31420
|
}
|
|
31411
|
-
async
|
|
31421
|
+
async abandon() {
|
|
31412
31422
|
if (!this.socket) {
|
|
31413
31423
|
this.connected = false;
|
|
31414
31424
|
this.initialized = false;
|
|
@@ -31416,11 +31426,6 @@ class GodotDAPClient {
|
|
|
31416
31426
|
this.halt = null;
|
|
31417
31427
|
return;
|
|
31418
31428
|
}
|
|
31419
|
-
if (this.connected) {
|
|
31420
|
-
try {
|
|
31421
|
-
await this.sendRequest("disconnect", { restart: false });
|
|
31422
|
-
} catch {}
|
|
31423
|
-
}
|
|
31424
31429
|
await new Promise((resolve) => {
|
|
31425
31430
|
const socket = this.socket;
|
|
31426
31431
|
if (!socket) {
|
|
@@ -34722,6 +34727,9 @@ function servedPort(said) {
|
|
|
34722
34727
|
function mayYetConnect(listeningSince, now = Date.now()) {
|
|
34723
34728
|
return listeningSince === undefined || now - listeningSince.getTime() < CONNECT_WINDOW_MS;
|
|
34724
34729
|
}
|
|
34730
|
+
function anEditorIsStillComing(listeningSince, launchedEditorIsAlive, now = Date.now()) {
|
|
34731
|
+
return mayYetConnect(listeningSince, now) || launchedEditorIsAlive;
|
|
34732
|
+
}
|
|
34725
34733
|
function getDefaultBridge(ownProject = null) {
|
|
34726
34734
|
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
|
|
34727
34735
|
return defaultBridge;
|
|
@@ -37005,6 +37013,8 @@ class GodotServer {
|
|
|
37005
37013
|
successorWatch = null;
|
|
37006
37014
|
lastProjectPath = null;
|
|
37007
37015
|
shutdownInitiated = false;
|
|
37016
|
+
launchedEditor = null;
|
|
37017
|
+
launchedFrom = null;
|
|
37008
37018
|
ownProject;
|
|
37009
37019
|
announcedAt = null;
|
|
37010
37020
|
constructor() {
|
|
@@ -37158,9 +37168,9 @@ class GodotServer {
|
|
|
37158
37168
|
}
|
|
37159
37169
|
if (this.dapClient) {
|
|
37160
37170
|
try {
|
|
37161
|
-
await this.dapClient.
|
|
37171
|
+
await this.dapClient.abandon();
|
|
37162
37172
|
} catch (error) {
|
|
37163
|
-
this.logDebug(`DAP client did not
|
|
37173
|
+
this.logDebug(`DAP client did not let go cleanly: ${errorMessage(error)}`);
|
|
37164
37174
|
}
|
|
37165
37175
|
this.dapClient = null;
|
|
37166
37176
|
}
|
|
@@ -38212,6 +38222,9 @@ class GodotServer {
|
|
|
38212
38222
|
}
|
|
38213
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.`;
|
|
38214
38224
|
}
|
|
38225
|
+
anEditorIsStillComing(listeningSince) {
|
|
38226
|
+
return anEditorIsStillComing(listeningSince, this.launchedEditor !== null && alive(this.launchedEditor));
|
|
38227
|
+
}
|
|
38215
38228
|
dap() {
|
|
38216
38229
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
38217
38230
|
if (this.dapClient !== null && this.dapClient.port !== port) {
|
|
@@ -38255,7 +38268,9 @@ class GodotServer {
|
|
|
38255
38268
|
serverVersion: SERVER_VERSION,
|
|
38256
38269
|
addonIsStale: status.connected ? stale : undefined,
|
|
38257
38270
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
38258
|
-
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() : {},
|
|
38259
38274
|
startupError: this.bridgeStartupError,
|
|
38260
38275
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
38261
38276
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
@@ -38352,7 +38367,6 @@ class GodotServer {
|
|
|
38352
38367
|
}
|
|
38353
38368
|
const now = this.godotBridge.getStatus();
|
|
38354
38369
|
const after = this.settingKeysOf(before.projectPath);
|
|
38355
|
-
const dropped = [...settingsBefore].filter(([key]) => !after.has(key));
|
|
38356
38370
|
return this.jsonTextResponse({
|
|
38357
38371
|
restarted: true,
|
|
38358
38372
|
editorPid: now.editorPid,
|
|
@@ -38360,11 +38374,21 @@ class GodotServer {
|
|
|
38360
38374
|
serverVersion: SERVER_VERSION,
|
|
38361
38375
|
addonIsStale: now.addonVersion !== SERVER_VERSION,
|
|
38362
38376
|
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
|
|
38363
|
-
|
|
38364
|
-
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),
|
|
38365
38378
|
tookMs: Date.now() - began
|
|
38366
38379
|
});
|
|
38367
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
|
+
}
|
|
38368
38392
|
async startItAgain(projectPath, editorPid) {
|
|
38369
38393
|
const engine = await this.engine();
|
|
38370
38394
|
if (!engine.ok) {
|
|
@@ -38414,16 +38438,19 @@ class GodotServer {
|
|
|
38414
38438
|
}
|
|
38415
38439
|
this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
|
|
38416
38440
|
const ports = await this.portsForAnEditor();
|
|
38441
|
+
const before = this.settingKeysOf(project.value.path);
|
|
38417
38442
|
const opened = await this.openAnEditor(engine.value, project.value.path, ports);
|
|
38418
38443
|
if (opened.error !== null) {
|
|
38419
38444
|
return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
|
|
38420
38445
|
}
|
|
38446
|
+
this.launchedFrom = { projectPath: project.value.path, before };
|
|
38421
38447
|
return this.jsonTextResponse({
|
|
38422
38448
|
launched: true,
|
|
38423
38449
|
pid: opened.pid,
|
|
38424
38450
|
projectPath: project.value.path,
|
|
38425
38451
|
lspPort: ports.lsp,
|
|
38426
|
-
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."
|
|
38427
38454
|
});
|
|
38428
38455
|
}
|
|
38429
38456
|
async openAnEditor(engine, projectPath, ports) {
|
|
@@ -38450,6 +38477,7 @@ class GodotServer {
|
|
|
38450
38477
|
return { pid: null, error: started };
|
|
38451
38478
|
}
|
|
38452
38479
|
editor.unref();
|
|
38480
|
+
this.launchedEditor = editor.pid ?? null;
|
|
38453
38481
|
return { pid: editor.pid ?? null, error: null };
|
|
38454
38482
|
}
|
|
38455
38483
|
async portsForAnEditor() {
|
|
@@ -38836,15 +38864,16 @@ class GodotServer {
|
|
|
38836
38864
|
return adopted;
|
|
38837
38865
|
}
|
|
38838
38866
|
nothingOfOursIsRunning() {
|
|
38867
|
+
const status = this.godotBridge.getStatus();
|
|
38868
|
+
const noEditor = !status.connected ? " No editor is connected, so a game the editor is playing cannot be seen from here at all:" + " editor_status says whether one is on its way, and a run of yours that is still up is" + " reachable again once it is." : "";
|
|
38839
38869
|
const record = readRunRecord();
|
|
38840
38870
|
if (record === null) {
|
|
38841
|
-
return
|
|
38871
|
+
return `No game is running.${noEditor} Start one with editor_run.`;
|
|
38842
38872
|
}
|
|
38843
|
-
const status = this.godotBridge.getStatus();
|
|
38844
38873
|
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
38845
38874
|
const whose = record.projectPath === "" ? "a project it does not name" : record.projectPath;
|
|
38846
38875
|
const ours = mine === null ? "this server was started without GDHARNESS_PROJECT and has no editor connected, so it cannot say whose that run is" : `this server serves ${mine}`;
|
|
38847
|
-
return `No game of this server's is running
|
|
38876
|
+
return `No game of this server's is running.${noEditor} A run started from ${whose} is recorded on this machine, and ${ours}, so it is not this server's to answer for or to end. Start one with editor_run.`;
|
|
38848
38877
|
}
|
|
38849
38878
|
couldBeOurs(project) {
|
|
38850
38879
|
const status = this.godotBridge.getStatus();
|
package/build/index.js
CHANGED
|
@@ -24886,7 +24886,7 @@ 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
24890
|
}
|
|
24891
24891
|
function uncachedClassNote(types) {
|
|
24892
24892
|
if (types.length === 0) {
|
|
@@ -25160,7 +25160,7 @@ class GodotDAPClient {
|
|
|
25160
25160
|
socket.once("error", handleError);
|
|
25161
25161
|
});
|
|
25162
25162
|
}
|
|
25163
|
-
async
|
|
25163
|
+
async abandon() {
|
|
25164
25164
|
if (!this.socket) {
|
|
25165
25165
|
this.connected = false;
|
|
25166
25166
|
this.initialized = false;
|
|
@@ -25168,11 +25168,6 @@ class GodotDAPClient {
|
|
|
25168
25168
|
this.halt = null;
|
|
25169
25169
|
return;
|
|
25170
25170
|
}
|
|
25171
|
-
if (this.connected) {
|
|
25172
|
-
try {
|
|
25173
|
-
await this.sendRequest("disconnect", { restart: false });
|
|
25174
|
-
} catch {}
|
|
25175
|
-
}
|
|
25176
25171
|
await new Promise((resolve) => {
|
|
25177
25172
|
const socket = this.socket;
|
|
25178
25173
|
if (!socket) {
|
|
@@ -25761,6 +25756,9 @@ var CONNECT_WINDOW_MS = 35000;
|
|
|
25761
25756
|
function mayYetConnect(listeningSince, now = Date.now()) {
|
|
25762
25757
|
return listeningSince === undefined || now - listeningSince.getTime() < CONNECT_WINDOW_MS;
|
|
25763
25758
|
}
|
|
25759
|
+
function anEditorIsStillComing(listeningSince, launchedEditorIsAlive, now = Date.now()) {
|
|
25760
|
+
return mayYetConnect(listeningSince, now) || launchedEditorIsAlive;
|
|
25761
|
+
}
|
|
25764
25762
|
|
|
25765
25763
|
class GodotBridge extends EventEmitter {
|
|
25766
25764
|
listeningSince = null;
|
|
@@ -27732,6 +27730,16 @@ function settingKeys(content) {
|
|
|
27732
27730
|
}
|
|
27733
27731
|
return named;
|
|
27734
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
|
+
}
|
|
27735
27743
|
function parseProjectGodot(content) {
|
|
27736
27744
|
const result = emptyRecord();
|
|
27737
27745
|
let section = emptyRecord();
|
|
@@ -29002,7 +29010,7 @@ var TOOL_SPECS = [
|
|
|
29002
29010
|
},
|
|
29003
29011
|
{
|
|
29004
29012
|
name: "editor_launch",
|
|
29005
|
-
description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, 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.
|
|
29013
|
+
description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, 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.",
|
|
29006
29014
|
parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
|
|
29007
29015
|
requires: [],
|
|
29008
29016
|
operations: {
|
|
@@ -29092,7 +29100,7 @@ var TOOL_SPECS = [
|
|
|
29092
29100
|
},
|
|
29093
29101
|
{
|
|
29094
29102
|
name: "editor_status",
|
|
29095
|
-
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.',
|
|
29096
29104
|
parameters: {},
|
|
29097
29105
|
requires: []
|
|
29098
29106
|
},
|
|
@@ -29747,6 +29755,8 @@ class GodotServer {
|
|
|
29747
29755
|
successorWatch = null;
|
|
29748
29756
|
lastProjectPath = null;
|
|
29749
29757
|
shutdownInitiated = false;
|
|
29758
|
+
launchedEditor = null;
|
|
29759
|
+
launchedFrom = null;
|
|
29750
29760
|
ownProject;
|
|
29751
29761
|
announcedAt = null;
|
|
29752
29762
|
constructor() {
|
|
@@ -29900,9 +29910,9 @@ class GodotServer {
|
|
|
29900
29910
|
}
|
|
29901
29911
|
if (this.dapClient) {
|
|
29902
29912
|
try {
|
|
29903
|
-
await this.dapClient.
|
|
29913
|
+
await this.dapClient.abandon();
|
|
29904
29914
|
} catch (error) {
|
|
29905
|
-
this.logDebug(`DAP client did not
|
|
29915
|
+
this.logDebug(`DAP client did not let go cleanly: ${errorMessage(error)}`);
|
|
29906
29916
|
}
|
|
29907
29917
|
this.dapClient = null;
|
|
29908
29918
|
}
|
|
@@ -30954,6 +30964,9 @@ class GodotServer {
|
|
|
30954
30964
|
}
|
|
30955
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.`;
|
|
30956
30966
|
}
|
|
30967
|
+
anEditorIsStillComing(listeningSince) {
|
|
30968
|
+
return anEditorIsStillComing(listeningSince, this.launchedEditor !== null && alive(this.launchedEditor));
|
|
30969
|
+
}
|
|
30957
30970
|
dap() {
|
|
30958
30971
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
30959
30972
|
if (this.dapClient !== null && this.dapClient.port !== port) {
|
|
@@ -30997,7 +31010,9 @@ class GodotServer {
|
|
|
30997
31010
|
serverVersion: SERVER_VERSION,
|
|
30998
31011
|
addonIsStale: status.connected ? stale : undefined,
|
|
30999
31012
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
31000
|
-
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() : {},
|
|
31001
31016
|
startupError: this.bridgeStartupError,
|
|
31002
31017
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
31003
31018
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
@@ -31094,7 +31109,6 @@ class GodotServer {
|
|
|
31094
31109
|
}
|
|
31095
31110
|
const now = this.godotBridge.getStatus();
|
|
31096
31111
|
const after = this.settingKeysOf(before.projectPath);
|
|
31097
|
-
const dropped = [...settingsBefore].filter(([key]) => !after.has(key));
|
|
31098
31112
|
return this.jsonTextResponse({
|
|
31099
31113
|
restarted: true,
|
|
31100
31114
|
editorPid: now.editorPid,
|
|
@@ -31102,11 +31116,21 @@ class GodotServer {
|
|
|
31102
31116
|
serverVersion: SERVER_VERSION,
|
|
31103
31117
|
addonIsStale: now.addonVersion !== SERVER_VERSION,
|
|
31104
31118
|
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
|
|
31105
|
-
|
|
31106
|
-
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),
|
|
31107
31120
|
tookMs: Date.now() - began
|
|
31108
31121
|
});
|
|
31109
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
|
+
}
|
|
31110
31134
|
async startItAgain(projectPath, editorPid) {
|
|
31111
31135
|
const engine = await this.engine();
|
|
31112
31136
|
if (!engine.ok) {
|
|
@@ -31156,16 +31180,19 @@ class GodotServer {
|
|
|
31156
31180
|
}
|
|
31157
31181
|
this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
|
|
31158
31182
|
const ports = await this.portsForAnEditor();
|
|
31183
|
+
const before = this.settingKeysOf(project.value.path);
|
|
31159
31184
|
const opened = await this.openAnEditor(engine.value, project.value.path, ports);
|
|
31160
31185
|
if (opened.error !== null) {
|
|
31161
31186
|
return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
|
|
31162
31187
|
}
|
|
31188
|
+
this.launchedFrom = { projectPath: project.value.path, before };
|
|
31163
31189
|
return this.jsonTextResponse({
|
|
31164
31190
|
launched: true,
|
|
31165
31191
|
pid: opened.pid,
|
|
31166
31192
|
projectPath: project.value.path,
|
|
31167
31193
|
lspPort: ports.lsp,
|
|
31168
|
-
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."
|
|
31169
31196
|
});
|
|
31170
31197
|
}
|
|
31171
31198
|
async openAnEditor(engine, projectPath, ports) {
|
|
@@ -31192,6 +31219,7 @@ class GodotServer {
|
|
|
31192
31219
|
return { pid: null, error: started };
|
|
31193
31220
|
}
|
|
31194
31221
|
editor.unref();
|
|
31222
|
+
this.launchedEditor = editor.pid ?? null;
|
|
31195
31223
|
return { pid: editor.pid ?? null, error: null };
|
|
31196
31224
|
}
|
|
31197
31225
|
async portsForAnEditor() {
|
|
@@ -31578,15 +31606,16 @@ class GodotServer {
|
|
|
31578
31606
|
return adopted;
|
|
31579
31607
|
}
|
|
31580
31608
|
nothingOfOursIsRunning() {
|
|
31609
|
+
const status = this.godotBridge.getStatus();
|
|
31610
|
+
const noEditor = !status.connected ? " No editor is connected, so a game the editor is playing cannot be seen from here at all:" + " editor_status says whether one is on its way, and a run of yours that is still up is" + " reachable again once it is." : "";
|
|
31581
31611
|
const record = readRunRecord();
|
|
31582
31612
|
if (record === null) {
|
|
31583
|
-
return
|
|
31613
|
+
return `No game is running.${noEditor} Start one with editor_run.`;
|
|
31584
31614
|
}
|
|
31585
|
-
const status = this.godotBridge.getStatus();
|
|
31586
31615
|
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
31587
31616
|
const whose = record.projectPath === "" ? "a project it does not name" : record.projectPath;
|
|
31588
31617
|
const ours = mine === null ? "this server was started without GDHARNESS_PROJECT and has no editor connected, so it cannot say whose that run is" : `this server serves ${mine}`;
|
|
31589
|
-
return `No game of this server's is running
|
|
31618
|
+
return `No game of this server's is running.${noEditor} A run started from ${whose} is recorded on this machine, and ${ours}, so it is not this server's to answer for or to end. Start one with editor_run.`;
|
|
31590
31619
|
}
|
|
31591
31620
|
couldBeOurs(project) {
|
|
31592
31621
|
const status = this.godotBridge.getStatus();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.30",
|
|
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",
|