gdharness 1.2.26 → 1.2.27

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/index.js CHANGED
@@ -27929,6 +27929,18 @@ class GodotDAPClient {
27929
27929
  lines: Array.from(lines).sort((a, b) => a - b)
27930
27930
  }));
27931
27931
  }
27932
+ forgetBreakpoint(scriptPath, line) {
27933
+ const key = fileKey(scriptPath);
27934
+ const lines = this.breakpoints.get(key);
27935
+ if (lines === undefined) {
27936
+ return;
27937
+ }
27938
+ lines.delete(line);
27939
+ if (lines.size === 0) {
27940
+ this.breakpoints.delete(key);
27941
+ this.spelled.delete(key);
27942
+ }
27943
+ }
27932
27944
  holdBreakpoints(held) {
27933
27945
  for (const { scriptPath, lines } of held) {
27934
27946
  const key = fileKey(scriptPath);
@@ -28707,6 +28719,22 @@ function howItEnded(run) {
28707
28719
  return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}`;
28708
28720
  }
28709
28721
 
28722
+ // src/godot-variant.ts
28723
+ var FLAG_64 = 1 << 16;
28724
+ var MOST_MESSAGE_BYTES = (8 << 20) + 4;
28725
+
28726
+ // src/game-debugger.ts
28727
+ var PAUSE_MS = 30000;
28728
+ var STEP_MS = 1e4;
28729
+ var READ_MS = 5000;
28730
+ var REPLIES = new Set([
28731
+ "debug_enter",
28732
+ "debug_exit",
28733
+ "stack_dump",
28734
+ "stack_frame_vars",
28735
+ "stack_frame_var"
28736
+ ]);
28737
+
28710
28738
  // src/godot-bridge.ts
28711
28739
  import { randomUUID } from "node:crypto";
28712
28740
  import { EventEmitter } from "node:events";
@@ -30340,7 +30368,31 @@ function testProgressMessage(printed, elapsedMs) {
30340
30368
 
30341
30369
  // src/keeper-channel.ts
30342
30370
  import { connect, createServer as createServer2 } from "node:net";
30343
- function askTheKeeperToStop(address, withinMs) {
30371
+ var DEBUG_PREFIX = "debug ";
30372
+ async function askTheKeeperToStop(address, withinMs) {
30373
+ const answered = await askTheKeeper(address, "stop", withinMs);
30374
+ if (typeof answered !== "string") {
30375
+ return answered;
30376
+ }
30377
+ return answered === "signalled" || answered === "gone" ? answered : { unreached: `it answered ${JSON.stringify(answered)}` };
30378
+ }
30379
+ async function askTheKeepersDebugger(address, request, withinMs) {
30380
+ const answered = await askTheKeeper(address, `${DEBUG_PREFIX}${JSON.stringify(request)}`, withinMs);
30381
+ if (typeof answered !== "string") {
30382
+ return answered;
30383
+ }
30384
+ if (answered === "unknown") {
30385
+ return { unreached: "its keeper was started by a gdharness from before game debuggers, and has none" };
30386
+ }
30387
+ try {
30388
+ const parsed = JSON.parse(answered);
30389
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
30390
+ return parsed;
30391
+ }
30392
+ } catch {}
30393
+ return { unreached: `it answered ${JSON.stringify(answered.slice(0, 200))}` };
30394
+ }
30395
+ function askTheKeeper(address, line, withinMs) {
30344
30396
  return new Promise((resolve) => {
30345
30397
  let said = "";
30346
30398
  let settled = false;
@@ -30358,16 +30410,15 @@ function askTheKeeperToStop(address, withinMs) {
30358
30410
  }, withinMs);
30359
30411
  socket.setEncoding("utf8");
30360
30412
  socket.on("connect", () => {
30361
- socket.write(`stop
30413
+ socket.write(`${line}
30362
30414
  `);
30363
30415
  });
30364
30416
  socket.on("data", (chunk) => {
30365
30417
  said += chunk;
30366
- const line = said.split(`
30367
- `)[0] ?? "";
30368
30418
  if (said.includes(`
30369
30419
  `)) {
30370
- settle(line === "signalled" || line === "gone" ? line : { unreached: `it answered ${JSON.stringify(line)}` });
30420
+ settle(said.split(`
30421
+ `)[0] ?? "");
30371
30422
  }
30372
30423
  });
30373
30424
  socket.on("error", (error) => {
@@ -33868,12 +33919,6 @@ function withoutListed(entries, errors) {
33868
33919
 
33869
33920
  // src/script-profile.ts
33870
33921
  import { readFileSync as readFileSync19 } from "node:fs";
33871
-
33872
- // src/godot-variant.ts
33873
- var FLAG_64 = 1 << 16;
33874
- var MOST_MESSAGE_BYTES = (8 << 20) + 4;
33875
-
33876
- // src/script-profile.ts
33877
33922
  function readProfileFile(path) {
33878
33923
  try {
33879
33924
  const parsed = JSON.parse(readFileSync19(path, "utf8"));
@@ -34992,7 +35037,7 @@ var TOOL_SPECS = [
34992
35037
  type: "array",
34993
35038
  items: { type: "string" },
34994
35039
  ops: ["start", "check"],
34995
- description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here, so a flag the engine reads goes to the game instead, and the answer says so under argsNote. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
35040
+ description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here, so a flag the engine reads goes to the game instead, and the answer says so under argsNote. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so its debugger is the keeper holding it rather than the editor; the debug_* tools answer for it all the same.`
34996
35041
  },
34997
35042
  engineArgs: {
34998
35043
  type: "array",
@@ -35009,7 +35054,7 @@ var TOOL_SPECS = [
35009
35054
  silent: {
35010
35055
  type: "boolean",
35011
35056
  ops: ["start"],
35012
- description: "start: run the game with no sound out of the machine, by starting the engine with --audio-driver Dummy: every stream still plays and every playback position still advances, so what is playing can still be asked. The editor takes no engine flags for a game it plays, so a silent windowed run is started by this server, and the debug_* tools do not answer for it. A headless run, check's included, is silent already. Default false, or true when GDHARNESS_SILENT is set in the server's environment to anything but 0 or false; silent: false then plays through the editor as before."
35057
+ description: "start: run the game with no sound out of the machine, by starting the engine with --audio-driver Dummy: every stream still plays and every playback position still advances, so what is playing can still be asked. The editor takes no engine flags for a game it plays, so a silent windowed run is started by this server, whose keeper is its debugger rather than the editor. A headless run, check's included, is silent already. Default false, or true when GDHARNESS_SILENT is set in the server's environment to anything but 0 or false; silent: false then plays through the editor as before."
35013
35058
  },
35014
35059
  savesIn: {
35015
35060
  type: "string",
@@ -35690,7 +35735,7 @@ var TOOL_SPECS = [
35690
35735
  {
35691
35736
  name: "debug_breakpoint",
35692
35737
  title: "Breakpoints",
35693
- description: "Sets or removes a breakpoint through the editor's debug adapter. Needs the editor, not a running game: set them first, then editor_run, and the game stops where you asked. The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor running an older addon is said so under breakpointsAtRisk, here and in editor_status.",
35738
+ description: "Sets or removes a breakpoint. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it at once (runningGame says so). The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor running an older addon is said so under breakpointsAtRisk, here and in editor_status.",
35694
35739
  parameters: {
35695
35740
  projectPath: PROJECT_PATH2,
35696
35741
  scriptPath: SCRIPT_PATH,
@@ -35705,23 +35750,33 @@ var TOOL_SPECS = [
35705
35750
  {
35706
35751
  name: "debug_control",
35707
35752
  title: "Step the debugger",
35708
- description: "Continues or steps the debugged game through the editor's debug adapter. A step waits for the game to stop again and answers with the stack where it ended up; one that has not stopped the game again within ten seconds answers heldAgain false and no stack. There is no pause and no step_out: Godot's adapter answers a pause by reporting the game stopped and leaving it running, and implements no stepOut at all, so hold the game where you want it with a breakpoint and step over or into from there.",
35753
+ description: "Pauses, continues or steps the debugged game. A step waits for the game to stop again and answers with the stack where it ended up; one that has not stopped the game again within ten seconds answers heldAgain false and no stack. A game the editor plays is moved through the editor's debug adapter, which cannot pause or step out: it answers a pause by reporting the game stopped and leaving it running, and implements no stepOut, so hold such a game with a breakpoint and step over or into from there. A game this server started itself has its keeper as its debugger, which does both: pause breaks into the game wherever it is, a script spinning in a loop included, which is how a game hung on its first frame is asked where it is.",
35709
35754
  parameters: {},
35710
35755
  requires: [],
35711
35756
  operations: {
35757
+ pause: {
35758
+ effect: "additive",
35759
+ summary: "stop a game this server started where it is, answering with the stack. The game takes it between frames, and in a script every 2048 lines it runs, so a loop that sleeps between few lines can take seconds; the call waits thirty, and past that answers paused false with the request still standing. Stopped between frames, where no script runs, the game is stepped on to the next line of script it runs",
35760
+ requires: []
35761
+ },
35712
35762
  continue: { effect: "additive", summary: "resume after a breakpoint", requires: [] },
35713
35763
  step_over: { effect: "additive", summary: "run the current line", requires: [] },
35714
35764
  step_into: {
35715
35765
  effect: "additive",
35716
35766
  summary: "run the current line, stopping inside whatever it calls",
35717
35767
  requires: []
35768
+ },
35769
+ step_out: {
35770
+ effect: "additive",
35771
+ summary: "run to the end of the current function and stop in its caller; a game this server started only",
35772
+ requires: []
35718
35773
  }
35719
35774
  }
35720
35775
  },
35721
35776
  {
35722
35777
  name: "debug_state",
35723
35778
  title: "Debugger state",
35724
- description: "Where the debugged game is stopped: the stack trace, what is in scope at a frame with the values, or the debug adapter's console output so far, its last thousand lines, with any earlier ones counted under notShown. A stop is reported by the editor's adapter only to the sessions connected when it happened, so a server that connected afterwards, which is every server after a reconnect, is shown a thread and no frames about a game still sitting at its breakpoint. That server asks the runtime instead: a held game accepts a connection and never answers, and the refusal then says held rather than running, and that the stack cannot be read from here. debug_control continue lets it go, and the next stop is one this session is told about, with its stack.",
35779
+ description: "Where the debugged game is stopped: the stack trace, what is in scope at a frame with the values, or the debugger's console output so far, its last thousand lines, with any earlier ones counted under notShown. For a game this server started, its keeper's debugger answers: each frame names its script as the project spells it under script and on this machine under path, and held says which thread stopped and why. For a game the editor plays, the editor's debug adapter answers. A stop is reported by the editor's adapter only to the sessions connected when it happened, so a server that connected afterwards, which is every server after a reconnect, is shown a thread and no frames about a game still sitting at its breakpoint. That server asks the runtime instead: a held game accepts a connection and never answers, and the refusal then says held rather than running, and that the stack cannot be read from here. debug_control continue lets it go, and the next stop is one this session is told about, with its stack.",
35725
35780
  parameters: {
35726
35781
  frameId: {
35727
35782
  type: "number",
@@ -35811,7 +35866,7 @@ function serverInstructions() {
35811
35866
  "Call editor_status first: it says whether an editor is connected and whether its addon matches this server.",
35812
35867
  "Start the game with editor_run start, never by spawning an engine, and read editor_output after every run:",
35813
35868
  "its clean verdict and its error entries are how a run is judged, and editor_run wait is how to wait for one.",
35814
- "The runtime_* tools work on the running game and the debug_* tools on the editor's debugger.",
35869
+ "The runtime_* tools work on the running game and the debug_* tools on its debugger.",
35815
35870
  "A refusal names what would have worked, so read it before calling again."
35816
35871
  ].join(" ");
35817
35872
  }
@@ -36152,6 +36207,31 @@ function longerWaitNote(tool, op, waitedMs) {
36152
36207
  const how = tool === "runtime_wait" && op === "frames" ? `the wait is counted from the number of frames, at ${SLOWEST_FRAME_RATE} a second on top of GDHARNESS_RUNTIME_TIMEOUT_MS: a game drawing frames more slowly than that is waited on in fewer frames at a time, and a game inside one long frame, such as a long _ready, answers no call however few frames are asked for` : "the wait is GDHARNESS_RUNTIME_TIMEOUT_MS, which is set in the environment the server is started with";
36153
36208
  return `${tool} ${op} takes no timeoutMs: it was given ${waitedMs}ms, and ${how}.`;
36154
36209
  }
36210
+ function keptGameRequest(tool, op, args) {
36211
+ if (tool === "debug_state") {
36212
+ if (op === "output") {
36213
+ return { op: "output" };
36214
+ }
36215
+ if (op === "variables") {
36216
+ const frame = args["frameId"];
36217
+ return { op: "variables", frame: typeof frame === "number" ? frame : 0 };
36218
+ }
36219
+ return { op: "stack" };
36220
+ }
36221
+ switch (op) {
36222
+ case "pause":
36223
+ return { op: "pause" };
36224
+ case "step_over":
36225
+ return { op: "resume", how: "next" };
36226
+ case "step_into":
36227
+ return { op: "resume", how: "step" };
36228
+ case "step_out":
36229
+ return { op: "resume", how: "out" };
36230
+ default:
36231
+ return { op: "resume", how: "continue" };
36232
+ }
36233
+ }
36234
+ var OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.";
36155
36235
  function projectSpelling(projectPath, absolutePath) {
36156
36236
  return `res://${relative4(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
36157
36237
  }
@@ -36239,6 +36319,7 @@ function stopWaitMs() {
36239
36319
  return waitFromEnv("GDHARNESS_STOP_WAIT_MS", 1e4);
36240
36320
  }
36241
36321
  var KEEPER_ANSWER_MS = 15000;
36322
+ var KEEPER_DEBUG_MS = PAUSE_MS + STEP_MS + 2 * READ_MS;
36242
36323
  var EXIT_REPORTED_WITHIN_MS = 2000;
36243
36324
  async function untilGone(pids, withinMs = stopWaitMs()) {
36244
36325
  const gone = () => pids.every((pid) => !alive(pid));
@@ -38139,28 +38220,70 @@ class GodotServer {
38139
38220
  return this.createErrorResponse(located.reason, PATH_SOLUTIONS);
38140
38221
  }
38141
38222
  const session = this.dapHoldingBreakpointsOf(project.value.path);
38142
- const answered = await this.handleDAP(op === "set" ? "dap_set_breakpoint" : "dap_remove_breakpoint", {
38143
- ...args,
38144
- scriptPath: realPathOr(located.absolutePath)
38145
- });
38146
- if (answered.isError === true) {
38147
- return answered;
38223
+ const scriptPath = realPathOr(located.absolutePath);
38224
+ const line = readPositiveNumber(args, "line");
38225
+ if (line === undefined) {
38226
+ return this.createErrorResponse("line is required: the one-based line to stop on.");
38227
+ }
38228
+ const editor = this.godotBridge.isConnected();
38229
+ let answer;
38230
+ if (editor) {
38231
+ const answered = await this.handleDAP(op === "set" ? "dap_set_breakpoint" : "dap_remove_breakpoint", {
38232
+ ...args,
38233
+ scriptPath
38234
+ });
38235
+ if (answered.isError === true) {
38236
+ return answered;
38237
+ }
38238
+ answer = asParams(JSON.parse(answered.content[0]?.text ?? "{}"));
38239
+ } else {
38240
+ if (op === "set") {
38241
+ session.holdBreakpoints([{ scriptPath, lines: [line] }]);
38242
+ } else {
38243
+ session.forgetBreakpoint(scriptPath, line);
38244
+ }
38245
+ answer = { [op === "set" ? "set" : "removed"]: true };
38148
38246
  }
38149
38247
  const held = session.breakpointsHeld();
38150
38248
  writeBreakpointNote(project.value.path, held);
38249
+ const kept = this.keptRun();
38250
+ const told = kept === null ? null : await askTheKeepersDebugger(kept.keeper, {
38251
+ op: "breakpoint",
38252
+ script: projectSpelling(project.value.path, scriptPath),
38253
+ line,
38254
+ set: op === "set"
38255
+ }, KEEPER_DEBUG_MS);
38151
38256
  return this.jsonTextResponse({
38152
- ...asParams(JSON.parse(answered.content[0]?.text ?? "{}")),
38257
+ ...answer,
38153
38258
  held: this.breakpointsAsProjectSpells(project.value.path, held),
38154
- setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor()),
38155
- note: "The editor keeps a breakpoint set this way for one play. Every breakpoint held here is sent again before each play editor_run starts, and kept in the project for the server after a reconnect.",
38156
- ...this.breakpointsAtRisk()
38259
+ ...editor ? {
38260
+ setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor())
38261
+ } : {},
38262
+ ...told === null ? {} : ("unreached" in told) || typeof told["refused"] === "string" ? {
38263
+ runningGame: `The game this server started is running, and its debugger was not told: ${String("unreached" in told ? told.unreached : told["refused"])}. It takes the line when it is started again.`
38264
+ } : {
38265
+ runningGame: `Told to the running game (pid ${kept?.pid ?? "?"}), which this server started.`
38266
+ },
38267
+ note: editor ? "The editor keeps a breakpoint set this way for one play. Every breakpoint held here is sent again before each play editor_run starts, handed to every game this server starts itself, and kept in the project for the server after a reconnect." : "No editor is connected, so the set under held is kept here: every game this server starts is handed it as it starts, an editor that connects is sent it before its first play, and it is kept in the project for the server after a reconnect.",
38268
+ ...editor ? this.breakpointsAtRisk() : {}
38157
38269
  });
38158
38270
  }
38159
38271
  case "debug_control":
38160
38272
  case "debug_state": {
38273
+ const kept = this.keptRun();
38274
+ if (kept !== null) {
38275
+ return await this.askTheKeptGame(kept.keeper, keptGameRequest(tool, op, args), kept.projectPath ?? "");
38276
+ }
38161
38277
  if (tool === "debug_state" && op === "output") {
38162
38278
  return await this.handleDAP("dap_get_output", args);
38163
38279
  }
38280
+ const played = this.activeProcess;
38281
+ if ((op === "pause" || op === "step_out") && played?.throughEditor === true && !exited(played)) {
38282
+ return this.createErrorResponse(op === "pause" ? "A game the editor plays cannot be paused from here: Godot's debug adapter answers a pause by reporting the game stopped and leaving it running." : "A game the editor plays cannot be stepped out of a function from here: Godot's debug adapter implements no stepOut.", [
38283
+ "Set a breakpoint where it should stop, then step over or into from there",
38284
+ "A game this server starts, as editor_run start does with args, savesIn or env, has a debugger here that pauses and steps out"
38285
+ ]);
38286
+ }
38164
38287
  const held = await this.debuggedGame(tool === "debug_control" ? "move" : "read");
38165
38288
  if (!held.ok) {
38166
38289
  return held.response;
@@ -38304,8 +38427,8 @@ class GodotServer {
38304
38427
  if (!game.throughEditor) {
38305
38428
  return {
38306
38429
  ok: false,
38307
- response: this.createErrorResponse("The running game is its own process, so no debugger is holding it: breakpoints never hit and there is no stack to read.", [
38308
- "editor_run start plays it through the open editor, whose debugger the debug_* tools speak to",
38430
+ response: this.createErrorResponse("The running game has no debugger: it was started with none, or by a keeper that could not take questions, so breakpoints never hit and there is no stack to read.", [
38431
+ "editor_run start starts it again, and the keeper holding a game this server starts is its debugger",
38309
38432
  "editor_status says whether an editor has reached this server"
38310
38433
  ])
38311
38434
  };
@@ -39516,6 +39639,38 @@ class GodotServer {
39516
39639
  breakpointsAtRisk: status.syncsBreakpoints === false ? "This editor clears every breakpoint in its script editor whenever a debug session opens on its adapter, which this server does for its first breakpoint or stack read: network/debug_adapter/sync_breakpoints is off. The installed addon turns it on as it loads, so the editor is running an older one; editor_launch restart puts the installed addon in, or turn the setting on in Editor Settings > Network > Debug Adapter." : "Whether this editor keeps its breakpoints when a debug session opens cannot be told: the addon it is running predates the report. Godot clears them unless network/debug_adapter/sync_breakpoints is on, which the installed addon turns on as it loads; editor_launch restart puts the installed addon in."
39517
39640
  };
39518
39641
  }
39642
+ breakpointsForAGame(projectPath) {
39643
+ return this.dapHoldingBreakpointsOf(projectPath).breakpointsHeld().flatMap((one) => one.lines.map((line) => `${projectSpelling(projectPath, one.scriptPath)}:${line}`));
39644
+ }
39645
+ keptRun() {
39646
+ const game = this.activeProcess;
39647
+ if (game === null || game.throughEditor || exited(game) || game.keeper === undefined) {
39648
+ return null;
39649
+ }
39650
+ return { ...game, keeper: game.keeper };
39651
+ }
39652
+ async askTheKeptGame(keeper, request, projectPath) {
39653
+ const answered = await askTheKeepersDebugger(keeper, request, KEEPER_DEBUG_MS);
39654
+ if ("unreached" in answered) {
39655
+ return this.createErrorResponse(`The debugger of the running game did not answer: ${String(answered.unreached)}.`, [
39656
+ "editor_run start starts the game again, with a debugger",
39657
+ "editor_status says whether it is still running"
39658
+ ]);
39659
+ }
39660
+ if (typeof answered["refused"] === "string") {
39661
+ return this.createErrorResponse(answered["refused"]);
39662
+ }
39663
+ const stack = answered["stack"];
39664
+ return this.jsonTextResponse({
39665
+ ...answered,
39666
+ ...Array.isArray(stack) ? {
39667
+ stack: stack.map((frame) => ({
39668
+ ...frame,
39669
+ ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join23(projectPath, frame["script"].slice("res://".length)) } : {}
39670
+ }))
39671
+ } : {}
39672
+ });
39673
+ }
39519
39674
  breakpointsAsProjectSpells(projectPath, held) {
39520
39675
  return held.map((one) => ({
39521
39676
  scriptPath: projectSpelling(projectPath, one.scriptPath),
@@ -40177,9 +40332,10 @@ class GodotServer {
40177
40332
  started.announcedBefore = alreadyPlaying;
40178
40333
  started.bootScene = runtimeWaitMs.scene;
40179
40334
  this.activeProcess = started;
40180
- const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && (given.value.length > 0 || flags.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." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
40335
+ const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && (given.value.length > 0 || flags.value.length > 0) ? ` The editor cannot be handed arguments for a game it plays, so this one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? ` The editor plays a game with its own environment, so a run given one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}, and silent: false plays it through the editor.${OWN_DEBUGGER}` : "";
40181
40336
  const endedForThis = endedToStartThis(ended);
40182
40337
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
40338
+ const handed = this.dapHoldingBreakpointsOf(project.value.path).breakpointsHeld();
40183
40339
  const alsoRunning = leftRunningNote(stranded);
40184
40340
  return this.jsonTextResponse({
40185
40341
  started: true,
@@ -40189,6 +40345,7 @@ class GodotServer {
40189
40345
  arguments: cmdArgs,
40190
40346
  argsNote: engineFlagsNote(given.value),
40191
40347
  transcript: started.transcript,
40348
+ breakpoints: started.keeper === undefined || handed.length === 0 ? undefined : this.breakpointsAsProjectSpells(project.value.path, handed),
40192
40349
  profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
40193
40350
  refreshedClasses: refreshed.value,
40194
40351
  ...scanWaitAnswer(scanned),
@@ -40197,7 +40354,7 @@ class GodotServer {
40197
40354
  previousRunLeft: previousRunLeft(ended),
40198
40355
  ...previousRunHow(ended),
40199
40356
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
40200
- message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
40357
+ message: `Use editor_output for what it prints and editor_run stop to end it.${started.keeper !== undefined && handed.length > 0 ? " It stops at the lines under breakpoints, and debug_state stack reads where it is held." : ""}${spawnedInstead}${endedForThis}${alsoRunning}`
40201
40358
  });
40202
40359
  }
40203
40360
  async runtimeUp(projectPath, before, budget, withArgs) {
@@ -40724,7 +40881,8 @@ class GodotServer {
40724
40881
  startedAt,
40725
40882
  projectPath,
40726
40883
  ...this.servedBy(),
40727
- ...profile === undefined ? {} : { profile }
40884
+ ...profile === undefined ? {} : { profile },
40885
+ breakpoints: this.breakpointsForAGame(projectPath)
40728
40886
  },
40729
40887
  ...desktop === undefined ? {} : { desktop }
40730
40888
  });