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 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 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 rather than one: a built copy has been seen stale here while " + "the diagnostics on it read clean, so the reload is worth doing and is not known to be what " + "clears these. 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
+ }
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 disconnect() {
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.disconnect();
37171
+ await this.dapClient.abandon();
37155
37172
  } catch (error) {
37156
- this.logDebug(`DAP client did not disconnect cleanly: ${errorMessage(error)}`);
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(`${uncached.map((type) => type.type).join(", ")} ${uncached.length === 1 ? "is declared" : "are declared"} in this project and missing from .godot/global_script_class_cache.cfg, so a game launched now would not resolve ${uncached.length === 1 ? "it" : "them"} either. project_import refresh_classes rewrites the cache from the declarations on disk.`);
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 : 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() : {},
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
- settingsDropped: dropped.length > 0 ? dropped.map(([setting, was]) => ({ setting, was })) : undefined,
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 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 rather than one: a built copy has been seen stale here while " + "the diagnostics on it read clean, so the reload is worth doing and is not known to be what " + "clears these. 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
+ }
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 disconnect() {
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.disconnect();
29913
+ await this.dapClient.abandon();
29897
29914
  } catch (error) {
29898
- this.logDebug(`DAP client did not disconnect cleanly: ${errorMessage(error)}`);
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(`${uncached.map((type) => type.type).join(", ")} ${uncached.length === 1 ? "is declared" : "are declared"} in this project and missing from .godot/global_script_class_cache.cfg, so a game launched now would not resolve ${uncached.length === 1 ? "it" : "them"} either. project_import refresh_classes rewrites the cache from the declarations on disk.`);
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 : 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() : {},
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
- settingsDropped: dropped.length > 0 ? dropped.map(([setting, was]) => ({ setting, was })) : undefined,
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.27",
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",