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/cli.js CHANGED
@@ -652,7 +652,7 @@ function engineFlags(given) {
652
652
  const setBy = Object.hasOwn(SET_BY_ARGUMENT, flag) ? SET_BY_ARGUMENT[flag] : undefined;
653
653
  return {
654
654
  ok: false,
655
- 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"].`
655
+ 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"].`
656
656
  };
657
657
  }
658
658
  if (takes === "none") {
@@ -748,12 +748,27 @@ var init_launch = __esm(() => {
748
748
  "--fixed-fps": "number",
749
749
  "--max-fps": "number",
750
750
  "--resolution": "size",
751
- "--write-movie": "file"
751
+ "--write-movie": "file",
752
+ "--rendering-driver": "driver",
753
+ "--rendering-method": "method",
754
+ "--accessibility": "accessibility"
752
755
  });
753
756
  VALUE_KIND = {
754
757
  number: { pattern: /^\d+(?:\.\d+)?$/, named: "a number" },
755
758
  size: { pattern: /^\d+x\d+$/, named: "a size such as 1280x720" },
756
- file: { pattern: /^[^-]/, named: "a file path" }
759
+ file: { pattern: /^[^-]/, named: "a file path" },
760
+ driver: {
761
+ pattern: /^(?:vulkan|d3d12|metal|opengl3|opengl3_es|opengl3_angle|dummy)$/,
762
+ named: "a rendering driver: vulkan, d3d12, metal, opengl3, opengl3_es, opengl3_angle or dummy"
763
+ },
764
+ method: {
765
+ pattern: /^(?:forward_plus|mobile|gl_compatibility)$/,
766
+ named: "a rendering method: forward_plus, mobile or gl_compatibility"
767
+ },
768
+ accessibility: {
769
+ pattern: /^(?:auto|always|disabled)$/,
770
+ named: "an accessibility mode: auto, always or disabled"
771
+ }
757
772
  };
758
773
  SET_BY_ARGUMENT = Object.freeze({
759
774
  "--headless": "headless",
@@ -8365,7 +8380,7 @@ function reported2(problems) {
8365
8380
  return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
8366
8381
  }
8367
8382
  function reason(log, problems) {
8368
- const own = log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
8383
+ const own = refusalsIn(log);
8369
8384
  if (own.length > 0) {
8370
8385
  return own.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; ");
8371
8386
  }
@@ -8377,6 +8392,9 @@ function reason(log, problems) {
8377
8392
  const printed = log.everything().filter((entry) => entry.source === "stdout");
8378
8393
  return printed.at(-1)?.text ?? "no output at all";
8379
8394
  }
8395
+ function refusalsIn(log) {
8396
+ return log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
8397
+ }
8380
8398
  function scriptFault(problems, script) {
8381
8399
  const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
8382
8400
  const directories = [...new Set([spelled(dirname4(script)), spelled(resolvedOr(dirname4(script)))])];
@@ -8496,6 +8514,15 @@ async function runOperation(engine, operation, params, projectPath) {
8496
8514
  };
8497
8515
  }
8498
8516
  if (answer.kind === "none") {
8517
+ const refusals = refusalsIn(ran.log);
8518
+ if (refusals.length > 0) {
8519
+ const said = new Set(refusals.map((entry) => entry.index));
8520
+ return {
8521
+ ok: false,
8522
+ message: refusals.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; "),
8523
+ ...reported2(problems.filter((entry) => !said.has(entry.index)))
8524
+ };
8525
+ }
8499
8526
  const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
8500
8527
  return {
8501
8528
  ok: false,
@@ -11127,7 +11154,7 @@ var init_tool_definitions = __esm(() => {
11127
11154
  {
11128
11155
  name: "project_settings",
11129
11156
  title: "Project settings",
11130
- 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.",
11157
+ 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.",
11131
11158
  parameters: {
11132
11159
  projectPath: PROJECT_PATH,
11133
11160
  setting: {
@@ -11312,7 +11339,7 @@ var init_tool_definitions = __esm(() => {
11312
11339
  },
11313
11340
  options: {
11314
11341
  type: "object",
11315
- description: 'set_options: import options keyed as the .import file spells them, {"compress/mode": 1}.',
11342
+ 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.`,
11316
11343
  additionalProperties: true
11317
11344
  },
11318
11345
  reimport: {
@@ -11400,7 +11427,7 @@ var init_tool_definitions = __esm(() => {
11400
11427
  engineArgs: {
11401
11428
  type: "array",
11402
11429
  items: { type: "string" },
11403
- 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.`
11430
+ 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.`
11404
11431
  },
11405
11432
  failFast: {
11406
11433
  type: "boolean",
@@ -11871,13 +11898,13 @@ var init_tool_definitions = __esm(() => {
11871
11898
  type: "array",
11872
11899
  items: { type: "string" },
11873
11900
  ops: ["start", "check"],
11874
- 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.`
11901
+ 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.`
11875
11902
  },
11876
11903
  engineArgs: {
11877
11904
  type: "array",
11878
11905
  items: { type: "string" },
11879
11906
  ops: ["start", "check"],
11880
- 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.`
11907
+ 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.`
11881
11908
  },
11882
11909
  headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
11883
11910
  visible: {
@@ -11888,7 +11915,7 @@ var init_tool_definitions = __esm(() => {
11888
11915
  silent: {
11889
11916
  type: "boolean",
11890
11917
  ops: ["start"],
11891
- 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."
11918
+ 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."
11892
11919
  },
11893
11920
  savesIn: {
11894
11921
  type: "string",
@@ -12117,7 +12144,7 @@ var init_tool_definitions = __esm(() => {
12117
12144
  {
12118
12145
  name: "runtime_inspect",
12119
12146
  title: "Inspect the running game",
12120
- 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.",
12147
+ 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.",
12121
12148
  parameters: {
12122
12149
  projectPath: RUNNING_PROJECT_PATH,
12123
12150
  pid: RUNNING_PID,
@@ -12186,7 +12213,7 @@ var init_tool_definitions = __esm(() => {
12186
12213
  timeoutMs: {
12187
12214
  type: "integer",
12188
12215
  minimum: 1,
12189
- 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."
12216
+ 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."
12190
12217
  }
12191
12218
  },
12192
12219
  requires: [],
@@ -12214,7 +12241,12 @@ var init_tool_definitions = __esm(() => {
12214
12241
  },
12215
12242
  metrics: {
12216
12243
  effect: "read",
12217
- 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",
12244
+ 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",
12245
+ requires: []
12246
+ },
12247
+ accessibility: {
12248
+ effect: "read",
12249
+ 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`,
12218
12250
  requires: []
12219
12251
  }
12220
12252
  },
@@ -12344,7 +12376,7 @@ var init_tool_definitions = __esm(() => {
12344
12376
  name: "runtime_input",
12345
12377
  title: "Send input to the game",
12346
12378
  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.",
12347
- 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.',
12379
+ 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.',
12348
12380
  parameters: {
12349
12381
  projectPath: RUNNING_PROJECT_PATH,
12350
12382
  pid: RUNNING_PID,
@@ -12365,8 +12397,8 @@ var init_tool_definitions = __esm(() => {
12365
12397
  },
12366
12398
  pressed: {
12367
12399
  type: "boolean",
12368
- ops: ["action", "key", "mouse_click"],
12369
- 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."
12400
+ ops: ["action", "key", "mouse_click", "joypad_button"],
12401
+ 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."
12370
12402
  },
12371
12403
  strength: {
12372
12404
  type: "number",
@@ -12411,8 +12443,26 @@ var init_tool_definitions = __esm(() => {
12411
12443
  },
12412
12444
  button: {
12413
12445
  type: ["string", "number"],
12414
- ops: ["click", "mouse_click"],
12415
- description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left."
12446
+ ops: ["click", "mouse_click", "joypad_button"],
12447
+ 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."
12448
+ },
12449
+ axis: {
12450
+ type: ["string", "number"],
12451
+ ops: ["joypad_motion"],
12452
+ 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."
12453
+ },
12454
+ value: {
12455
+ type: "number",
12456
+ minimum: -1,
12457
+ maximum: 1,
12458
+ ops: ["joypad_motion"],
12459
+ 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."
12460
+ },
12461
+ device: {
12462
+ type: "number",
12463
+ minimum: 0,
12464
+ ops: ["joypad_button", "joypad_motion"],
12465
+ description: "joypad_button, joypad_motion: which controller, counting from 0. Default 0."
12416
12466
  },
12417
12467
  doubleClick: {
12418
12468
  type: "boolean",
@@ -12462,6 +12512,16 @@ var init_tool_definitions = __esm(() => {
12462
12512
  effect: "additive",
12463
12513
  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",
12464
12514
  requires: ["x", "y"]
12515
+ },
12516
+ joypad_button: {
12517
+ effect: "additive",
12518
+ 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",
12519
+ requires: ["button"]
12520
+ },
12521
+ joypad_motion: {
12522
+ effect: "additive",
12523
+ 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",
12524
+ requires: ["axis", "value"]
12465
12525
  }
12466
12526
  }
12467
12527
  },
@@ -12536,7 +12596,7 @@ var init_tool_definitions = __esm(() => {
12536
12596
  {
12537
12597
  name: "debug_breakpoint",
12538
12598
  title: "Breakpoints",
12539
- 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.",
12599
+ 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.",
12540
12600
  parameters: {
12541
12601
  projectPath: PROJECT_PATH,
12542
12602
  scriptPath: SCRIPT_PATH,
@@ -12551,23 +12611,33 @@ var init_tool_definitions = __esm(() => {
12551
12611
  {
12552
12612
  name: "debug_control",
12553
12613
  title: "Step the debugger",
12554
- 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.",
12614
+ 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.",
12555
12615
  parameters: {},
12556
12616
  requires: [],
12557
12617
  operations: {
12618
+ pause: {
12619
+ effect: "additive",
12620
+ 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",
12621
+ requires: []
12622
+ },
12558
12623
  continue: { effect: "additive", summary: "resume after a breakpoint", requires: [] },
12559
12624
  step_over: { effect: "additive", summary: "run the current line", requires: [] },
12560
12625
  step_into: {
12561
12626
  effect: "additive",
12562
12627
  summary: "run the current line, stopping inside whatever it calls",
12563
12628
  requires: []
12629
+ },
12630
+ step_out: {
12631
+ effect: "additive",
12632
+ summary: "run to the end of the current function and stop in its caller; a game this server started only",
12633
+ requires: []
12564
12634
  }
12565
12635
  }
12566
12636
  },
12567
12637
  {
12568
12638
  name: "debug_state",
12569
12639
  title: "Debugger state",
12570
- 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.",
12640
+ 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.",
12571
12641
  parameters: {
12572
12642
  frameId: {
12573
12643
  type: "number",
@@ -35638,6 +35708,18 @@ class GodotDAPClient {
35638
35708
  lines: Array.from(lines).sort((a, b) => a - b)
35639
35709
  }));
35640
35710
  }
35711
+ forgetBreakpoint(scriptPath, line) {
35712
+ const key = fileKey(scriptPath);
35713
+ const lines = this.breakpoints.get(key);
35714
+ if (lines === undefined) {
35715
+ return;
35716
+ }
35717
+ lines.delete(line);
35718
+ if (lines.size === 0) {
35719
+ this.breakpoints.delete(key);
35720
+ this.spelled.delete(key);
35721
+ }
35722
+ }
35641
35723
  holdBreakpoints(held) {
35642
35724
  for (const { scriptPath, lines } of held) {
35643
35725
  const key = fileKey(scriptPath);
@@ -36143,6 +36225,26 @@ var init_editor_log = __esm(() => {
36143
36225
  SLOTS = [/"[^"]*"/g, /'[^']*'/g, /\b(?:res|user):\/\/\S+/g, /(?<=\.gd|\.tscn|\.cs):\d+(?::\d+)?/g];
36144
36226
  });
36145
36227
 
36228
+ // src/godot-variant.ts
36229
+ var FLAG_64, MOST_MESSAGE_BYTES;
36230
+ var init_godot_variant = __esm(() => {
36231
+ FLAG_64 = 1 << 16;
36232
+ MOST_MESSAGE_BYTES = (8 << 20) + 4;
36233
+ });
36234
+
36235
+ // src/game-debugger.ts
36236
+ var PAUSE_MS = 30000, STEP_MS = 1e4, READ_MS = 5000, REPLIES;
36237
+ var init_game_debugger = __esm(() => {
36238
+ init_godot_variant();
36239
+ REPLIES = new Set([
36240
+ "debug_enter",
36241
+ "debug_exit",
36242
+ "stack_dump",
36243
+ "stack_frame_vars",
36244
+ "stack_frame_var"
36245
+ ]);
36246
+ });
36247
+
36146
36248
  // node_modules/ws/lib/constants.js
36147
36249
  var require_constants = __commonJS(function(exports, module) {
36148
36250
  var BINARY_TYPES = ["nodebuffer", "arraybuffer", "fragments"];
@@ -40107,7 +40209,30 @@ var init_junit = __esm(() => {
40107
40209
 
40108
40210
  // src/keeper-channel.ts
40109
40211
  import { connect as connect2, createServer as createServer2 } from "node:net";
40110
- function askTheKeeperToStop(address, withinMs) {
40212
+ async function askTheKeeperToStop(address, withinMs) {
40213
+ const answered = await askTheKeeper(address, "stop", withinMs);
40214
+ if (typeof answered !== "string") {
40215
+ return answered;
40216
+ }
40217
+ return answered === "signalled" || answered === "gone" ? answered : { unreached: `it answered ${JSON.stringify(answered)}` };
40218
+ }
40219
+ async function askTheKeepersDebugger(address, request, withinMs) {
40220
+ const answered = await askTheKeeper(address, `${DEBUG_PREFIX}${JSON.stringify(request)}`, withinMs);
40221
+ if (typeof answered !== "string") {
40222
+ return answered;
40223
+ }
40224
+ if (answered === "unknown") {
40225
+ return { unreached: "its keeper was started by a gdharness from before game debuggers, and has none" };
40226
+ }
40227
+ try {
40228
+ const parsed = JSON.parse(answered);
40229
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
40230
+ return parsed;
40231
+ }
40232
+ } catch {}
40233
+ return { unreached: `it answered ${JSON.stringify(answered.slice(0, 200))}` };
40234
+ }
40235
+ function askTheKeeper(address, line, withinMs) {
40111
40236
  return new Promise((resolve) => {
40112
40237
  let said = "";
40113
40238
  let settled = false;
@@ -40125,16 +40250,15 @@ function askTheKeeperToStop(address, withinMs) {
40125
40250
  }, withinMs);
40126
40251
  socket.setEncoding("utf8");
40127
40252
  socket.on("connect", () => {
40128
- socket.write(`stop
40253
+ socket.write(`${line}
40129
40254
  `);
40130
40255
  });
40131
40256
  socket.on("data", (chunk) => {
40132
40257
  said += chunk;
40133
- const line = said.split(`
40134
- `)[0] ?? "";
40135
40258
  if (said.includes(`
40136
40259
  `)) {
40137
- settle(line === "signalled" || line === "gone" ? line : { unreached: `it answered ${JSON.stringify(line)}` });
40260
+ settle(said.split(`
40261
+ `)[0] ?? "");
40138
40262
  }
40139
40263
  });
40140
40264
  socket.on("error", (error) => {
@@ -40145,6 +40269,7 @@ function askTheKeeperToStop(address, withinMs) {
40145
40269
  });
40146
40270
  });
40147
40271
  }
40272
+ var DEBUG_PREFIX = "debug ";
40148
40273
  var init_keeper_channel = () => {};
40149
40274
 
40150
40275
  // src/lifecycle-log.ts
@@ -43123,13 +43248,6 @@ var init_script_origins = __esm(() => {
43123
43248
  RELOADED_AT = /GDScript::reload \((res:\/\/.+):(\d+)\)$/;
43124
43249
  });
43125
43250
 
43126
- // src/godot-variant.ts
43127
- var FLAG_64, MOST_MESSAGE_BYTES;
43128
- var init_godot_variant = __esm(() => {
43129
- FLAG_64 = 1 << 16;
43130
- MOST_MESSAGE_BYTES = (8 << 20) + 4;
43131
- });
43132
-
43133
43251
  // src/script-profile.ts
43134
43252
  import { readFileSync as readFileSync22 } from "node:fs";
43135
43253
  function readProfileFile(path) {
@@ -43296,7 +43414,7 @@ function serverInstructions() {
43296
43414
  "Call editor_status first: it says whether an editor is connected and whether its addon matches this server.",
43297
43415
  "Start the game with editor_run start, never by spawning an engine, and read editor_output after every run:",
43298
43416
  "its clean verdict and its error entries are how a run is judged, and editor_run wait is how to wait for one.",
43299
- "The runtime_* tools work on the running game and the debug_* tools on the editor's debugger.",
43417
+ "The runtime_* tools work on the running game and the debug_* tools on its debugger.",
43300
43418
  "A refusal names what would have worked, so read it before calling again."
43301
43419
  ].join(" ");
43302
43420
  }
@@ -43426,6 +43544,30 @@ function longerWaitNote(tool, op, waitedMs) {
43426
43544
  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";
43427
43545
  return `${tool} ${op} takes no timeoutMs: it was given ${waitedMs}ms, and ${how}.`;
43428
43546
  }
43547
+ function keptGameRequest(tool, op, args) {
43548
+ if (tool === "debug_state") {
43549
+ if (op === "output") {
43550
+ return { op: "output" };
43551
+ }
43552
+ if (op === "variables") {
43553
+ const frame = args["frameId"];
43554
+ return { op: "variables", frame: typeof frame === "number" ? frame : 0 };
43555
+ }
43556
+ return { op: "stack" };
43557
+ }
43558
+ switch (op) {
43559
+ case "pause":
43560
+ return { op: "pause" };
43561
+ case "step_over":
43562
+ return { op: "resume", how: "next" };
43563
+ case "step_into":
43564
+ return { op: "resume", how: "step" };
43565
+ case "step_out":
43566
+ return { op: "resume", how: "out" };
43567
+ default:
43568
+ return { op: "resume", how: "continue" };
43569
+ }
43570
+ }
43429
43571
  function projectSpelling(projectPath, absolutePath) {
43430
43572
  return `res://${relative5(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
43431
43573
  }
@@ -45267,6 +45409,12 @@ class GodotServer {
45267
45409
  path: readNonEmptyString(args, "nodePath") ?? "",
45268
45410
  property: readNonEmptyString(args, "property") ?? ""
45269
45411
  }, { tool: "runtime_inspect", op, patience });
45412
+ case "accessibility":
45413
+ return await this.handleRuntimeCommand("get_accessibility", whichGame(args), {
45414
+ tool: "runtime_inspect",
45415
+ op,
45416
+ patience: readPositiveNumber(args, "timeoutMs") ?? Math.max(30000, this.runtimeTimeoutMs())
45417
+ });
45270
45418
  default:
45271
45419
  return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, { tool: "runtime_inspect", op, patience });
45272
45420
  }
@@ -45326,28 +45474,70 @@ class GodotServer {
45326
45474
  return this.createErrorResponse(located.reason, PATH_SOLUTIONS);
45327
45475
  }
45328
45476
  const session = this.dapHoldingBreakpointsOf(project.value.path);
45329
- const answered = await this.handleDAP(op === "set" ? "dap_set_breakpoint" : "dap_remove_breakpoint", {
45330
- ...args,
45331
- scriptPath: realPathOr(located.absolutePath)
45332
- });
45333
- if (answered.isError === true) {
45334
- return answered;
45477
+ const scriptPath = realPathOr(located.absolutePath);
45478
+ const line = readPositiveNumber(args, "line");
45479
+ if (line === undefined) {
45480
+ return this.createErrorResponse("line is required: the one-based line to stop on.");
45481
+ }
45482
+ const editor = this.godotBridge.isConnected();
45483
+ let answer;
45484
+ if (editor) {
45485
+ const answered = await this.handleDAP(op === "set" ? "dap_set_breakpoint" : "dap_remove_breakpoint", {
45486
+ ...args,
45487
+ scriptPath
45488
+ });
45489
+ if (answered.isError === true) {
45490
+ return answered;
45491
+ }
45492
+ answer = asParams(JSON.parse(answered.content[0]?.text ?? "{}"));
45493
+ } else {
45494
+ if (op === "set") {
45495
+ session.holdBreakpoints([{ scriptPath, lines: [line] }]);
45496
+ } else {
45497
+ session.forgetBreakpoint(scriptPath, line);
45498
+ }
45499
+ answer = { [op === "set" ? "set" : "removed"]: true };
45335
45500
  }
45336
45501
  const held = session.breakpointsHeld();
45337
45502
  writeBreakpointNote(project.value.path, held);
45503
+ const kept = this.keptRun();
45504
+ const told = kept === null ? null : await askTheKeepersDebugger(kept.keeper, {
45505
+ op: "breakpoint",
45506
+ script: projectSpelling(project.value.path, scriptPath),
45507
+ line,
45508
+ set: op === "set"
45509
+ }, KEEPER_DEBUG_MS);
45338
45510
  return this.jsonTextResponse({
45339
- ...asParams(JSON.parse(answered.content[0]?.text ?? "{}")),
45511
+ ...answer,
45340
45512
  held: this.breakpointsAsProjectSpells(project.value.path, held),
45341
- setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor()),
45342
- 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.",
45343
- ...this.breakpointsAtRisk()
45513
+ ...editor ? {
45514
+ setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor())
45515
+ } : {},
45516
+ ...told === null ? {} : ("unreached" in told) || typeof told["refused"] === "string" ? {
45517
+ 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.`
45518
+ } : {
45519
+ runningGame: `Told to the running game (pid ${kept?.pid ?? "?"}), which this server started.`
45520
+ },
45521
+ 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.",
45522
+ ...editor ? this.breakpointsAtRisk() : {}
45344
45523
  });
45345
45524
  }
45346
45525
  case "debug_control":
45347
45526
  case "debug_state": {
45527
+ const kept = this.keptRun();
45528
+ if (kept !== null) {
45529
+ return await this.askTheKeptGame(kept.keeper, keptGameRequest(tool, op, args), kept.projectPath ?? "");
45530
+ }
45348
45531
  if (tool === "debug_state" && op === "output") {
45349
45532
  return await this.handleDAP("dap_get_output", args);
45350
45533
  }
45534
+ const played = this.activeProcess;
45535
+ if ((op === "pause" || op === "step_out") && played?.throughEditor === true && !exited(played)) {
45536
+ 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.", [
45537
+ "Set a breakpoint where it should stop, then step over or into from there",
45538
+ "A game this server starts, as editor_run start does with args, savesIn or env, has a debugger here that pauses and steps out"
45539
+ ]);
45540
+ }
45351
45541
  const held = await this.debuggedGame(tool === "debug_control" ? "move" : "read");
45352
45542
  if (!held.ok) {
45353
45543
  return held.response;
@@ -45491,8 +45681,8 @@ class GodotServer {
45491
45681
  if (!game.throughEditor) {
45492
45682
  return {
45493
45683
  ok: false,
45494
- 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.", [
45495
- "editor_run start plays it through the open editor, whose debugger the debug_* tools speak to",
45684
+ 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.", [
45685
+ "editor_run start starts it again, and the keeper holding a game this server starts is its debugger",
45496
45686
  "editor_status says whether an editor has reached this server"
45497
45687
  ])
45498
45688
  };
@@ -46703,6 +46893,38 @@ class GodotServer {
46703
46893
  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."
46704
46894
  };
46705
46895
  }
46896
+ breakpointsForAGame(projectPath) {
46897
+ return this.dapHoldingBreakpointsOf(projectPath).breakpointsHeld().flatMap((one) => one.lines.map((line) => `${projectSpelling(projectPath, one.scriptPath)}:${line}`));
46898
+ }
46899
+ keptRun() {
46900
+ const game = this.activeProcess;
46901
+ if (game === null || game.throughEditor || exited(game) || game.keeper === undefined) {
46902
+ return null;
46903
+ }
46904
+ return { ...game, keeper: game.keeper };
46905
+ }
46906
+ async askTheKeptGame(keeper, request, projectPath) {
46907
+ const answered = await askTheKeepersDebugger(keeper, request, KEEPER_DEBUG_MS);
46908
+ if ("unreached" in answered) {
46909
+ return this.createErrorResponse(`The debugger of the running game did not answer: ${String(answered.unreached)}.`, [
46910
+ "editor_run start starts the game again, with a debugger",
46911
+ "editor_status says whether it is still running"
46912
+ ]);
46913
+ }
46914
+ if (typeof answered["refused"] === "string") {
46915
+ return this.createErrorResponse(answered["refused"]);
46916
+ }
46917
+ const stack = answered["stack"];
46918
+ return this.jsonTextResponse({
46919
+ ...answered,
46920
+ ...Array.isArray(stack) ? {
46921
+ stack: stack.map((frame) => ({
46922
+ ...frame,
46923
+ ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join27(projectPath, frame["script"].slice("res://".length)) } : {}
46924
+ }))
46925
+ } : {}
46926
+ });
46927
+ }
46706
46928
  breakpointsAsProjectSpells(projectPath, held) {
46707
46929
  return held.map((one) => ({
46708
46930
  scriptPath: projectSpelling(projectPath, one.scriptPath),
@@ -47364,9 +47586,10 @@ class GodotServer {
47364
47586
  started.announcedBefore = alreadyPlaying;
47365
47587
  started.bootScene = runtimeWaitMs.scene;
47366
47588
  this.activeProcess = started;
47367
- 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.` : "";
47589
+ 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}` : "";
47368
47590
  const endedForThis = endedToStartThis(ended);
47369
47591
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
47592
+ const handed = this.dapHoldingBreakpointsOf(project.value.path).breakpointsHeld();
47370
47593
  const alsoRunning = leftRunningNote(stranded);
47371
47594
  return this.jsonTextResponse({
47372
47595
  started: true,
@@ -47376,6 +47599,7 @@ class GodotServer {
47376
47599
  arguments: cmdArgs,
47377
47600
  argsNote: engineFlagsNote(given.value),
47378
47601
  transcript: started.transcript,
47602
+ breakpoints: started.keeper === undefined || handed.length === 0 ? undefined : this.breakpointsAsProjectSpells(project.value.path, handed),
47379
47603
  profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
47380
47604
  refreshedClasses: refreshed.value,
47381
47605
  ...scanWaitAnswer(scanned),
@@ -47384,7 +47608,7 @@ class GodotServer {
47384
47608
  previousRunLeft: previousRunLeft(ended),
47385
47609
  ...previousRunHow(ended),
47386
47610
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
47387
- message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
47611
+ 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}`
47388
47612
  });
47389
47613
  }
47390
47614
  async runtimeUp(projectPath, before, budget, withArgs) {
@@ -47911,7 +48135,8 @@ class GodotServer {
47911
48135
  startedAt,
47912
48136
  projectPath,
47913
48137
  ...this.servedBy(),
47914
- ...profile === undefined ? {} : { profile }
48138
+ ...profile === undefined ? {} : { profile },
48139
+ breakpoints: this.breakpointsForAGame(projectPath)
47915
48140
  },
47916
48141
  ...desktop === undefined ? {} : { desktop }
47917
48142
  });
@@ -49764,7 +49989,7 @@ async function runGodotServer() {
49764
49989
  const server = new GodotServer;
49765
49990
  await server.run();
49766
49991
  }
49767
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, EDITOR_FREE_INSTEAD, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
49992
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, 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.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, EDITOR_FREE_INSTEAD, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
49768
49993
  var init_server = __esm(() => {
49769
49994
  init_dist();
49770
49995
  init_stdio();
@@ -49783,6 +50008,7 @@ var init_server = __esm(() => {
49783
50008
  init_editor_log();
49784
50009
  init_engine_run();
49785
50010
  init_errors();
50011
+ init_game_debugger();
49786
50012
  init_game_log();
49787
50013
  init_gdscript_source();
49788
50014
  init_godot_bridge();
@@ -49820,6 +50046,7 @@ var init_server = __esm(() => {
49820
50046
  init_update_check();
49821
50047
  run4 = promisify5(execFile5);
49822
50048
  __dirname2 = dirname20(fileURLToPath5(import.meta.url));
50049
+ KEEPER_DEBUG_MS = PAUSE_MS + STEP_MS + 2 * READ_MS;
49823
50050
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
49824
50051
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
49825
50052
  VALUE_NAMES = {