gdharness 0.13.34 → 0.13.35

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
@@ -16022,7 +16022,7 @@ var init_tool_definitions = __esm(() => {
16022
16022
  },
16023
16023
  {
16024
16024
  name: "editor_status",
16025
- 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.',
16025
+ 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. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. 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. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade: installing moves the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with. Those games are real and running, but no tool here can reach them until the two halves match, so runtimes stays empty and runtimeConnected stays false.',
16026
16026
  parameters: {},
16027
16027
  requires: []
16028
16028
  },
@@ -36752,6 +36752,9 @@ function judgeRun(record, running, needed) {
36752
36752
  if (wanted !== null && !said.includes(wanted)) {
36753
36753
  return false;
36754
36754
  }
36755
+ if (running.kind === "commandLine" && AN_EDITOR.test(said)) {
36756
+ return false;
36757
+ }
36755
36758
  if (record.projectPath === "") {
36756
36759
  return wanted !== null;
36757
36760
  }
@@ -36801,8 +36804,10 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
36801
36804
  } catch {}
36802
36805
  }
36803
36806
  }
36807
+ var AN_EDITOR;
36804
36808
  var init_run_record = __esm(() => {
36805
36809
  init_runtime_client();
36810
+ AN_EDITOR = /(?:^|\s)(?:-e|--editor)(?:\s|$)/;
36806
36811
  });
36807
36812
 
36808
36813
  // src/server.ts
@@ -38229,7 +38234,10 @@ class GodotServer {
38229
38234
  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.`;
38230
38235
  }
38231
38236
  anEditorIsStillComing(listeningSince) {
38232
- return anEditorIsStillComing(listeningSince, this.launchedEditor !== null && alive(this.launchedEditor));
38237
+ return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
38238
+ }
38239
+ launchedEditorIsUp() {
38240
+ return this.launchedEditor?.exitCode === null && this.launchedEditor.signalCode === null;
38233
38241
  }
38234
38242
  dap() {
38235
38243
  const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
@@ -38275,7 +38283,7 @@ class GodotServer {
38275
38283
  addonIsStale: status.connected ? stale : undefined,
38276
38284
  bridgeAvailable: this.bridgeStartupError === null,
38277
38285
  mayYetConnect: status.connected ? undefined : this.anEditorIsStillComing(status.listeningSince),
38278
- awaitingLaunchedEditor: status.connected || this.launchedEditor === null || !alive(this.launchedEditor) ? undefined : this.launchedEditor,
38286
+ awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
38279
38287
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
38280
38288
  startupError: this.bridgeStartupError,
38281
38289
  staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
@@ -38311,7 +38319,8 @@ class GodotServer {
38311
38319
  }
38312
38320
  async handleEditorStatus() {
38313
38321
  const godotPath = await this.locator.find();
38314
- const games = await Promise.all(discoverRuntimes().map(async (endpoint) => {
38322
+ const announced = runtimesAnnounced();
38323
+ const games = await Promise.all(announced.running.map(async (endpoint) => {
38315
38324
  const reply = await runtimeRequest(endpoint, "ping", {}, this.runtimeTimeoutMs());
38316
38325
  return {
38317
38326
  pid: endpoint.pid,
@@ -38333,10 +38342,17 @@ class GodotServer {
38333
38342
  version: godotPath === null ? null : await this.godotVersion(godotPath)
38334
38343
  },
38335
38344
  game: {
38336
- processActive: stillRunning(this.currentRun()),
38345
+ processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
38337
38346
  playingInEditor: playing,
38338
38347
  runtimeConnected: games.some((game) => game.reachable),
38339
- runtimes: games
38348
+ runtimes: games,
38349
+ unreadable: announced.unspoken.map((game) => ({
38350
+ pid: game.pid,
38351
+ project: game.project,
38352
+ protocol: game.protocol,
38353
+ serverSpeaks: RUNTIME_PROTOCOL,
38354
+ behind: game.protocol > RUNTIME_PROTOCOL ? "server" : "addon"
38355
+ }))
38340
38356
  }
38341
38357
  });
38342
38358
  }
@@ -38486,7 +38502,7 @@ class GodotServer {
38486
38502
  return { pid: null, error: started };
38487
38503
  }
38488
38504
  editor.unref();
38489
- this.launchedEditor = editor.pid ?? null;
38505
+ this.launchedEditor = editor.pid === undefined ? null : editor;
38490
38506
  return { pid: editor.pid ?? null, error: null };
38491
38507
  }
38492
38508
  async portsForAnEditor() {
@@ -38533,6 +38549,7 @@ class GodotServer {
38533
38549
  await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
38534
38550
  }
38535
38551
  const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
38552
+ const wereHereFirst = this.everyRuntimeOfOurs();
38536
38553
  const headless = resolveHeadless(args["headless"], {
38537
38554
  platform: process.platform,
38538
38555
  variables: process.env
@@ -38553,16 +38570,20 @@ class GodotServer {
38553
38570
  this.activeProcess = started;
38554
38571
  const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : "";
38555
38572
  const endedForThis = ended === null ? "" : ` The run that was going, pid ${ended}, was ended to start this one; its output is no longer what editor_output answers about.`;
38573
+ const stranded = wereHereFirst.filter((one) => one.pid !== ended && alive(one.pid));
38574
+ const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
38575
+ const alsoRunning = stranded.length === 0 ? "" : ` A game of this project was already running and still is: ${named}. Starting this one did not end it and nothing here can, because this server did not start it. End it yourself, or bring the halves to one version where a protocol is named.`;
38556
38576
  return this.jsonTextResponse({
38557
38577
  started: true,
38558
38578
  through: "gdharness",
38579
+ alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
38559
38580
  pid: started.process.pid ?? null,
38560
38581
  arguments: cmdArgs,
38561
38582
  transcript: started.transcript,
38562
38583
  refreshedClasses: refreshed.value,
38563
38584
  endedPreviousRun: ended ?? undefined,
38564
38585
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
38565
- message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}`
38586
+ message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
38566
38587
  });
38567
38588
  }
38568
38589
  async gameIsUp() {
@@ -38878,7 +38899,12 @@ class GodotServer {
38878
38899
  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." : "";
38879
38900
  const live = this.aRuntimeOfOursIsAnswering();
38880
38901
  if (live !== null) {
38881
- return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" + " editor_output have nothing to answer about. The runtime_* tools reach it now." + " Do not start another: editor_run start replaces the game that is playing rather than" + " adding one, so it would end this one." + (this.godotBridge.getStatus().connected ? "" : " editor_status says whether an editor is on its way, and editor_run can end it once one is.");
38902
+ return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" + " editor_output have nothing to answer about. The runtime_* tools reach it now." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well." + (this.godotBridge.getStatus().connected ? "" : " editor_status says whether an editor is on its way, and editor_run can end it once one is.");
38903
+ }
38904
+ const unreadable = this.aRuntimeOfOursIsTooFarOff();
38905
+ if (unreadable !== null) {
38906
+ const behind = unreadable.protocol > RUNTIME_PROTOCOL ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
38907
+ return `A game is running for ${unreadable.project.path}, pid ${unreadable.pid}, announced in protocol` + ` ${unreadable.protocol}, which this server does not speak: it speaks ${RUNTIME_PROTOCOL}, so ${behind}.` + " Until then neither editor_run nor the runtime_* tools can reach it." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well.";
38882
38908
  }
38883
38909
  const record = readRunRecord();
38884
38910
  if (record === null) {
@@ -38890,12 +38916,34 @@ class GodotServer {
38890
38916
  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.`;
38891
38917
  }
38892
38918
  aRuntimeOfOursIsAnswering() {
38919
+ return this.announcedForOurProject(discoverRuntimes());
38920
+ }
38921
+ aRuntimeOfOursIsTooFarOff() {
38922
+ return this.announcedForOurProject(runtimesAnnounced().unspoken);
38923
+ }
38924
+ announcedForOurProject(announced) {
38925
+ return this.allAnnouncedForOurProject(announced)[0] ?? null;
38926
+ }
38927
+ allAnnouncedForOurProject(announced) {
38893
38928
  const status = this.godotBridge.getStatus();
38894
38929
  const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
38895
38930
  if (mine === null) {
38896
- return null;
38931
+ return [];
38897
38932
  }
38898
- return discoverRuntimes().find((one) => one.project.path !== "" && isSameDirectory(mine, one.project.path)) ?? null;
38933
+ return announced.filter((one) => one.project.path !== "" && isSameDirectory(mine, one.project.path));
38934
+ }
38935
+ everyRuntimeOfOurs() {
38936
+ const announced = runtimesAnnounced();
38937
+ return [
38938
+ ...this.allAnnouncedForOurProject(announced.running).map((one) => ({
38939
+ pid: one.pid,
38940
+ protocol: null
38941
+ })),
38942
+ ...this.allAnnouncedForOurProject(announced.unspoken).map((one) => ({
38943
+ pid: one.pid,
38944
+ protocol: one.protocol
38945
+ }))
38946
+ ];
38899
38947
  }
38900
38948
  couldBeOurs(project) {
38901
38949
  const status = this.godotBridge.getStatus();
package/build/index.js CHANGED
@@ -28336,6 +28336,7 @@ function stillTheRecordedRun(record) {
28336
28336
  function couldStillBeTheRecordedRun(record) {
28337
28337
  return judgeRun(record, runningAs(record.pid), "possible");
28338
28338
  }
28339
+ var AN_EDITOR = /(?:^|\s)(?:-e|--editor)(?:\s|$)/;
28339
28340
  function judgeRun(record, running, needed) {
28340
28341
  if (running === null) {
28341
28342
  return false;
@@ -28346,6 +28347,9 @@ function judgeRun(record, running, needed) {
28346
28347
  if (wanted !== null && !said.includes(wanted)) {
28347
28348
  return false;
28348
28349
  }
28350
+ if (running.kind === "commandLine" && AN_EDITOR.test(said)) {
28351
+ return false;
28352
+ }
28349
28353
  if (record.projectPath === "") {
28350
28354
  return wanted !== null;
28351
28355
  }
@@ -29104,7 +29108,7 @@ var TOOL_SPECS = [
29104
29108
  },
29105
29109
  {
29106
29110
  name: "editor_status",
29107
- 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.',
29111
+ 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. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. 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. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade: installing moves the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with. Those games are real and running, but no tool here can reach them until the two halves match, so runtimes stays empty and runtimeConnected stays false.',
29108
29112
  parameters: {},
29109
29113
  requires: []
29110
29114
  },
@@ -30970,7 +30974,10 @@ class GodotServer {
30970
30974
  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.`;
30971
30975
  }
30972
30976
  anEditorIsStillComing(listeningSince) {
30973
- return anEditorIsStillComing(listeningSince, this.launchedEditor !== null && alive(this.launchedEditor));
30977
+ return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
30978
+ }
30979
+ launchedEditorIsUp() {
30980
+ return this.launchedEditor?.exitCode === null && this.launchedEditor.signalCode === null;
30974
30981
  }
30975
30982
  dap() {
30976
30983
  const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
@@ -31016,7 +31023,7 @@ class GodotServer {
31016
31023
  addonIsStale: status.connected ? stale : undefined,
31017
31024
  bridgeAvailable: this.bridgeStartupError === null,
31018
31025
  mayYetConnect: status.connected ? undefined : this.anEditorIsStillComing(status.listeningSince),
31019
- awaitingLaunchedEditor: status.connected || this.launchedEditor === null || !alive(this.launchedEditor) ? undefined : this.launchedEditor,
31026
+ awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
31020
31027
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
31021
31028
  startupError: this.bridgeStartupError,
31022
31029
  staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
@@ -31052,7 +31059,8 @@ class GodotServer {
31052
31059
  }
31053
31060
  async handleEditorStatus() {
31054
31061
  const godotPath = await this.locator.find();
31055
- const games = await Promise.all(discoverRuntimes().map(async (endpoint) => {
31062
+ const announced = runtimesAnnounced();
31063
+ const games = await Promise.all(announced.running.map(async (endpoint) => {
31056
31064
  const reply = await runtimeRequest(endpoint, "ping", {}, this.runtimeTimeoutMs());
31057
31065
  return {
31058
31066
  pid: endpoint.pid,
@@ -31074,10 +31082,17 @@ class GodotServer {
31074
31082
  version: godotPath === null ? null : await this.godotVersion(godotPath)
31075
31083
  },
31076
31084
  game: {
31077
- processActive: stillRunning(this.currentRun()),
31085
+ processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
31078
31086
  playingInEditor: playing,
31079
31087
  runtimeConnected: games.some((game) => game.reachable),
31080
- runtimes: games
31088
+ runtimes: games,
31089
+ unreadable: announced.unspoken.map((game) => ({
31090
+ pid: game.pid,
31091
+ project: game.project,
31092
+ protocol: game.protocol,
31093
+ serverSpeaks: RUNTIME_PROTOCOL,
31094
+ behind: game.protocol > RUNTIME_PROTOCOL ? "server" : "addon"
31095
+ }))
31081
31096
  }
31082
31097
  });
31083
31098
  }
@@ -31227,7 +31242,7 @@ class GodotServer {
31227
31242
  return { pid: null, error: started };
31228
31243
  }
31229
31244
  editor.unref();
31230
- this.launchedEditor = editor.pid ?? null;
31245
+ this.launchedEditor = editor.pid === undefined ? null : editor;
31231
31246
  return { pid: editor.pid ?? null, error: null };
31232
31247
  }
31233
31248
  async portsForAnEditor() {
@@ -31274,6 +31289,7 @@ class GodotServer {
31274
31289
  await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
31275
31290
  }
31276
31291
  const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
31292
+ const wereHereFirst = this.everyRuntimeOfOurs();
31277
31293
  const headless = resolveHeadless(args["headless"], {
31278
31294
  platform: process.platform,
31279
31295
  variables: process.env
@@ -31294,16 +31310,20 @@ class GodotServer {
31294
31310
  this.activeProcess = started;
31295
31311
  const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : "";
31296
31312
  const endedForThis = ended === null ? "" : ` The run that was going, pid ${ended}, was ended to start this one; its output is no longer what editor_output answers about.`;
31313
+ const stranded = wereHereFirst.filter((one) => one.pid !== ended && alive(one.pid));
31314
+ const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
31315
+ const alsoRunning = stranded.length === 0 ? "" : ` A game of this project was already running and still is: ${named}. Starting this one did not end it and nothing here can, because this server did not start it. End it yourself, or bring the halves to one version where a protocol is named.`;
31297
31316
  return this.jsonTextResponse({
31298
31317
  started: true,
31299
31318
  through: "gdharness",
31319
+ alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
31300
31320
  pid: started.process.pid ?? null,
31301
31321
  arguments: cmdArgs,
31302
31322
  transcript: started.transcript,
31303
31323
  refreshedClasses: refreshed.value,
31304
31324
  endedPreviousRun: ended ?? undefined,
31305
31325
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
31306
- message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}`
31326
+ message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
31307
31327
  });
31308
31328
  }
31309
31329
  async gameIsUp() {
@@ -31619,7 +31639,12 @@ class GodotServer {
31619
31639
  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." : "";
31620
31640
  const live = this.aRuntimeOfOursIsAnswering();
31621
31641
  if (live !== null) {
31622
- return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" + " editor_output have nothing to answer about. The runtime_* tools reach it now." + " Do not start another: editor_run start replaces the game that is playing rather than" + " adding one, so it would end this one." + (this.godotBridge.getStatus().connected ? "" : " editor_status says whether an editor is on its way, and editor_run can end it once one is.");
31642
+ return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" + " editor_output have nothing to answer about. The runtime_* tools reach it now." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well." + (this.godotBridge.getStatus().connected ? "" : " editor_status says whether an editor is on its way, and editor_run can end it once one is.");
31643
+ }
31644
+ const unreadable = this.aRuntimeOfOursIsTooFarOff();
31645
+ if (unreadable !== null) {
31646
+ const behind = unreadable.protocol > RUNTIME_PROTOCOL ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
31647
+ return `A game is running for ${unreadable.project.path}, pid ${unreadable.pid}, announced in protocol` + ` ${unreadable.protocol}, which this server does not speak: it speaks ${RUNTIME_PROTOCOL}, so ${behind}.` + " Until then neither editor_run nor the runtime_* tools can reach it." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well.";
31623
31648
  }
31624
31649
  const record = readRunRecord();
31625
31650
  if (record === null) {
@@ -31631,12 +31656,34 @@ class GodotServer {
31631
31656
  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.`;
31632
31657
  }
31633
31658
  aRuntimeOfOursIsAnswering() {
31659
+ return this.announcedForOurProject(discoverRuntimes());
31660
+ }
31661
+ aRuntimeOfOursIsTooFarOff() {
31662
+ return this.announcedForOurProject(runtimesAnnounced().unspoken);
31663
+ }
31664
+ announcedForOurProject(announced) {
31665
+ return this.allAnnouncedForOurProject(announced)[0] ?? null;
31666
+ }
31667
+ allAnnouncedForOurProject(announced) {
31634
31668
  const status = this.godotBridge.getStatus();
31635
31669
  const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
31636
31670
  if (mine === null) {
31637
- return null;
31671
+ return [];
31638
31672
  }
31639
- return discoverRuntimes().find((one) => one.project.path !== "" && isSameDirectory(mine, one.project.path)) ?? null;
31673
+ return announced.filter((one) => one.project.path !== "" && isSameDirectory(mine, one.project.path));
31674
+ }
31675
+ everyRuntimeOfOurs() {
31676
+ const announced = runtimesAnnounced();
31677
+ return [
31678
+ ...this.allAnnouncedForOurProject(announced.running).map((one) => ({
31679
+ pid: one.pid,
31680
+ protocol: null
31681
+ })),
31682
+ ...this.allAnnouncedForOurProject(announced.unspoken).map((one) => ({
31683
+ pid: one.pid,
31684
+ protocol: one.protocol
31685
+ }))
31686
+ ];
31640
31687
  }
31641
31688
  couldBeOurs(project) {
31642
31689
  const status = this.godotBridge.getStatus();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.13.34",
3
+ "version": "0.13.35",
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",