gdharness 0.13.7 → 0.13.8

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.
Files changed (3) hide show
  1. package/build/cli.js +119 -13
  2. package/build/index.js +119 -13
  3. package/package.json +1 -1
package/build/cli.js CHANGED
@@ -14896,6 +14896,15 @@ function isValueComplete(value) {
14896
14896
  }
14897
14897
  return depth <= 0 && !inString;
14898
14898
  }
14899
+ function settingKeys(content) {
14900
+ const named = new Set;
14901
+ for (const [section, entries] of Object.entries(parseProjectGodot(content))) {
14902
+ for (const key of Object.keys(entries)) {
14903
+ named.add(`${section}/${key}`);
14904
+ }
14905
+ }
14906
+ return named;
14907
+ }
14899
14908
  function parseProjectGodot(content) {
14900
14909
  const result = emptyRecord();
14901
14910
  let section = emptyRecord();
@@ -15730,7 +15739,7 @@ var init_tool_definitions = __esm(() => {
15730
15739
  },
15731
15740
  {
15732
15741
  name: "editor_launch",
15733
- description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon.",
15742
+ description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. A restart saves project.godot on the way out, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped: settingsDropped names any that went, because nothing else will say so until something depends on one.",
15734
15743
  parameters: { projectPath: PROJECT_PATH },
15735
15744
  requires: [],
15736
15745
  operations: {
@@ -36321,6 +36330,41 @@ function writeRunRecord(record) {
36321
36330
  mkdirSync6(runsDirectory(), { recursive: true });
36322
36331
  writeFileSync8(recordPath(), JSON.stringify(record, null, 2), "utf8");
36323
36332
  }
36333
+ function editorNotePath() {
36334
+ return join15(runsDirectory(), "editor-run.json");
36335
+ }
36336
+ function writeEditorRunNote(note) {
36337
+ mkdirSync6(runsDirectory(), { recursive: true });
36338
+ writeFileSync8(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
36339
+ }
36340
+ function readEditorRunNote() {
36341
+ for (const directory of runtimeDirectories()) {
36342
+ const note = editorNoteAt(join15(directory, "runs", "editor-run.json"));
36343
+ if (note !== null) {
36344
+ return note;
36345
+ }
36346
+ }
36347
+ return null;
36348
+ }
36349
+ function editorNoteAt(path) {
36350
+ let parsed;
36351
+ try {
36352
+ parsed = JSON.parse(readFileSync13(path, "utf8"));
36353
+ } catch {
36354
+ return null;
36355
+ }
36356
+ if (!parsed || typeof parsed !== "object") {
36357
+ return null;
36358
+ }
36359
+ const fields = parsed;
36360
+ const projectPath = fields["projectPath"];
36361
+ const transcript = fields["transcript"];
36362
+ const startedAt = fields["startedAt"];
36363
+ if (typeof projectPath !== "string" || typeof transcript !== "string" || typeof startedAt !== "number") {
36364
+ return null;
36365
+ }
36366
+ return { projectPath, transcript, startedAt };
36367
+ }
36324
36368
  function recordRunEnded(pid, exitCode) {
36325
36369
  const path = recordPath();
36326
36370
  const record = recordAt(path);
@@ -36513,6 +36557,7 @@ var init_setup = __esm(() => {
36513
36557
  import { execFile as execFile6, spawn } from "node:child_process";
36514
36558
  import { randomUUID as randomUUID2 } from "node:crypto";
36515
36559
  import {
36560
+ appendFileSync,
36516
36561
  closeSync,
36517
36562
  existsSync as existsSync14,
36518
36563
  fstatSync,
@@ -36522,7 +36567,8 @@ import {
36522
36567
  readdirSync as readdirSync7,
36523
36568
  readFileSync as readFileSync15,
36524
36569
  readSync,
36525
- rmSync as rmSync9
36570
+ rmSync as rmSync9,
36571
+ statSync as statSync4
36526
36572
  } from "node:fs";
36527
36573
  import { tmpdir as tmpdir5 } from "node:os";
36528
36574
  import { basename as basename3, dirname as dirname9, join as join17, normalize as normalize3 } from "node:path";
@@ -37906,6 +37952,16 @@ class GodotServer {
37906
37952
  }
37907
37953
  });
37908
37954
  }
37955
+ settingKeysOf(projectPath) {
37956
+ if (projectPath === undefined) {
37957
+ return new Set;
37958
+ }
37959
+ try {
37960
+ return settingKeys(readFileSync15(join17(projectPath, "project.godot"), "utf8"));
37961
+ } catch {
37962
+ return new Set;
37963
+ }
37964
+ }
37909
37965
  async handleRestartEditor() {
37910
37966
  const before = this.godotBridge.getStatus();
37911
37967
  if (!before.connected) {
@@ -37914,6 +37970,7 @@ class GodotServer {
37914
37970
  "editor_status says whether the bridge is up and what has reached it"
37915
37971
  ]);
37916
37972
  }
37973
+ const settingsBefore = this.settingKeysOf(before.projectPath);
37917
37974
  const mine = before.openedByAServer === true && before.projectPath !== undefined;
37918
37975
  const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid) : await this.handleViaBridge("restart_editor", {});
37919
37976
  if (asked.isError === true) {
@@ -37933,6 +37990,8 @@ class GodotServer {
37933
37990
  ]);
37934
37991
  }
37935
37992
  const now = this.godotBridge.getStatus();
37993
+ const after = this.settingKeysOf(before.projectPath);
37994
+ const dropped = [...settingsBefore].filter((key) => !after.has(key));
37936
37995
  return this.jsonTextResponse({
37937
37996
  restarted: true,
37938
37997
  editorPid: now.editorPid,
@@ -37940,6 +37999,8 @@ class GodotServer {
37940
37999
  serverVersion: SERVER_VERSION,
37941
38000
  addonIsStale: now.addonVersion !== SERVER_VERSION,
37942
38001
  staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
38002
+ settingsDropped: dropped.length > 0 ? dropped : undefined,
38003
+ settingsNote: dropped.length > 0 ? 'The editor saved project.godot on its way out and these keys are no longer in it. 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. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, put them back: nothing else will say they have gone until something depends on one.' : undefined,
37943
38004
  tookMs: Date.now() - began
37944
38005
  });
37945
38006
  }
@@ -38130,6 +38191,7 @@ class GodotServer {
38130
38191
  const log = new GameLog;
38131
38192
  try {
38132
38193
  await this.dap().connect();
38194
+ this.dapClient?.getOutput(true);
38133
38195
  } catch (error) {
38134
38196
  return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
38135
38197
  `This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
@@ -38140,19 +38202,24 @@ class GodotServer {
38140
38202
  if (answer.isError === true) {
38141
38203
  return answer;
38142
38204
  }
38205
+ const startedAt = Date.now();
38206
+ const transcript = openTranscript(startedAt);
38207
+ closeSync(transcript.fd);
38143
38208
  const played = {
38144
38209
  process: null,
38145
38210
  pid: null,
38146
38211
  log,
38147
- transcript: null,
38212
+ transcript: transcript.path,
38148
38213
  readOffset: 0,
38149
38214
  projectPath,
38150
- startedAt: Date.now(),
38215
+ startedAt,
38151
38216
  exitCode: null,
38152
38217
  throughEditor: true,
38153
38218
  brokeOn: null
38154
38219
  };
38155
38220
  this.activeProcess = played;
38221
+ writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
38222
+ sweepTranscripts();
38156
38223
  return this.jsonTextResponse({
38157
38224
  started: true,
38158
38225
  through: "editor",
@@ -38168,10 +38235,24 @@ class GodotServer {
38168
38235
  if (!game.throughEditor || !this.dapClient) {
38169
38236
  return;
38170
38237
  }
38238
+ const heard = [];
38171
38239
  for (const line of this.dapClient.getOutput(true)) {
38172
- game.log.append("stdout", line.endsWith(`
38240
+ const ended = line.endsWith(`
38173
38241
  `) ? line : `${line}
38174
- `);
38242
+ `;
38243
+ game.log.append("stdout", ended);
38244
+ heard.push(ended);
38245
+ }
38246
+ if (heard.length > 0 && game.transcript !== null) {
38247
+ try {
38248
+ appendFileSync(game.transcript, heard.join(""));
38249
+ game.readOffset = statSync4(game.transcript).size;
38250
+ } catch (error) {
38251
+ this.logDebug(`Could not write an editor-played run's transcript: ${errorMessage(error)}`);
38252
+ }
38253
+ }
38254
+ if (!this.dapClient.isConnected()) {
38255
+ game.consoleLost = true;
38175
38256
  }
38176
38257
  this.recordWhatItBrokeOn(game);
38177
38258
  }
@@ -38325,7 +38406,20 @@ class GodotServer {
38325
38406
  }
38326
38407
  }
38327
38408
  async pickUpWhatTheEditorIsPlaying() {
38328
- if (this.activeProcess !== null || !this.godotBridge.isConnected()) {
38409
+ if (!this.godotBridge.isConnected()) {
38410
+ return;
38411
+ }
38412
+ const going = this.activeProcess;
38413
+ if (going !== null) {
38414
+ if (going.throughEditor && going.consoleLost === true && (await this.editorPlayingState())?.playing) {
38415
+ try {
38416
+ await this.dap().connect();
38417
+ going.log.append("stdout", `[gdharness] the debug adapter was reconnected here
38418
+ `);
38419
+ } catch (error) {
38420
+ this.logDebug(`Could not reconnect the adapter for a playing run: ${errorMessage(error)}`);
38421
+ }
38422
+ }
38329
38423
  return;
38330
38424
  }
38331
38425
  const playing = await this.editorPlayingState();
@@ -38337,19 +38431,23 @@ class GodotServer {
38337
38431
  } catch (error) {
38338
38432
  this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
38339
38433
  }
38340
- this.activeProcess = {
38434
+ const project = this.godotBridge.getStatus().projectPath ?? null;
38435
+ const left = readEditorRunNote();
38436
+ const note = left !== null && project !== null && isSameDirectory(left.projectPath, project) ? left : null;
38437
+ const picked = {
38341
38438
  process: null,
38342
38439
  pid: null,
38343
38440
  log: new GameLog,
38344
- transcript: null,
38441
+ transcript: note?.transcript ?? null,
38345
38442
  readOffset: 0,
38346
- projectPath: this.godotBridge.getStatus().projectPath ?? null,
38347
- startedAt: Date.now(),
38443
+ projectPath: project,
38444
+ startedAt: note?.startedAt ?? Date.now(),
38348
38445
  exitCode: null,
38349
38446
  throughEditor: true,
38350
38447
  brokeOn: null,
38351
38448
  pickedUpPlaying: true
38352
38449
  };
38450
+ this.activeProcess = picked;
38353
38451
  }
38354
38452
  adoptRecordedRun() {
38355
38453
  const record = readRunRecord();
@@ -38469,6 +38567,9 @@ class GodotServer {
38469
38567
  const wanted = readBoolean(args, "cpu") ?? false;
38470
38568
  const cpuSeconds = wanted && run.pid !== null && stillRunning(run) ? await cpuSecondsOf(run.pid) : undefined;
38471
38569
  const notes = [];
38570
+ if (wanted && cpuSeconds === undefined && stillRunning(run)) {
38571
+ notes.push(run.pid === null ? "cpu was asked for and there is no process to ask: the editor is playing this run and holds it, so nothing here has its process id. editor_run start without a connected editor, or with args, starts the game here instead and that run answers cpuSeconds." : "cpu was asked for and this platform would not say what the run has used.");
38572
+ }
38472
38573
  if (waitedMs !== undefined) {
38473
38574
  notes.push(`The wait of ${waitedMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
38474
38575
  }
@@ -38476,7 +38577,11 @@ class GodotServer {
38476
38577
  notes.push("This run outlived the server that started it and is over now, so its exit code was never collected. Everything it printed is below, read back from its transcript.");
38477
38578
  }
38478
38579
  if (run.pickedUpPlaying === true) {
38479
- notes.push("The editor was already playing this when this server reached it, so this is not the run from its start: what it printed before that went with the server that was listening then. It is the run the editor is holding now, which is the one the debug_* tools answer for.");
38580
+ notes.push(run.transcript === null ? "The editor was already playing this when this server reached it, and the server that started it left no transcript, so this is not the run from its start: what it printed before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for." : "The editor was already playing this when this server reached it, and what it printed before that has been read back from its transcript. It is the run the editor is holding now, which is the one the debug_* tools answer for.");
38581
+ }
38582
+ if (run.consoleLost === true) {
38583
+ const back = this.dapClient?.isConnected() === true;
38584
+ notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
38480
38585
  }
38481
38586
  if (typeof run.endedHere === "string") {
38482
38587
  notes.push(`This run was ended here, by ${run.endedHere}.`);
@@ -38493,7 +38598,8 @@ class GodotServer {
38493
38598
  pid: run.pid,
38494
38599
  errors: run.log.count("error"),
38495
38600
  warnings: run.log.count("warning"),
38496
- clean: run.log.count("error") === 0,
38601
+ clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
38602
+ consoleLost: run.consoleLost === true ? true : undefined,
38497
38603
  heldAt: halt,
38498
38604
  endedUnwatched: run.endedUnwatched === true ? true : undefined,
38499
38605
  endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
package/build/index.js CHANGED
@@ -10142,6 +10142,7 @@ function defectReport(where, error, godotVersion) {
10142
10142
  import { execFile as execFile4, spawn } from "node:child_process";
10143
10143
  import { randomUUID as randomUUID2 } from "node:crypto";
10144
10144
  import {
10145
+ appendFileSync,
10145
10146
  closeSync,
10146
10147
  existsSync as existsSync9,
10147
10148
  fstatSync,
@@ -10151,7 +10152,8 @@ import {
10151
10152
  readdirSync as readdirSync6,
10152
10153
  readFileSync as readFileSync11,
10153
10154
  readSync,
10154
- rmSync as rmSync5
10155
+ rmSync as rmSync5,
10156
+ statSync as statSync4
10155
10157
  } from "node:fs";
10156
10158
  import { tmpdir as tmpdir4 } from "node:os";
10157
10159
  import { basename as basename2, dirname as dirname4, join as join11, normalize as normalize2 } from "node:path";
@@ -27545,6 +27547,15 @@ function isValueComplete(value) {
27545
27547
  }
27546
27548
  return depth <= 0 && !inString;
27547
27549
  }
27550
+ function settingKeys(content) {
27551
+ const named = new Set;
27552
+ for (const [section, entries] of Object.entries(parseProjectGodot(content))) {
27553
+ for (const key of Object.keys(entries)) {
27554
+ named.add(`${section}/${key}`);
27555
+ }
27556
+ }
27557
+ return named;
27558
+ }
27548
27559
  function parseProjectGodot(content) {
27549
27560
  const result = emptyRecord();
27550
27561
  let section = emptyRecord();
@@ -27973,6 +27984,41 @@ function writeRunRecord(record) {
27973
27984
  mkdirSync3(runsDirectory(), { recursive: true });
27974
27985
  writeFileSync4(recordPath(), JSON.stringify(record, null, 2), "utf8");
27975
27986
  }
27987
+ function editorNotePath() {
27988
+ return join9(runsDirectory(), "editor-run.json");
27989
+ }
27990
+ function writeEditorRunNote(note) {
27991
+ mkdirSync3(runsDirectory(), { recursive: true });
27992
+ writeFileSync4(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
27993
+ }
27994
+ function readEditorRunNote() {
27995
+ for (const directory of runtimeDirectories()) {
27996
+ const note = editorNoteAt(join9(directory, "runs", "editor-run.json"));
27997
+ if (note !== null) {
27998
+ return note;
27999
+ }
28000
+ }
28001
+ return null;
28002
+ }
28003
+ function editorNoteAt(path) {
28004
+ let parsed;
28005
+ try {
28006
+ parsed = JSON.parse(readFileSync9(path, "utf8"));
28007
+ } catch {
28008
+ return null;
28009
+ }
28010
+ if (!parsed || typeof parsed !== "object") {
28011
+ return null;
28012
+ }
28013
+ const fields = parsed;
28014
+ const projectPath = fields["projectPath"];
28015
+ const transcript = fields["transcript"];
28016
+ const startedAt = fields["startedAt"];
28017
+ if (typeof projectPath !== "string" || typeof transcript !== "string" || typeof startedAt !== "number") {
28018
+ return null;
28019
+ }
28020
+ return { projectPath, transcript, startedAt };
28021
+ }
27976
28022
  function recordRunEnded(pid, exitCode) {
27977
28023
  const path = recordPath();
27978
28024
  const record = recordAt(path);
@@ -28745,7 +28791,7 @@ var TOOL_SPECS = [
28745
28791
  },
28746
28792
  {
28747
28793
  name: "editor_launch",
28748
- description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon.",
28794
+ description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. A restart saves project.godot on the way out, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped: settingsDropped names any that went, because nothing else will say so until something depends on one.",
28749
28795
  parameters: { projectPath: PROJECT_PATH },
28750
28796
  requires: [],
28751
28797
  operations: {
@@ -30660,6 +30706,16 @@ class GodotServer {
30660
30706
  }
30661
30707
  });
30662
30708
  }
30709
+ settingKeysOf(projectPath) {
30710
+ if (projectPath === undefined) {
30711
+ return new Set;
30712
+ }
30713
+ try {
30714
+ return settingKeys(readFileSync11(join11(projectPath, "project.godot"), "utf8"));
30715
+ } catch {
30716
+ return new Set;
30717
+ }
30718
+ }
30663
30719
  async handleRestartEditor() {
30664
30720
  const before = this.godotBridge.getStatus();
30665
30721
  if (!before.connected) {
@@ -30668,6 +30724,7 @@ class GodotServer {
30668
30724
  "editor_status says whether the bridge is up and what has reached it"
30669
30725
  ]);
30670
30726
  }
30727
+ const settingsBefore = this.settingKeysOf(before.projectPath);
30671
30728
  const mine = before.openedByAServer === true && before.projectPath !== undefined;
30672
30729
  const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid) : await this.handleViaBridge("restart_editor", {});
30673
30730
  if (asked.isError === true) {
@@ -30687,6 +30744,8 @@ class GodotServer {
30687
30744
  ]);
30688
30745
  }
30689
30746
  const now = this.godotBridge.getStatus();
30747
+ const after = this.settingKeysOf(before.projectPath);
30748
+ const dropped = [...settingsBefore].filter((key) => !after.has(key));
30690
30749
  return this.jsonTextResponse({
30691
30750
  restarted: true,
30692
30751
  editorPid: now.editorPid,
@@ -30694,6 +30753,8 @@ class GodotServer {
30694
30753
  serverVersion: SERVER_VERSION,
30695
30754
  addonIsStale: now.addonVersion !== SERVER_VERSION,
30696
30755
  staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
30756
+ settingsDropped: dropped.length > 0 ? dropped : undefined,
30757
+ settingsNote: dropped.length > 0 ? 'The editor saved project.godot on its way out and these keys are no longer in it. 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. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, put them back: nothing else will say they have gone until something depends on one.' : undefined,
30697
30758
  tookMs: Date.now() - began
30698
30759
  });
30699
30760
  }
@@ -30884,6 +30945,7 @@ class GodotServer {
30884
30945
  const log = new GameLog;
30885
30946
  try {
30886
30947
  await this.dap().connect();
30948
+ this.dapClient?.getOutput(true);
30887
30949
  } catch (error) {
30888
30950
  return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
30889
30951
  `This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
@@ -30894,19 +30956,24 @@ class GodotServer {
30894
30956
  if (answer.isError === true) {
30895
30957
  return answer;
30896
30958
  }
30959
+ const startedAt = Date.now();
30960
+ const transcript = openTranscript(startedAt);
30961
+ closeSync(transcript.fd);
30897
30962
  const played = {
30898
30963
  process: null,
30899
30964
  pid: null,
30900
30965
  log,
30901
- transcript: null,
30966
+ transcript: transcript.path,
30902
30967
  readOffset: 0,
30903
30968
  projectPath,
30904
- startedAt: Date.now(),
30969
+ startedAt,
30905
30970
  exitCode: null,
30906
30971
  throughEditor: true,
30907
30972
  brokeOn: null
30908
30973
  };
30909
30974
  this.activeProcess = played;
30975
+ writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
30976
+ sweepTranscripts();
30910
30977
  return this.jsonTextResponse({
30911
30978
  started: true,
30912
30979
  through: "editor",
@@ -30922,10 +30989,24 @@ class GodotServer {
30922
30989
  if (!game.throughEditor || !this.dapClient) {
30923
30990
  return;
30924
30991
  }
30992
+ const heard = [];
30925
30993
  for (const line of this.dapClient.getOutput(true)) {
30926
- game.log.append("stdout", line.endsWith(`
30994
+ const ended = line.endsWith(`
30927
30995
  `) ? line : `${line}
30928
- `);
30996
+ `;
30997
+ game.log.append("stdout", ended);
30998
+ heard.push(ended);
30999
+ }
31000
+ if (heard.length > 0 && game.transcript !== null) {
31001
+ try {
31002
+ appendFileSync(game.transcript, heard.join(""));
31003
+ game.readOffset = statSync4(game.transcript).size;
31004
+ } catch (error) {
31005
+ this.logDebug(`Could not write an editor-played run's transcript: ${errorMessage(error)}`);
31006
+ }
31007
+ }
31008
+ if (!this.dapClient.isConnected()) {
31009
+ game.consoleLost = true;
30929
31010
  }
30930
31011
  this.recordWhatItBrokeOn(game);
30931
31012
  }
@@ -31079,7 +31160,20 @@ class GodotServer {
31079
31160
  }
31080
31161
  }
31081
31162
  async pickUpWhatTheEditorIsPlaying() {
31082
- if (this.activeProcess !== null || !this.godotBridge.isConnected()) {
31163
+ if (!this.godotBridge.isConnected()) {
31164
+ return;
31165
+ }
31166
+ const going = this.activeProcess;
31167
+ if (going !== null) {
31168
+ if (going.throughEditor && going.consoleLost === true && (await this.editorPlayingState())?.playing) {
31169
+ try {
31170
+ await this.dap().connect();
31171
+ going.log.append("stdout", `[gdharness] the debug adapter was reconnected here
31172
+ `);
31173
+ } catch (error) {
31174
+ this.logDebug(`Could not reconnect the adapter for a playing run: ${errorMessage(error)}`);
31175
+ }
31176
+ }
31083
31177
  return;
31084
31178
  }
31085
31179
  const playing = await this.editorPlayingState();
@@ -31091,19 +31185,23 @@ class GodotServer {
31091
31185
  } catch (error) {
31092
31186
  this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
31093
31187
  }
31094
- this.activeProcess = {
31188
+ const project = this.godotBridge.getStatus().projectPath ?? null;
31189
+ const left = readEditorRunNote();
31190
+ const note = left !== null && project !== null && isSameDirectory(left.projectPath, project) ? left : null;
31191
+ const picked = {
31095
31192
  process: null,
31096
31193
  pid: null,
31097
31194
  log: new GameLog,
31098
- transcript: null,
31195
+ transcript: note?.transcript ?? null,
31099
31196
  readOffset: 0,
31100
- projectPath: this.godotBridge.getStatus().projectPath ?? null,
31101
- startedAt: Date.now(),
31197
+ projectPath: project,
31198
+ startedAt: note?.startedAt ?? Date.now(),
31102
31199
  exitCode: null,
31103
31200
  throughEditor: true,
31104
31201
  brokeOn: null,
31105
31202
  pickedUpPlaying: true
31106
31203
  };
31204
+ this.activeProcess = picked;
31107
31205
  }
31108
31206
  adoptRecordedRun() {
31109
31207
  const record = readRunRecord();
@@ -31223,6 +31321,9 @@ class GodotServer {
31223
31321
  const wanted = readBoolean(args, "cpu") ?? false;
31224
31322
  const cpuSeconds = wanted && run.pid !== null && stillRunning(run) ? await cpuSecondsOf(run.pid) : undefined;
31225
31323
  const notes = [];
31324
+ if (wanted && cpuSeconds === undefined && stillRunning(run)) {
31325
+ notes.push(run.pid === null ? "cpu was asked for and there is no process to ask: the editor is playing this run and holds it, so nothing here has its process id. editor_run start without a connected editor, or with args, starts the game here instead and that run answers cpuSeconds." : "cpu was asked for and this platform would not say what the run has used.");
31326
+ }
31226
31327
  if (waitedMs !== undefined) {
31227
31328
  notes.push(`The wait of ${waitedMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
31228
31329
  }
@@ -31230,7 +31331,11 @@ class GodotServer {
31230
31331
  notes.push("This run outlived the server that started it and is over now, so its exit code was never collected. Everything it printed is below, read back from its transcript.");
31231
31332
  }
31232
31333
  if (run.pickedUpPlaying === true) {
31233
- notes.push("The editor was already playing this when this server reached it, so this is not the run from its start: what it printed before that went with the server that was listening then. It is the run the editor is holding now, which is the one the debug_* tools answer for.");
31334
+ notes.push(run.transcript === null ? "The editor was already playing this when this server reached it, and the server that started it left no transcript, so this is not the run from its start: what it printed before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for." : "The editor was already playing this when this server reached it, and what it printed before that has been read back from its transcript. It is the run the editor is holding now, which is the one the debug_* tools answer for.");
31335
+ }
31336
+ if (run.consoleLost === true) {
31337
+ const back = this.dapClient?.isConnected() === true;
31338
+ notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
31234
31339
  }
31235
31340
  if (typeof run.endedHere === "string") {
31236
31341
  notes.push(`This run was ended here, by ${run.endedHere}.`);
@@ -31247,7 +31352,8 @@ class GodotServer {
31247
31352
  pid: run.pid,
31248
31353
  errors: run.log.count("error"),
31249
31354
  warnings: run.log.count("warning"),
31250
- clean: run.log.count("error") === 0,
31355
+ clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
31356
+ consoleLost: run.consoleLost === true ? true : undefined,
31251
31357
  heldAt: halt,
31252
31358
  endedUnwatched: run.endedUnwatched === true ? true : undefined,
31253
31359
  endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.13.7",
3
+ "version": "0.13.8",
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",