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 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 one of five attempts in the project that " + "reported it, and it cleared both reproductions measured in a second project, the scan itself " + "returning in 243ms and 275ms. " + "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.";
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. A restart saves project.godot on the way out, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped: settingsDropped names any that went, because nothing else will say so until something depends on one.",
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 disconnect() {
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.disconnect();
37171
+ await this.dapClient.abandon();
37162
37172
  } catch (error) {
37163
- this.logDebug(`DAP client did not disconnect cleanly: ${errorMessage(error)}`);
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 : mayYetConnect(status.listeningSince),
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
- settingsDropped: dropped.length > 0 ? dropped.map(([setting, was]) => ({ setting, was })) : undefined,
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 "No game is running. Start one with editor_run.";
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. 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.`;
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 one of five attempts in the project that " + "reported it, and it cleared both reproductions measured in a second project, the scan itself " + "returning in 243ms and 275ms. " + "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.";
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 disconnect() {
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. A restart saves project.godot on the way out, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped: settingsDropped names any that went, because nothing else will say so until something depends on one.",
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.disconnect();
29913
+ await this.dapClient.abandon();
29904
29914
  } catch (error) {
29905
- this.logDebug(`DAP client did not disconnect cleanly: ${errorMessage(error)}`);
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 : mayYetConnect(status.listeningSince),
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
- settingsDropped: dropped.length > 0 ? dropped.map(([setting, was]) => ({ setting, was })) : undefined,
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 "No game is running. Start one with editor_run.";
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. 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.`;
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.28",
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",