gdharness 1.2.25 → 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
@@ -24933,12 +24933,27 @@ var ENGINE_FLAGS = Object.freeze({
24933
24933
  "--fixed-fps": "number",
24934
24934
  "--max-fps": "number",
24935
24935
  "--resolution": "size",
24936
- "--write-movie": "file"
24936
+ "--write-movie": "file",
24937
+ "--rendering-driver": "driver",
24938
+ "--rendering-method": "method",
24939
+ "--accessibility": "accessibility"
24937
24940
  });
24938
24941
  var VALUE_KIND = {
24939
24942
  number: { pattern: /^\d+(?:\.\d+)?$/, named: "a number" },
24940
24943
  size: { pattern: /^\d+x\d+$/, named: "a size such as 1280x720" },
24941
- file: { pattern: /^[^-]/, named: "a file path" }
24944
+ file: { pattern: /^[^-]/, named: "a file path" },
24945
+ driver: {
24946
+ pattern: /^(?:vulkan|d3d12|metal|opengl3|opengl3_es|opengl3_angle|dummy)$/,
24947
+ named: "a rendering driver: vulkan, d3d12, metal, opengl3, opengl3_es, opengl3_angle or dummy"
24948
+ },
24949
+ method: {
24950
+ pattern: /^(?:forward_plus|mobile|gl_compatibility)$/,
24951
+ named: "a rendering method: forward_plus, mobile or gl_compatibility"
24952
+ },
24953
+ accessibility: {
24954
+ pattern: /^(?:auto|always|disabled)$/,
24955
+ named: "an accessibility mode: auto, always or disabled"
24956
+ }
24942
24957
  };
24943
24958
  var SET_BY_ARGUMENT = Object.freeze({
24944
24959
  "--headless": "headless",
@@ -24954,7 +24969,7 @@ function engineFlags(given) {
24954
24969
  const setBy = Object.hasOwn(SET_BY_ARGUMENT, flag) ? SET_BY_ARGUMENT[flag] : undefined;
24955
24970
  return {
24956
24971
  ok: false,
24957
- reason: setBy !== undefined ? `${flag} is set by this tool's own argument, ${setBy}, rather than through engineArgs.` : `${flag} is not one of the engine flags engineArgs takes, which are those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames, and Movie Maker's --write-movie: ${offered}. A value goes as the next entry, as in ["--fixed-fps", "60"].`
24972
+ reason: setBy !== undefined ? `${flag} is set by this tool's own argument, ${setBy}, rather than through engineArgs.` : `${flag} is not one of the engine flags engineArgs takes, which are those that change what the engine logs, draws for debugging, checks on the GPU, which renderer draws, how it times frames or whether it builds the tree a screen reader reads, and Movie Maker's --write-movie: ${offered}. A value goes as the next entry, as in ["--fixed-fps", "60"].`
24958
24973
  };
24959
24974
  }
24960
24975
  if (takes === "none") {
@@ -27914,6 +27929,18 @@ class GodotDAPClient {
27914
27929
  lines: Array.from(lines).sort((a, b) => a - b)
27915
27930
  }));
27916
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
+ }
27917
27944
  holdBreakpoints(held) {
27918
27945
  for (const { scriptPath, lines } of held) {
27919
27946
  const key = fileKey(scriptPath);
@@ -28692,6 +28719,22 @@ function howItEnded(run) {
28692
28719
  return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}`;
28693
28720
  }
28694
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
+
28695
28738
  // src/godot-bridge.ts
28696
28739
  import { randomUUID } from "node:crypto";
28697
28740
  import { EventEmitter } from "node:events";
@@ -29643,7 +29686,7 @@ function reported2(problems) {
29643
29686
  return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
29644
29687
  }
29645
29688
  function reason(log, problems) {
29646
- const own = log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
29689
+ const own = refusalsIn(log);
29647
29690
  if (own.length > 0) {
29648
29691
  return own.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; ");
29649
29692
  }
@@ -29655,6 +29698,9 @@ function reason(log, problems) {
29655
29698
  const printed = log.everything().filter((entry) => entry.source === "stdout");
29656
29699
  return printed.at(-1)?.text ?? "no output at all";
29657
29700
  }
29701
+ function refusalsIn(log) {
29702
+ return log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
29703
+ }
29658
29704
  function scriptFault(problems, script) {
29659
29705
  const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
29660
29706
  const directories = [...new Set([spelled(dirname8(script)), spelled(resolvedOr(dirname8(script)))])];
@@ -29774,6 +29820,15 @@ async function runOperation(engine, operation, params, projectPath) {
29774
29820
  };
29775
29821
  }
29776
29822
  if (answer.kind === "none") {
29823
+ const refusals = refusalsIn(ran.log);
29824
+ if (refusals.length > 0) {
29825
+ const said = new Set(refusals.map((entry) => entry.index));
29826
+ return {
29827
+ ok: false,
29828
+ message: refusals.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; "),
29829
+ ...reported2(problems.filter((entry) => !said.has(entry.index)))
29830
+ };
29831
+ }
29777
29832
  const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
29778
29833
  return {
29779
29834
  ok: false,
@@ -30313,7 +30368,31 @@ function testProgressMessage(printed, elapsedMs) {
30313
30368
 
30314
30369
  // src/keeper-channel.ts
30315
30370
  import { connect, createServer as createServer2 } from "node:net";
30316
- 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) {
30317
30396
  return new Promise((resolve) => {
30318
30397
  let said = "";
30319
30398
  let settled = false;
@@ -30331,16 +30410,15 @@ function askTheKeeperToStop(address, withinMs) {
30331
30410
  }, withinMs);
30332
30411
  socket.setEncoding("utf8");
30333
30412
  socket.on("connect", () => {
30334
- socket.write(`stop
30413
+ socket.write(`${line}
30335
30414
  `);
30336
30415
  });
30337
30416
  socket.on("data", (chunk) => {
30338
30417
  said += chunk;
30339
- const line = said.split(`
30340
- `)[0] ?? "";
30341
30418
  if (said.includes(`
30342
30419
  `)) {
30343
- settle(line === "signalled" || line === "gone" ? line : { unreached: `it answered ${JSON.stringify(line)}` });
30420
+ settle(said.split(`
30421
+ `)[0] ?? "");
30344
30422
  }
30345
30423
  });
30346
30424
  socket.on("error", (error) => {
@@ -33841,12 +33919,6 @@ function withoutListed(entries, errors) {
33841
33919
 
33842
33920
  // src/script-profile.ts
33843
33921
  import { readFileSync as readFileSync19 } from "node:fs";
33844
-
33845
- // src/godot-variant.ts
33846
- var FLAG_64 = 1 << 16;
33847
- var MOST_MESSAGE_BYTES = (8 << 20) + 4;
33848
-
33849
- // src/script-profile.ts
33850
33922
  function readProfileFile(path) {
33851
33923
  try {
33852
33924
  const parsed = JSON.parse(readFileSync19(path, "utf8"));
@@ -34221,7 +34293,7 @@ var TOOL_SPECS = [
34221
34293
  {
34222
34294
  name: "project_settings",
34223
34295
  title: "Project settings",
34224
- description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
34296
+ description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change touches only its own lines. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
34225
34297
  parameters: {
34226
34298
  projectPath: PROJECT_PATH2,
34227
34299
  setting: {
@@ -34406,7 +34478,7 @@ var TOOL_SPECS = [
34406
34478
  },
34407
34479
  options: {
34408
34480
  type: "object",
34409
- description: 'set_options: import options keyed as the .import file spells them, {"compress/mode": 1}.',
34481
+ description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads.`,
34410
34482
  additionalProperties: true
34411
34483
  },
34412
34484
  reimport: {
@@ -34494,7 +34566,7 @@ var TOOL_SPECS = [
34494
34566
  engineArgs: {
34495
34567
  type: "array",
34496
34568
  items: { type: "string" },
34497
- description: `The engine's own flags for the run, such as ["--verbose"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused. With --verbose, engineEntries holds the ordinary lines the run printed as well, which is where the engine names the instances leaked and the resources still in use at exit.`
34569
+ description: `The engine's own flags for the run, such as ["--verbose"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU, which renderer draws, how it times frames or whether it builds the tree a screen reader reads: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused. With --verbose, engineEntries holds the ordinary lines the run printed as well, which is where the engine names the instances leaked and the resources still in use at exit.`
34498
34570
  },
34499
34571
  failFast: {
34500
34572
  type: "boolean",
@@ -34965,13 +35037,13 @@ var TOOL_SPECS = [
34965
35037
  type: "array",
34966
35038
  items: { type: "string" },
34967
35039
  ops: ["start", "check"],
34968
- 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.`
34969
35041
  },
34970
35042
  engineArgs: {
34971
35043
  type: "array",
34972
35044
  items: { type: "string" },
34973
35045
  ops: ["start", "check"],
34974
- description: `The engine's own flags, before the separator, such as ["--verbose"] or ["--fixed-fps", "60"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames, and Movie Maker's --write-movie: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused, since the engine's command line can also run scripts, open other projects and write files. --write-movie takes an absolute path outside the project, in a directory that exists, ending in .avi (one file, video and sound) or .png (one numbered PNG per frame and a .wav beside them), where nothing it would write is there already; it needs a start with headless: false, since the engine crashes writing a movie headless, and it records at the project's editor/movie_writer/fps unless --fixed-fps says otherwise. A run with any is started by this server, as one with args is. A check given --verbose answers with everything printed rather than only errors and warnings, since what verbose adds, the instances leaked at exit among it, is printed as ordinary lines.`
35046
+ description: `The engine's own flags, before the separator, such as ["--verbose"] or ["--fixed-fps", "60"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU, which renderer draws, how it times frames or whether it builds the tree a screen reader reads, and Movie Maker's --write-movie: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused, since the engine's command line can also run scripts, open other projects and write files. --rendering-driver takes vulkan, d3d12, metal, opengl3, opengl3_es, opengl3_angle or dummy, and --rendering-method forward_plus, mobile or gl_compatibility, so the same scene can be measured on another driver and a project's OpenGL fallback exercised on a machine that has Vulkan; they choose what draws a run with a window. --accessibility always has the game build the tree a screen reader reads, as the project's accessibility/general/accessibility_support at Always Active does, so runtime_inspect accessibility can read it; otherwise the engine builds it only when a screen reader is running. --write-movie takes an absolute path outside the project, in a directory that exists, ending in .avi (one file, video and sound) or .png (one numbered PNG per frame and a .wav beside them), where nothing it would write is there already; it needs a start with headless: false, since the engine crashes writing a movie headless, and it records at the project's editor/movie_writer/fps unless --fixed-fps says otherwise. A run with any is started by this server, as one with args is. A check given --verbose answers with everything printed rather than only errors and warnings, since what verbose adds, the instances leaked at exit among it, is printed as ordinary lines.`
34975
35047
  },
34976
35048
  headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
34977
35049
  visible: {
@@ -34982,7 +35054,7 @@ var TOOL_SPECS = [
34982
35054
  silent: {
34983
35055
  type: "boolean",
34984
35056
  ops: ["start"],
34985
- 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."
34986
35058
  },
34987
35059
  savesIn: {
34988
35060
  type: "string",
@@ -35211,7 +35283,7 @@ var TOOL_SPECS = [
35211
35283
  {
35212
35284
  name: "runtime_inspect",
35213
35285
  title: "Inspect the running game",
35214
- description: "Questions about the running game: what is written on the screen, the scene tree, the nodes matching a query, where one node is on screen, what one property reads, or the performance metrics. Needs the game running with the runtime addon.",
35286
+ description: "Questions about the running game: what is written on the screen, the scene tree, the nodes matching a query, where one node is on screen, what one property reads, the performance metrics, or what it tells a screen reader. Needs the game running with the runtime addon.",
35215
35287
  parameters: {
35216
35288
  projectPath: RUNNING_PROJECT_PATH,
35217
35289
  pid: RUNNING_PID,
@@ -35280,7 +35352,7 @@ var TOOL_SPECS = [
35280
35352
  timeoutMs: {
35281
35353
  type: "integer",
35282
35354
  minimum: 1,
35283
- description: "How long to wait for the answer before answering pending with a requestId, which runtime_invoke op result collects the reply by. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS."
35355
+ description: "How long to wait for the answer before answering pending with a requestId, which runtime_invoke op result collects the reply by. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS; accessibility defaults to 30000 or that, whichever is longer."
35284
35356
  }
35285
35357
  },
35286
35358
  requires: [],
@@ -35308,7 +35380,12 @@ var TOOL_SPECS = [
35308
35380
  },
35309
35381
  metrics: {
35310
35382
  effect: "read",
35311
- summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked",
35383
+ summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked. memory_static is what the engine allocated on the CPU side, in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are what the renderer has on the GPU, in bytes, which memory_static does not count",
35384
+ requires: []
35385
+ },
35386
+ accessibility: {
35387
+ effect: "read",
35388
+ summary: `what the game tells a screen reader, read through UI Automation as a Windows screen reader reads it, under windows: one tree per window, each element with its role as UI Automation names it (text for static text, button, check_box, group for a control holding others), its name, its value where it has one, its bounds in the window's pixels, the actions a screen reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the one holding the focus. The name is what is read aloud, so an empty one is read as nothing; static text takes it from DisplayServer.accessibility_update_set_value, not from a name set on it, and the answer says under _note how many text elements on screen have none. With no control focused, the engine reports the first node under the root as focused, which in a run with the runtime is GdharnessRuntime. Needs a windowed run on Windows started with engineArgs ["--accessibility", "always"] or with the project's accessibility/general/accessibility_support at Always Active, since the engine builds the tree only when asked; reading it takes a second or two, starting PowerShell inside the game so it reaches the game's own desktop`,
35312
35389
  requires: []
35313
35390
  }
35314
35391
  },
@@ -35438,7 +35515,7 @@ var TOOL_SPECS = [
35438
35515
  name: "runtime_input",
35439
35516
  title: "Send input to the game",
35440
35517
  description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position.",
35441
- answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key and mouse_click: the event as it was sent, with whole saying whether the press was let go of again, and for a key the keycode, physical_keycode and unicode it carried. mouse_motion: the position and the relative movement sent. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
35518
+ answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again, for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
35442
35519
  parameters: {
35443
35520
  projectPath: RUNNING_PROJECT_PATH,
35444
35521
  pid: RUNNING_PID,
@@ -35459,8 +35536,8 @@ var TOOL_SPECS = [
35459
35536
  },
35460
35537
  pressed: {
35461
35538
  type: "boolean",
35462
- ops: ["action", "key", "mouse_click"],
35463
- description: "action, key, mouse_click: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. A wheel button held down is still a whole step, since a wheel is never held."
35539
+ ops: ["action", "key", "mouse_click", "joypad_button"],
35540
+ description: "action, key, mouse_click, joypad_button: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. A wheel button held down is still a whole step, since a wheel is never held."
35464
35541
  },
35465
35542
  strength: {
35466
35543
  type: "number",
@@ -35505,8 +35582,26 @@ var TOOL_SPECS = [
35505
35582
  },
35506
35583
  button: {
35507
35584
  type: ["string", "number"],
35508
- ops: ["click", "mouse_click"],
35509
- description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left."
35585
+ ops: ["click", "mouse_click", "joypad_button"],
35586
+ description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left. joypad_button: the controller button, by name (a, b, x, y, back, guide, start, left_stick, right_stick, left_shoulder, right_shoulder, dpad_up, dpad_down, dpad_left, dpad_right, misc1, paddle1 to paddle4, touchpad) or by its JoyButton number; a, b, x and y are the bottom, right, left and top face buttons, as Godot names them, whatever the controller prints on them."
35587
+ },
35588
+ axis: {
35589
+ type: ["string", "number"],
35590
+ ops: ["joypad_motion"],
35591
+ description: "joypad_motion: the stick or trigger axis, by name (left_x, left_y, right_x, right_y, trigger_left, trigger_right) or by its JoyAxis number."
35592
+ },
35593
+ value: {
35594
+ type: "number",
35595
+ minimum: -1,
35596
+ maximum: 1,
35597
+ ops: ["joypad_motion"],
35598
+ description: "joypad_motion: where the axis is, -1 to 1: down and right are positive on a stick, and a trigger runs 0 to 1. The axis stays there until another motion moves it, as a stick held over does; send 0 to let it go back."
35599
+ },
35600
+ device: {
35601
+ type: "number",
35602
+ minimum: 0,
35603
+ ops: ["joypad_button", "joypad_motion"],
35604
+ description: "joypad_button, joypad_motion: which controller, counting from 0. Default 0."
35510
35605
  },
35511
35606
  doubleClick: {
35512
35607
  type: "boolean",
@@ -35556,6 +35651,16 @@ var TOOL_SPECS = [
35556
35651
  effect: "additive",
35557
35652
  summary: "move the mouse to a position, carrying the distance from where the last injected event put it unless relativeX and relativeY say otherwise, and the buttons held, so a control that drags moves under a run of these between a held mouse_click and its release",
35558
35653
  requires: ["x", "y"]
35654
+ },
35655
+ joypad_button: {
35656
+ effect: "additive",
35657
+ summary: "press a controller button, or hold it, as an InputEventJoypadButton, so a game reading the pad in _input or _unhandled_input gets what a controller sends; an action bound to the button fires with it, as it would for a real one. Pressed and released a frame apart unless pressed says otherwise",
35658
+ requires: ["button"]
35659
+ },
35660
+ joypad_motion: {
35661
+ effect: "additive",
35662
+ summary: "move a stick or trigger axis to a value, as an InputEventJoypadMotion, which is what a game reading the sticks gets; the axis stays there until another motion moves it",
35663
+ requires: ["axis", "value"]
35559
35664
  }
35560
35665
  }
35561
35666
  },
@@ -35630,7 +35735,7 @@ var TOOL_SPECS = [
35630
35735
  {
35631
35736
  name: "debug_breakpoint",
35632
35737
  title: "Breakpoints",
35633
- 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.",
35634
35739
  parameters: {
35635
35740
  projectPath: PROJECT_PATH2,
35636
35741
  scriptPath: SCRIPT_PATH,
@@ -35645,23 +35750,33 @@ var TOOL_SPECS = [
35645
35750
  {
35646
35751
  name: "debug_control",
35647
35752
  title: "Step the debugger",
35648
- 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.",
35649
35754
  parameters: {},
35650
35755
  requires: [],
35651
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
+ },
35652
35762
  continue: { effect: "additive", summary: "resume after a breakpoint", requires: [] },
35653
35763
  step_over: { effect: "additive", summary: "run the current line", requires: [] },
35654
35764
  step_into: {
35655
35765
  effect: "additive",
35656
35766
  summary: "run the current line, stopping inside whatever it calls",
35657
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: []
35658
35773
  }
35659
35774
  }
35660
35775
  },
35661
35776
  {
35662
35777
  name: "debug_state",
35663
35778
  title: "Debugger state",
35664
- 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.",
35665
35780
  parameters: {
35666
35781
  frameId: {
35667
35782
  type: "number",
@@ -35751,7 +35866,7 @@ function serverInstructions() {
35751
35866
  "Call editor_status first: it says whether an editor is connected and whether its addon matches this server.",
35752
35867
  "Start the game with editor_run start, never by spawning an engine, and read editor_output after every run:",
35753
35868
  "its clean verdict and its error entries are how a run is judged, and editor_run wait is how to wait for one.",
35754
- "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.",
35755
35870
  "A refusal names what would have worked, so read it before calling again."
35756
35871
  ].join(" ");
35757
35872
  }
@@ -36092,6 +36207,31 @@ function longerWaitNote(tool, op, waitedMs) {
36092
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";
36093
36208
  return `${tool} ${op} takes no timeoutMs: it was given ${waitedMs}ms, and ${how}.`;
36094
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.";
36095
36235
  function projectSpelling(projectPath, absolutePath) {
36096
36236
  return `res://${relative4(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
36097
36237
  }
@@ -36179,6 +36319,7 @@ function stopWaitMs() {
36179
36319
  return waitFromEnv("GDHARNESS_STOP_WAIT_MS", 1e4);
36180
36320
  }
36181
36321
  var KEEPER_ANSWER_MS = 15000;
36322
+ var KEEPER_DEBUG_MS = PAUSE_MS + STEP_MS + 2 * READ_MS;
36182
36323
  var EXIT_REPORTED_WITHIN_MS = 2000;
36183
36324
  async function untilGone(pids, withinMs = stopWaitMs()) {
36184
36325
  const gone = () => pids.every((pid) => !alive(pid));
@@ -38014,6 +38155,12 @@ class GodotServer {
38014
38155
  path: readNonEmptyString(args, "nodePath") ?? "",
38015
38156
  property: readNonEmptyString(args, "property") ?? ""
38016
38157
  }, { tool: "runtime_inspect", op, patience });
38158
+ case "accessibility":
38159
+ return await this.handleRuntimeCommand("get_accessibility", whichGame(args), {
38160
+ tool: "runtime_inspect",
38161
+ op,
38162
+ patience: readPositiveNumber(args, "timeoutMs") ?? Math.max(30000, this.runtimeTimeoutMs())
38163
+ });
38017
38164
  default:
38018
38165
  return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, { tool: "runtime_inspect", op, patience });
38019
38166
  }
@@ -38073,28 +38220,70 @@ class GodotServer {
38073
38220
  return this.createErrorResponse(located.reason, PATH_SOLUTIONS);
38074
38221
  }
38075
38222
  const session = this.dapHoldingBreakpointsOf(project.value.path);
38076
- const answered = await this.handleDAP(op === "set" ? "dap_set_breakpoint" : "dap_remove_breakpoint", {
38077
- ...args,
38078
- scriptPath: realPathOr(located.absolutePath)
38079
- });
38080
- if (answered.isError === true) {
38081
- 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 };
38082
38246
  }
38083
38247
  const held = session.breakpointsHeld();
38084
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);
38085
38256
  return this.jsonTextResponse({
38086
- ...asParams(JSON.parse(answered.content[0]?.text ?? "{}")),
38257
+ ...answer,
38087
38258
  held: this.breakpointsAsProjectSpells(project.value.path, held),
38088
- setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor()),
38089
- 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.",
38090
- ...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() : {}
38091
38269
  });
38092
38270
  }
38093
38271
  case "debug_control":
38094
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
+ }
38095
38277
  if (tool === "debug_state" && op === "output") {
38096
38278
  return await this.handleDAP("dap_get_output", args);
38097
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
+ }
38098
38287
  const held = await this.debuggedGame(tool === "debug_control" ? "move" : "read");
38099
38288
  if (!held.ok) {
38100
38289
  return held.response;
@@ -38238,8 +38427,8 @@ class GodotServer {
38238
38427
  if (!game.throughEditor) {
38239
38428
  return {
38240
38429
  ok: false,
38241
- 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.", [
38242
- "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",
38243
38432
  "editor_status says whether an editor has reached this server"
38244
38433
  ])
38245
38434
  };
@@ -39450,6 +39639,38 @@ class GodotServer {
39450
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."
39451
39640
  };
39452
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
+ }
39453
39674
  breakpointsAsProjectSpells(projectPath, held) {
39454
39675
  return held.map((one) => ({
39455
39676
  scriptPath: projectSpelling(projectPath, one.scriptPath),
@@ -40111,9 +40332,10 @@ class GodotServer {
40111
40332
  started.announcedBefore = alreadyPlaying;
40112
40333
  started.bootScene = runtimeWaitMs.scene;
40113
40334
  this.activeProcess = started;
40114
- 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}` : "";
40115
40336
  const endedForThis = endedToStartThis(ended);
40116
40337
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
40338
+ const handed = this.dapHoldingBreakpointsOf(project.value.path).breakpointsHeld();
40117
40339
  const alsoRunning = leftRunningNote(stranded);
40118
40340
  return this.jsonTextResponse({
40119
40341
  started: true,
@@ -40123,6 +40345,7 @@ class GodotServer {
40123
40345
  arguments: cmdArgs,
40124
40346
  argsNote: engineFlagsNote(given.value),
40125
40347
  transcript: started.transcript,
40348
+ breakpoints: started.keeper === undefined || handed.length === 0 ? undefined : this.breakpointsAsProjectSpells(project.value.path, handed),
40126
40349
  profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
40127
40350
  refreshedClasses: refreshed.value,
40128
40351
  ...scanWaitAnswer(scanned),
@@ -40131,7 +40354,7 @@ class GodotServer {
40131
40354
  previousRunLeft: previousRunLeft(ended),
40132
40355
  ...previousRunHow(ended),
40133
40356
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
40134
- 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}`
40135
40358
  });
40136
40359
  }
40137
40360
  async runtimeUp(projectPath, before, budget, withArgs) {
@@ -40658,7 +40881,8 @@ class GodotServer {
40658
40881
  startedAt,
40659
40882
  projectPath,
40660
40883
  ...this.servedBy(),
40661
- ...profile === undefined ? {} : { profile }
40884
+ ...profile === undefined ? {} : { profile },
40885
+ breakpoints: this.breakpointsForAGame(projectPath)
40662
40886
  },
40663
40887
  ...desktop === undefined ? {} : { desktop }
40664
40888
  });