gdharness 0.13.33 → 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,
@@ -38289,11 +38297,17 @@ class GodotServer {
38289
38297
  }
38290
38298
  async editorPlayingState() {
38291
38299
  const status = this.godotBridge.getStatus();
38292
- if (!status.connected || status.addonVersion !== SERVER_VERSION) {
38300
+ if (!status.connected) {
38293
38301
  return null;
38294
38302
  }
38295
38303
  try {
38296
- const answer = await this.godotBridge.invokeTool("playing_status", {});
38304
+ const answer = await Promise.race([
38305
+ this.godotBridge.invokeTool("playing_status", {}),
38306
+ delay3(PLAYING_STATUS_MS).then(() => null)
38307
+ ]);
38308
+ if (answer === null) {
38309
+ return null;
38310
+ }
38297
38311
  return {
38298
38312
  playing: readBoolean(asParams(answer), "playing") ?? false,
38299
38313
  scene: readString(asParams(answer), "scenePath") ?? "",
@@ -38305,7 +38319,8 @@ class GodotServer {
38305
38319
  }
38306
38320
  async handleEditorStatus() {
38307
38321
  const godotPath = await this.locator.find();
38308
- const games = await Promise.all(discoverRuntimes().map(async (endpoint) => {
38322
+ const announced = runtimesAnnounced();
38323
+ const games = await Promise.all(announced.running.map(async (endpoint) => {
38309
38324
  const reply = await runtimeRequest(endpoint, "ping", {}, this.runtimeTimeoutMs());
38310
38325
  return {
38311
38326
  pid: endpoint.pid,
@@ -38327,10 +38342,17 @@ class GodotServer {
38327
38342
  version: godotPath === null ? null : await this.godotVersion(godotPath)
38328
38343
  },
38329
38344
  game: {
38330
- processActive: stillRunning(this.currentRun()),
38345
+ processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
38331
38346
  playingInEditor: playing,
38332
38347
  runtimeConnected: games.some((game) => game.reachable),
38333
- 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
+ }))
38334
38356
  }
38335
38357
  });
38336
38358
  }
@@ -38480,7 +38502,7 @@ class GodotServer {
38480
38502
  return { pid: null, error: started };
38481
38503
  }
38482
38504
  editor.unref();
38483
- this.launchedEditor = editor.pid ?? null;
38505
+ this.launchedEditor = editor.pid === undefined ? null : editor;
38484
38506
  return { pid: editor.pid ?? null, error: null };
38485
38507
  }
38486
38508
  async portsForAnEditor() {
@@ -38527,6 +38549,7 @@ class GodotServer {
38527
38549
  await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
38528
38550
  }
38529
38551
  const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
38552
+ const wereHereFirst = this.everyRuntimeOfOurs();
38530
38553
  const headless = resolveHeadless(args["headless"], {
38531
38554
  platform: process.platform,
38532
38555
  variables: process.env
@@ -38547,16 +38570,20 @@ class GodotServer {
38547
38570
  this.activeProcess = started;
38548
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." : "";
38549
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.`;
38550
38576
  return this.jsonTextResponse({
38551
38577
  started: true,
38552
38578
  through: "gdharness",
38579
+ alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
38553
38580
  pid: started.process.pid ?? null,
38554
38581
  arguments: cmdArgs,
38555
38582
  transcript: started.transcript,
38556
38583
  refreshedClasses: refreshed.value,
38557
38584
  endedPreviousRun: ended ?? undefined,
38558
38585
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
38559
- 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}`
38560
38587
  });
38561
38588
  }
38562
38589
  async gameIsUp() {
@@ -38872,7 +38899,12 @@ class GodotServer {
38872
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." : "";
38873
38900
  const live = this.aRuntimeOfOursIsAnswering();
38874
38901
  if (live !== null) {
38875
- 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.";
38876
38908
  }
38877
38909
  const record = readRunRecord();
38878
38910
  if (record === null) {
@@ -38884,12 +38916,34 @@ class GodotServer {
38884
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.`;
38885
38917
  }
38886
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) {
38887
38928
  const status = this.godotBridge.getStatus();
38888
38929
  const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
38889
38930
  if (mine === null) {
38890
- return null;
38931
+ return [];
38891
38932
  }
38892
- 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
+ ];
38893
38947
  }
38894
38948
  couldBeOurs(project) {
38895
38949
  const status = this.godotBridge.getStatus();
@@ -39284,7 +39338,7 @@ async function runGodotServer() {
39284
39338
  const server = new GodotServer;
39285
39339
  await server.run();
39286
39340
  }
39287
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run6, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
39341
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run6, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
39288
39342
  var init_server3 = __esm(() => {
39289
39343
  init_mcp();
39290
39344
  init_stdio2();
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
  },
@@ -29651,6 +29655,7 @@ function outsideTheirLists(spec, args) {
29651
29655
  var BRIDGE_RETRY_MS = 2000;
29652
29656
  var SUCCESSOR_CHECK_MS = 1e4;
29653
29657
  var CACHE_WRITE_MS = 2000;
29658
+ var PLAYING_STATUS_MS = 1000;
29654
29659
  var SCAN_START_MS = 2000;
29655
29660
  var WAIT_FOR_RUN_MS = 600000;
29656
29661
  var RUN_POLL_MS = 250;
@@ -30969,7 +30974,10 @@ class GodotServer {
30969
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.`;
30970
30975
  }
30971
30976
  anEditorIsStillComing(listeningSince) {
30972
- 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;
30973
30981
  }
30974
30982
  dap() {
30975
30983
  const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
@@ -31015,7 +31023,7 @@ class GodotServer {
31015
31023
  addonIsStale: status.connected ? stale : undefined,
31016
31024
  bridgeAvailable: this.bridgeStartupError === null,
31017
31025
  mayYetConnect: status.connected ? undefined : this.anEditorIsStillComing(status.listeningSince),
31018
- awaitingLaunchedEditor: status.connected || this.launchedEditor === null || !alive(this.launchedEditor) ? undefined : this.launchedEditor,
31026
+ awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
31019
31027
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
31020
31028
  startupError: this.bridgeStartupError,
31021
31029
  staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
@@ -31029,11 +31037,17 @@ class GodotServer {
31029
31037
  }
31030
31038
  async editorPlayingState() {
31031
31039
  const status = this.godotBridge.getStatus();
31032
- if (!status.connected || status.addonVersion !== SERVER_VERSION) {
31040
+ if (!status.connected) {
31033
31041
  return null;
31034
31042
  }
31035
31043
  try {
31036
- const answer = await this.godotBridge.invokeTool("playing_status", {});
31044
+ const answer = await Promise.race([
31045
+ this.godotBridge.invokeTool("playing_status", {}),
31046
+ delay3(PLAYING_STATUS_MS).then(() => null)
31047
+ ]);
31048
+ if (answer === null) {
31049
+ return null;
31050
+ }
31037
31051
  return {
31038
31052
  playing: readBoolean(asParams(answer), "playing") ?? false,
31039
31053
  scene: readString(asParams(answer), "scenePath") ?? "",
@@ -31045,7 +31059,8 @@ class GodotServer {
31045
31059
  }
31046
31060
  async handleEditorStatus() {
31047
31061
  const godotPath = await this.locator.find();
31048
- const games = await Promise.all(discoverRuntimes().map(async (endpoint) => {
31062
+ const announced = runtimesAnnounced();
31063
+ const games = await Promise.all(announced.running.map(async (endpoint) => {
31049
31064
  const reply = await runtimeRequest(endpoint, "ping", {}, this.runtimeTimeoutMs());
31050
31065
  return {
31051
31066
  pid: endpoint.pid,
@@ -31067,10 +31082,17 @@ class GodotServer {
31067
31082
  version: godotPath === null ? null : await this.godotVersion(godotPath)
31068
31083
  },
31069
31084
  game: {
31070
- processActive: stillRunning(this.currentRun()),
31085
+ processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
31071
31086
  playingInEditor: playing,
31072
31087
  runtimeConnected: games.some((game) => game.reachable),
31073
- 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
+ }))
31074
31096
  }
31075
31097
  });
31076
31098
  }
@@ -31220,7 +31242,7 @@ class GodotServer {
31220
31242
  return { pid: null, error: started };
31221
31243
  }
31222
31244
  editor.unref();
31223
- this.launchedEditor = editor.pid ?? null;
31245
+ this.launchedEditor = editor.pid === undefined ? null : editor;
31224
31246
  return { pid: editor.pid ?? null, error: null };
31225
31247
  }
31226
31248
  async portsForAnEditor() {
@@ -31267,6 +31289,7 @@ class GodotServer {
31267
31289
  await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
31268
31290
  }
31269
31291
  const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
31292
+ const wereHereFirst = this.everyRuntimeOfOurs();
31270
31293
  const headless = resolveHeadless(args["headless"], {
31271
31294
  platform: process.platform,
31272
31295
  variables: process.env
@@ -31287,16 +31310,20 @@ class GodotServer {
31287
31310
  this.activeProcess = started;
31288
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." : "";
31289
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.`;
31290
31316
  return this.jsonTextResponse({
31291
31317
  started: true,
31292
31318
  through: "gdharness",
31319
+ alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
31293
31320
  pid: started.process.pid ?? null,
31294
31321
  arguments: cmdArgs,
31295
31322
  transcript: started.transcript,
31296
31323
  refreshedClasses: refreshed.value,
31297
31324
  endedPreviousRun: ended ?? undefined,
31298
31325
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
31299
- 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}`
31300
31327
  });
31301
31328
  }
31302
31329
  async gameIsUp() {
@@ -31612,7 +31639,12 @@ class GodotServer {
31612
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." : "";
31613
31640
  const live = this.aRuntimeOfOursIsAnswering();
31614
31641
  if (live !== null) {
31615
- 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.";
31616
31648
  }
31617
31649
  const record = readRunRecord();
31618
31650
  if (record === null) {
@@ -31624,12 +31656,34 @@ class GodotServer {
31624
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.`;
31625
31657
  }
31626
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) {
31627
31668
  const status = this.godotBridge.getStatus();
31628
31669
  const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
31629
31670
  if (mine === null) {
31630
- return null;
31671
+ return [];
31631
31672
  }
31632
- 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
+ ];
31633
31687
  }
31634
31688
  couldBeOurs(project) {
31635
31689
  const status = this.godotBridge.getStatus();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.13.33",
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",