gdharness 1.2.26 → 1.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -11898,7 +11898,7 @@ var init_tool_definitions = __esm(() => {
11898
11898
  type: "array",
11899
11899
  items: { type: "string" },
11900
11900
  ops: ["start", "check"],
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 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.`
11902
11902
  },
11903
11903
  engineArgs: {
11904
11904
  type: "array",
@@ -11915,7 +11915,7 @@ var init_tool_definitions = __esm(() => {
11915
11915
  silent: {
11916
11916
  type: "boolean",
11917
11917
  ops: ["start"],
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, 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."
11919
11919
  },
11920
11920
  savesIn: {
11921
11921
  type: "string",
@@ -12596,7 +12596,7 @@ var init_tool_definitions = __esm(() => {
12596
12596
  {
12597
12597
  name: "debug_breakpoint",
12598
12598
  title: "Breakpoints",
12599
- 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.",
12600
12600
  parameters: {
12601
12601
  projectPath: PROJECT_PATH,
12602
12602
  scriptPath: SCRIPT_PATH,
@@ -12611,23 +12611,33 @@ var init_tool_definitions = __esm(() => {
12611
12611
  {
12612
12612
  name: "debug_control",
12613
12613
  title: "Step the debugger",
12614
- 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.",
12615
12615
  parameters: {},
12616
12616
  requires: [],
12617
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
+ },
12618
12623
  continue: { effect: "additive", summary: "resume after a breakpoint", requires: [] },
12619
12624
  step_over: { effect: "additive", summary: "run the current line", requires: [] },
12620
12625
  step_into: {
12621
12626
  effect: "additive",
12622
12627
  summary: "run the current line, stopping inside whatever it calls",
12623
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: []
12624
12634
  }
12625
12635
  }
12626
12636
  },
12627
12637
  {
12628
12638
  name: "debug_state",
12629
12639
  title: "Debugger state",
12630
- 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.",
12631
12641
  parameters: {
12632
12642
  frameId: {
12633
12643
  type: "number",
@@ -35698,6 +35708,18 @@ class GodotDAPClient {
35698
35708
  lines: Array.from(lines).sort((a, b) => a - b)
35699
35709
  }));
35700
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
+ }
35701
35723
  holdBreakpoints(held) {
35702
35724
  for (const { scriptPath, lines } of held) {
35703
35725
  const key = fileKey(scriptPath);
@@ -36203,6 +36225,26 @@ var init_editor_log = __esm(() => {
36203
36225
  SLOTS = [/"[^"]*"/g, /'[^']*'/g, /\b(?:res|user):\/\/\S+/g, /(?<=\.gd|\.tscn|\.cs):\d+(?::\d+)?/g];
36204
36226
  });
36205
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
+
36206
36248
  // node_modules/ws/lib/constants.js
36207
36249
  var require_constants = __commonJS(function(exports, module) {
36208
36250
  var BINARY_TYPES = ["nodebuffer", "arraybuffer", "fragments"];
@@ -40167,7 +40209,30 @@ var init_junit = __esm(() => {
40167
40209
 
40168
40210
  // src/keeper-channel.ts
40169
40211
  import { connect as connect2, createServer as createServer2 } from "node:net";
40170
- 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) {
40171
40236
  return new Promise((resolve) => {
40172
40237
  let said = "";
40173
40238
  let settled = false;
@@ -40185,16 +40250,15 @@ function askTheKeeperToStop(address, withinMs) {
40185
40250
  }, withinMs);
40186
40251
  socket.setEncoding("utf8");
40187
40252
  socket.on("connect", () => {
40188
- socket.write(`stop
40253
+ socket.write(`${line}
40189
40254
  `);
40190
40255
  });
40191
40256
  socket.on("data", (chunk) => {
40192
40257
  said += chunk;
40193
- const line = said.split(`
40194
- `)[0] ?? "";
40195
40258
  if (said.includes(`
40196
40259
  `)) {
40197
- settle(line === "signalled" || line === "gone" ? line : { unreached: `it answered ${JSON.stringify(line)}` });
40260
+ settle(said.split(`
40261
+ `)[0] ?? "");
40198
40262
  }
40199
40263
  });
40200
40264
  socket.on("error", (error) => {
@@ -40205,6 +40269,7 @@ function askTheKeeperToStop(address, withinMs) {
40205
40269
  });
40206
40270
  });
40207
40271
  }
40272
+ var DEBUG_PREFIX = "debug ";
40208
40273
  var init_keeper_channel = () => {};
40209
40274
 
40210
40275
  // src/lifecycle-log.ts
@@ -43183,13 +43248,6 @@ var init_script_origins = __esm(() => {
43183
43248
  RELOADED_AT = /GDScript::reload \((res:\/\/.+):(\d+)\)$/;
43184
43249
  });
43185
43250
 
43186
- // src/godot-variant.ts
43187
- var FLAG_64, MOST_MESSAGE_BYTES;
43188
- var init_godot_variant = __esm(() => {
43189
- FLAG_64 = 1 << 16;
43190
- MOST_MESSAGE_BYTES = (8 << 20) + 4;
43191
- });
43192
-
43193
43251
  // src/script-profile.ts
43194
43252
  import { readFileSync as readFileSync22 } from "node:fs";
43195
43253
  function readProfileFile(path) {
@@ -43356,7 +43414,7 @@ function serverInstructions() {
43356
43414
  "Call editor_status first: it says whether an editor is connected and whether its addon matches this server.",
43357
43415
  "Start the game with editor_run start, never by spawning an engine, and read editor_output after every run:",
43358
43416
  "its clean verdict and its error entries are how a run is judged, and editor_run wait is how to wait for one.",
43359
- "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.",
43360
43418
  "A refusal names what would have worked, so read it before calling again."
43361
43419
  ].join(" ");
43362
43420
  }
@@ -43486,6 +43544,30 @@ function longerWaitNote(tool, op, waitedMs) {
43486
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";
43487
43545
  return `${tool} ${op} takes no timeoutMs: it was given ${waitedMs}ms, and ${how}.`;
43488
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
+ }
43489
43571
  function projectSpelling(projectPath, absolutePath) {
43490
43572
  return `res://${relative5(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
43491
43573
  }
@@ -45392,28 +45474,70 @@ class GodotServer {
45392
45474
  return this.createErrorResponse(located.reason, PATH_SOLUTIONS);
45393
45475
  }
45394
45476
  const session = this.dapHoldingBreakpointsOf(project.value.path);
45395
- const answered = await this.handleDAP(op === "set" ? "dap_set_breakpoint" : "dap_remove_breakpoint", {
45396
- ...args,
45397
- scriptPath: realPathOr(located.absolutePath)
45398
- });
45399
- if (answered.isError === true) {
45400
- 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 };
45401
45500
  }
45402
45501
  const held = session.breakpointsHeld();
45403
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);
45404
45510
  return this.jsonTextResponse({
45405
- ...asParams(JSON.parse(answered.content[0]?.text ?? "{}")),
45511
+ ...answer,
45406
45512
  held: this.breakpointsAsProjectSpells(project.value.path, held),
45407
- setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor()),
45408
- 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.",
45409
- ...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() : {}
45410
45523
  });
45411
45524
  }
45412
45525
  case "debug_control":
45413
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
+ }
45414
45531
  if (tool === "debug_state" && op === "output") {
45415
45532
  return await this.handleDAP("dap_get_output", args);
45416
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
+ }
45417
45541
  const held = await this.debuggedGame(tool === "debug_control" ? "move" : "read");
45418
45542
  if (!held.ok) {
45419
45543
  return held.response;
@@ -45557,8 +45681,8 @@ class GodotServer {
45557
45681
  if (!game.throughEditor) {
45558
45682
  return {
45559
45683
  ok: false,
45560
- 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.", [
45561
- "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",
45562
45686
  "editor_status says whether an editor has reached this server"
45563
45687
  ])
45564
45688
  };
@@ -46769,6 +46893,38 @@ class GodotServer {
46769
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."
46770
46894
  };
46771
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
+ }
46772
46928
  breakpointsAsProjectSpells(projectPath, held) {
46773
46929
  return held.map((one) => ({
46774
46930
  scriptPath: projectSpelling(projectPath, one.scriptPath),
@@ -47430,9 +47586,10 @@ class GodotServer {
47430
47586
  started.announcedBefore = alreadyPlaying;
47431
47587
  started.bootScene = runtimeWaitMs.scene;
47432
47588
  this.activeProcess = started;
47433
- 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}` : "";
47434
47590
  const endedForThis = endedToStartThis(ended);
47435
47591
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
47592
+ const handed = this.dapHoldingBreakpointsOf(project.value.path).breakpointsHeld();
47436
47593
  const alsoRunning = leftRunningNote(stranded);
47437
47594
  return this.jsonTextResponse({
47438
47595
  started: true,
@@ -47442,6 +47599,7 @@ class GodotServer {
47442
47599
  arguments: cmdArgs,
47443
47600
  argsNote: engineFlagsNote(given.value),
47444
47601
  transcript: started.transcript,
47602
+ breakpoints: started.keeper === undefined || handed.length === 0 ? undefined : this.breakpointsAsProjectSpells(project.value.path, handed),
47445
47603
  profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
47446
47604
  refreshedClasses: refreshed.value,
47447
47605
  ...scanWaitAnswer(scanned),
@@ -47450,7 +47608,7 @@ class GodotServer {
47450
47608
  previousRunLeft: previousRunLeft(ended),
47451
47609
  ...previousRunHow(ended),
47452
47610
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
47453
- 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}`
47454
47612
  });
47455
47613
  }
47456
47614
  async runtimeUp(projectPath, before, budget, withArgs) {
@@ -47977,7 +48135,8 @@ class GodotServer {
47977
48135
  startedAt,
47978
48136
  projectPath,
47979
48137
  ...this.servedBy(),
47980
- ...profile === undefined ? {} : { profile }
48138
+ ...profile === undefined ? {} : { profile },
48139
+ breakpoints: this.breakpointsForAGame(projectPath)
47981
48140
  },
47982
48141
  ...desktop === undefined ? {} : { desktop }
47983
48142
  });
@@ -49830,7 +49989,7 @@ async function runGodotServer() {
49830
49989
  const server = new GodotServer;
49831
49990
  await server.run();
49832
49991
  }
49833
- 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;
49834
49993
  var init_server = __esm(() => {
49835
49994
  init_dist();
49836
49995
  init_stdio();
@@ -49849,6 +50008,7 @@ var init_server = __esm(() => {
49849
50008
  init_editor_log();
49850
50009
  init_engine_run();
49851
50010
  init_errors();
50011
+ init_game_debugger();
49852
50012
  init_game_log();
49853
50013
  init_gdscript_source();
49854
50014
  init_godot_bridge();
@@ -49886,6 +50046,7 @@ var init_server = __esm(() => {
49886
50046
  init_update_check();
49887
50047
  run4 = promisify5(execFile5);
49888
50048
  __dirname2 = dirname20(fileURLToPath5(import.meta.url));
50049
+ KEEPER_DEBUG_MS = PAUSE_MS + STEP_MS + 2 * READ_MS;
49889
50050
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
49890
50051
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
49891
50052
  VALUE_NAMES = {
@@ -100,15 +100,19 @@ try {
100
100
  $windows = @()
101
101
  foreach ($handle in $Handles) {
102
102
  $root = [System.Windows.Automation.AutomationElement]::FromHandle([IntPtr][long]$handle)
103
- $r = $root.Current.BoundingRectangle
104
103
  $deadline = [DateTime]::UtcNow.AddMilliseconds($SettleMs)
105
104
  $empty = [DateTime]::UtcNow.AddMilliseconds($EmptyMs)
105
+ # The window's corner is read on every walk: before the engine builds its tree the window is
106
+ # its frame, and after it the client area inside it, so a corner read once placed everything
107
+ # by the frame's.
108
+ $r = $root.Current.BoundingRectangle
106
109
  $tree = Describe $root $r.X $r.Y 0
107
110
  $last = Count $tree
108
111
  # Walked again until two walks in a row agree, since the first question is what makes the
109
112
  # engine build the tree, which arrives over the frames after it.
110
113
  while ([DateTime]::UtcNow -lt $deadline) {
111
114
  Start-Sleep -Milliseconds 150
115
+ $r = $root.Current.BoundingRectangle
112
116
  $tree = Describe $root $r.X $r.Y 0
113
117
  $count = Count $tree
114
118
  if ($count -gt 1 -and $count -eq $last) { break }