gdharness 0.13.39 → 0.13.40

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
@@ -15373,7 +15373,7 @@ function buildToolDefinitions() {
15373
15373
  };
15374
15374
  });
15375
15375
  }
15376
- var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, SCENE_PATH, SCRIPT_PATH, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
15376
+ var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, SCRIPT_PATH, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
15377
15377
  var init_tool_definitions = __esm(() => {
15378
15378
  PROJECT_INFO_SECTIONS = [
15379
15379
  "autoloads",
@@ -15391,6 +15391,10 @@ var init_tool_definitions = __esm(() => {
15391
15391
  type: "string",
15392
15392
  description: "Which game, when more than one is running: the project directory it was started from. Not needed with one game."
15393
15393
  };
15394
+ RUNNING_PID = {
15395
+ type: "number",
15396
+ description: "Which game, when several are running from one project, such as a bench and its workers: its process id, as editor_status lists under runtimes. Not needed with one game per project."
15397
+ };
15394
15398
  SCENE_PATH = {
15395
15399
  type: "string",
15396
15400
  description: 'Scene file inside the project, such as "scenes/main.tscn" or "res://scenes/main.tscn".'
@@ -16114,6 +16118,7 @@ var init_tool_definitions = __esm(() => {
16114
16118
  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.",
16115
16119
  parameters: {
16116
16120
  projectPath: RUNNING_PROJECT_PATH,
16121
+ pid: RUNNING_PID,
16117
16122
  nodePath: {
16118
16123
  type: "string",
16119
16124
  ops: ["tree", "text", "find", "rect", "property"],
@@ -16192,6 +16197,7 @@ var init_tool_definitions = __esm(() => {
16192
16197
  description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon.",
16193
16198
  parameters: {
16194
16199
  projectPath: RUNNING_PROJECT_PATH,
16200
+ pid: RUNNING_PID,
16195
16201
  nodePath: { type: "string", description: 'Absolute node path, such as "/root/Main/Player".' },
16196
16202
  property: {
16197
16203
  type: "string",
@@ -16231,6 +16237,7 @@ var init_tool_definitions = __esm(() => {
16231
16237
  description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window.",
16232
16238
  parameters: {
16233
16239
  projectPath: RUNNING_PROJECT_PATH,
16240
+ pid: RUNNING_PID,
16234
16241
  viewportPath: {
16235
16242
  type: "string",
16236
16243
  ops: ["viewport"],
@@ -16251,6 +16258,7 @@ var init_tool_definitions = __esm(() => {
16251
16258
  description: "Input to the running game: a whole click on a Control or a 3D node named by path, 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.",
16252
16259
  parameters: {
16253
16260
  projectPath: RUNNING_PROJECT_PATH,
16261
+ pid: RUNNING_PID,
16254
16262
  nodePath: {
16255
16263
  type: "string",
16256
16264
  ops: ["click", "choose"],
@@ -16347,6 +16355,7 @@ var init_tool_definitions = __esm(() => {
16347
16355
  description: "Lets the running game get on with it and answers when something has happened: a number of frames, a signal, a property reaching a value, or words appearing on a screen. Needs the game running with the runtime addon.",
16348
16356
  parameters: {
16349
16357
  projectPath: RUNNING_PROJECT_PATH,
16358
+ pid: RUNNING_PID,
16350
16359
  frames: {
16351
16360
  type: "number",
16352
16361
  ops: ["frames"],
@@ -16382,7 +16391,7 @@ var init_tool_definitions = __esm(() => {
16382
16391
  },
16383
16392
  {
16384
16393
  name: "debug_breakpoint",
16385
- 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.",
16394
+ 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.",
16386
16395
  parameters: {
16387
16396
  projectPath: PROJECT_PATH,
16388
16397
  scriptPath: SCRIPT_PATH,
@@ -31481,6 +31490,8 @@ class GodotDAPClient {
31481
31490
  halt = null;
31482
31491
  holdKnown = false;
31483
31492
  breakpoints = new Map;
31493
+ inEditor = new Map;
31494
+ onBreakpointsChanged = null;
31484
31495
  constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
31485
31496
  this.port = port;
31486
31497
  this.host = host;
@@ -31570,6 +31581,7 @@ class GodotDAPClient {
31570
31581
  this.attached = false;
31571
31582
  this.halt = null;
31572
31583
  this.holdKnown = false;
31584
+ this.inEditor.clear();
31573
31585
  }
31574
31586
  async ensureConnected() {
31575
31587
  if (!this.connected) {
@@ -31666,6 +31678,31 @@ class GodotDAPClient {
31666
31678
  this.holdKnown = true;
31667
31679
  return;
31668
31680
  }
31681
+ if (eventName === "breakpoint") {
31682
+ const body = event.body;
31683
+ const breakpoint = body?.["breakpoint"];
31684
+ const source = typeof breakpoint === "object" && breakpoint !== null ? breakpoint["source"] : undefined;
31685
+ const path = typeof source === "object" && source !== null ? source["path"] : undefined;
31686
+ const line = typeof breakpoint === "object" && breakpoint !== null ? breakpoint["line"] : undefined;
31687
+ if (typeof path !== "string" || typeof line !== "number") {
31688
+ return;
31689
+ }
31690
+ if (said(body, "reason") === "removed") {
31691
+ this.inEditor.get(path)?.delete(line);
31692
+ const held = this.breakpoints.get(path);
31693
+ if (held?.delete(line) === true) {
31694
+ if (held.size === 0) {
31695
+ this.breakpoints.delete(path);
31696
+ }
31697
+ this.onBreakpointsChanged?.(this.breakpointsHeld());
31698
+ }
31699
+ return;
31700
+ }
31701
+ const known = this.inEditor.get(path) ?? new Set;
31702
+ known.add(line);
31703
+ this.inEditor.set(path, known);
31704
+ return;
31705
+ }
31669
31706
  if (eventName === "terminated" || eventName === "exited") {
31670
31707
  this.attached = false;
31671
31708
  this.halt = null;
@@ -31758,23 +31795,46 @@ class GodotDAPClient {
31758
31795
  async removeBreakpoint(filePath, line) {
31759
31796
  const wanted = new Set(this.breakpoints.get(filePath) ?? []);
31760
31797
  wanted.delete(line);
31761
- return await this.sendBreakpoints(filePath, wanted);
31798
+ return await this.sendBreakpoints(filePath, wanted, line);
31762
31799
  }
31763
- async sendBreakpoints(filePath, lines) {
31800
+ async sendBreakpoints(filePath, mine, clearing) {
31764
31801
  await this.ensureConnected();
31765
31802
  await this.initialize();
31803
+ const lines = new Set([...mine, ...this.inEditor.get(filePath) ?? []]);
31804
+ if (clearing !== undefined) {
31805
+ lines.delete(clearing);
31806
+ }
31766
31807
  const sorted = Array.from(lines).sort((a, b) => a - b);
31767
31808
  const answer = await this.sendRequest("setBreakpoints", {
31768
31809
  source: { path: filePath },
31769
31810
  breakpoints: sorted.map((breakpointLine) => ({ line: breakpointLine }))
31770
31811
  });
31771
- if (lines.size === 0) {
31812
+ if (mine.size === 0) {
31772
31813
  this.breakpoints.delete(filePath);
31773
31814
  } else {
31774
- this.breakpoints.set(filePath, new Set(lines));
31815
+ this.breakpoints.set(filePath, new Set(mine));
31816
+ }
31817
+ if (lines.size === 0) {
31818
+ this.inEditor.delete(filePath);
31819
+ } else {
31820
+ this.inEditor.set(filePath, lines);
31775
31821
  }
31776
31822
  return answer;
31777
31823
  }
31824
+ setBreakpointsSink(sink) {
31825
+ this.onBreakpointsChanged = sink;
31826
+ }
31827
+ breakpointsInEditor() {
31828
+ const theirs = [];
31829
+ for (const [scriptPath, lines] of this.inEditor) {
31830
+ const mine = this.breakpoints.get(scriptPath);
31831
+ const rest = Array.from(lines).filter((line) => !mine?.has(line)).sort((a, b) => a - b);
31832
+ if (rest.length > 0) {
31833
+ theirs.push({ scriptPath, lines: rest });
31834
+ }
31835
+ }
31836
+ return theirs;
31837
+ }
31778
31838
  async reapplyBreakpoints() {
31779
31839
  const applied = [];
31780
31840
  const refused = [];
@@ -31790,6 +31850,9 @@ class GodotDAPClient {
31790
31850
  });
31791
31851
  }
31792
31852
  }
31853
+ if (refused.length > 0) {
31854
+ this.onBreakpointsChanged?.(this.breakpointsHeld());
31855
+ }
31793
31856
  return { applied, refused };
31794
31857
  }
31795
31858
  breakpointsHeld() {
@@ -31992,14 +32055,10 @@ async function handleDAPTool(client, toolName, args) {
31992
32055
  }
31993
32056
  } catch (error) {
31994
32057
  const message = error instanceof Error ? error.message : String(error);
32058
+ const cure = toolName === "dap_set_breakpoint" || toolName === "dap_remove_breakpoint" ? "Breakpoints need the editor open, not a running game: editor_status says whether one is connected, and the path is the script as the project spells it." : "The debug tools answer for a game the editor is playing: start one with editor_run.";
31995
32059
  return {
31996
32060
  isError: true,
31997
- content: [
31998
- {
31999
- type: "text",
32000
- text: `DAP tool '${toolName}' failed: ${message}. ` + "The debug tools answer for a game the editor is playing: start one with editor_run."
32001
- }
32002
- ]
32061
+ content: [{ type: "text", text: `DAP tool '${toolName}' failed: ${message}. ${cure}` }]
32003
32062
  };
32004
32063
  }
32005
32064
  }
@@ -35076,6 +35135,7 @@ var init_godot_bridge = __esm(() => {
35076
35135
  dapPort: this.connectionInfo?.dapPort,
35077
35136
  debugPort: this.connectionInfo?.debugPort,
35078
35137
  openedByAServer: this.connectionInfo?.openedByAServer,
35138
+ syncsBreakpoints: this.connectionInfo?.syncsBreakpoints,
35079
35139
  pendingRequests: this.pendingRequests.size,
35080
35140
  queuedResources: this.resourceQueues.size,
35081
35141
  listeningSince: this.listeningSince ?? undefined
@@ -35218,6 +35278,7 @@ var init_godot_bridge = __esm(() => {
35218
35278
  this.connectionInfo.dapPort = servedPort(message.dap_port);
35219
35279
  this.connectionInfo.debugPort = servedPort(message.debug_port);
35220
35280
  this.connectionInfo.openedByAServer = message.opened_by_a_server === true;
35281
+ this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
35221
35282
  this.log("info", `Godot ready: ${message.project_path}`);
35222
35283
  this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
35223
35284
  }
@@ -36708,7 +36769,25 @@ function tooNew(unspoken) {
36708
36769
  const half = unspoken.some((game) => game.protocol > RUNTIME_PROTOCOL) ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
36709
36770
  return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
36710
36771
  }
36711
- function chooseRuntime(endpoints, projectPath, unspoken = []) {
36772
+ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
36773
+ if (pid !== undefined) {
36774
+ const named = endpoints.find((endpoint) => endpoint.pid === pid);
36775
+ if (named !== undefined) {
36776
+ if (projectPath !== undefined && resolve4(named.project.path) !== resolve4(projectPath)) {
36777
+ return {
36778
+ problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve4(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
36779
+ };
36780
+ }
36781
+ return { endpoint: named };
36782
+ }
36783
+ const tooNewToo = unspoken.find((game) => game.pid === pid);
36784
+ if (tooNewToo !== undefined) {
36785
+ return { problem: tooNew([tooNewToo]) };
36786
+ }
36787
+ return {
36788
+ problem: endpoints.length === 0 ? `No game with the runtime addon is running, so there is none with pid ${pid}. Start one with editor_run, or play the project from the editor with the addon enabled.` : `No running game has pid ${pid}. Running: ${endpoints.map(describe2).join("; ")}. editor_status lists them under runtimes as they come and go.`
36789
+ };
36790
+ }
36712
36791
  if (endpoints.length === 0) {
36713
36792
  if (unspoken.length > 0) {
36714
36793
  return { problem: tooNew(unspoken) };
@@ -36739,14 +36818,14 @@ function chooseRuntime(endpoints, projectPath, unspoken = []) {
36739
36818
  return { endpoint: matching[0] };
36740
36819
  }
36741
36820
  return {
36742
- problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Stop all but one.`
36821
+ problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
36743
36822
  };
36744
36823
  }
36745
36824
  if (endpoints.length === 1 && endpoints[0]) {
36746
36825
  return { endpoint: endpoints[0] };
36747
36826
  }
36748
36827
  return {
36749
- problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one.`
36828
+ problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
36750
36829
  };
36751
36830
  }
36752
36831
  function runtimeRequest(endpoint, command, params, timeoutMs) {
@@ -37151,6 +37230,9 @@ function patienceForFrames(frames, atLeast) {
37151
37230
  function projectSpelling(projectPath, absolutePath) {
37152
37231
  return `res://${relative3(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
37153
37232
  }
37233
+ function whichGame(args) {
37234
+ return { projectPath: args["projectPath"], pid: args["pid"] };
37235
+ }
37154
37236
  function describeHalt(halt) {
37155
37237
  switch (halt.reason) {
37156
37238
  case "breakpoint":
@@ -37186,6 +37268,19 @@ function runIsUp(run, editorSays) {
37186
37268
  }
37187
37269
  return editorSays ?? stillRunning(run);
37188
37270
  }
37271
+ function endedPreviousRun(ended) {
37272
+ if (ended === null) {
37273
+ return;
37274
+ }
37275
+ return ended.pid ?? true;
37276
+ }
37277
+ function endedToStartThis(ended) {
37278
+ if (ended === null) {
37279
+ return "";
37280
+ }
37281
+ const which = ended.pid === null ? "The game the editor was playing" : `The run that was going, pid ${ended.pid},`;
37282
+ return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
37283
+ }
37189
37284
  function runtimeVerdict(endpoint, after) {
37190
37285
  if (endpoint !== null) {
37191
37286
  return { listening: true, pid: endpoint.pid, port: endpoint.port };
@@ -37884,7 +37979,7 @@ class GodotServer {
37884
37979
  switch (op) {
37885
37980
  case "tree":
37886
37981
  return await this.handleRuntimeCommand("get_tree", {
37887
- projectPath: args["projectPath"],
37982
+ ...whichGame(args),
37888
37983
  root: readNonEmptyString(args, "nodePath") ?? "/root",
37889
37984
  depth: readPositiveNumber(args, "depth") ?? 3,
37890
37985
  include_properties: readBoolean(args, "includeProperties") ?? false
@@ -37893,36 +37988,36 @@ class GodotServer {
37893
37988
  return await this.handleFindRuntimeNodes(args);
37894
37989
  case "text":
37895
37990
  return await this.handleRuntimeCommand("read_text", {
37896
- projectPath: args["projectPath"],
37991
+ ...whichGame(args),
37897
37992
  root: readNonEmptyString(args, "nodePath") ?? "/root",
37898
37993
  include_hidden: readBoolean(args, "includeHidden") ?? false,
37899
37994
  limit: readPositiveNumber(args, "limit") ?? 500
37900
37995
  });
37901
37996
  case "rect":
37902
37997
  return await this.handleRuntimeCommand("get_rect", {
37903
- projectPath: args["projectPath"],
37998
+ ...whichGame(args),
37904
37999
  path: readNonEmptyString(args, "nodePath") ?? ""
37905
38000
  });
37906
38001
  case "property":
37907
38002
  return await this.handleRuntimeCommand("get_property", {
37908
- projectPath: args["projectPath"],
38003
+ ...whichGame(args),
37909
38004
  path: readNonEmptyString(args, "nodePath") ?? "",
37910
38005
  property: readNonEmptyString(args, "property") ?? ""
37911
38006
  });
37912
38007
  default:
37913
38008
  return await this.handleRuntimeCommand("get_metrics", {
37914
- projectPath: args["projectPath"],
38009
+ ...whichGame(args),
37915
38010
  metrics: readArray(args, "metrics") ?? []
37916
38011
  });
37917
38012
  }
37918
38013
  case "runtime_invoke":
37919
38014
  return op === "set" ? await this.handleRuntimeCommand("set_property", {
37920
- projectPath: args["projectPath"],
38015
+ ...whichGame(args),
37921
38016
  path: readNonEmptyString(args, "nodePath") ?? "",
37922
38017
  property: readString(args, "property") ?? "",
37923
38018
  value: args["value"]
37924
38019
  }) : await this.handleRuntimeCommand("call_method", {
37925
- projectPath: args["projectPath"],
38020
+ ...whichGame(args),
37926
38021
  path: readNonEmptyString(args, "nodePath") ?? "",
37927
38022
  method: readString(args, "method") ?? "",
37928
38023
  args: readArray(args, "args") ?? []
@@ -37932,7 +38027,7 @@ class GodotServer {
37932
38027
  case "runtime_input":
37933
38028
  if (op === "click") {
37934
38029
  return await this.handleRuntimeCommand("click", {
37935
- projectPath: args["projectPath"],
38030
+ ...whichGame(args),
37936
38031
  path: readNonEmptyString(args, "nodePath") ?? "",
37937
38032
  button: readString(args, "button") ?? "left",
37938
38033
  double: readBoolean(args, "doubleClick") ?? false
@@ -37940,7 +38035,7 @@ class GodotServer {
37940
38035
  }
37941
38036
  if (op === "choose") {
37942
38037
  return await this.handleRuntimeCommand("choose", {
37943
- projectPath: args["projectPath"],
38038
+ ...whichGame(args),
37944
38039
  path: readNonEmptyString(args, "nodePath") ?? "",
37945
38040
  text: readString(args, "text") ?? "",
37946
38041
  ...args["index"] === undefined ? {} : { index: args["index"] }
@@ -37971,7 +38066,9 @@ class GodotServer {
37971
38066
  return this.jsonTextResponse({
37972
38067
  ...asParams(JSON.parse(answered.content[0]?.text ?? "{}")),
37973
38068
  held: this.breakpointsAsProjectSpells(project.value.path, held),
37974
- 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."
38069
+ setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor()),
38070
+ 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.",
38071
+ ...this.breakpointsAtRisk()
37975
38072
  });
37976
38073
  }
37977
38074
  case "debug_control":
@@ -38045,6 +38142,16 @@ class GodotServer {
38045
38142
  response: this.createErrorResponse("No game is running, so there is no debug session to answer for.", ["Start one with editor_run start, which has the editor play it"])
38046
38143
  };
38047
38144
  }
38145
+ const going = await this.runStillGoing(game);
38146
+ if (!going) {
38147
+ return {
38148
+ ok: false,
38149
+ response: this.createErrorResponse("The last run has already ended, so there is no debug session to answer for.", [
38150
+ "editor_output still reads what it printed, until the next run starts",
38151
+ "editor_run start plays another, which the debugger can hold"
38152
+ ])
38153
+ };
38154
+ }
38048
38155
  if (!game.throughEditor) {
38049
38156
  return {
38050
38157
  ok: false,
@@ -38066,7 +38173,7 @@ class GodotServer {
38066
38173
  };
38067
38174
  }
38068
38175
  if (!this.dap().isStopped()) {
38069
- const hold = await this.holdOf(game);
38176
+ const hold = await this.holdOf(game, going);
38070
38177
  if (hold.heldAt !== null) {
38071
38178
  if (intent === "move") {
38072
38179
  return { ok: true, value: game };
@@ -38632,6 +38739,12 @@ class GodotServer {
38632
38739
  this.dapClient.setOutputSink((line) => {
38633
38740
  this.writeToTranscript(line);
38634
38741
  });
38742
+ this.dapClient.setBreakpointsSink((held) => {
38743
+ const project = this.godotBridge.getStatus().projectPath ?? this.ownProject;
38744
+ if (project !== null) {
38745
+ writeBreakpointNote(project, held);
38746
+ }
38747
+ });
38635
38748
  }
38636
38749
  return this.dapClient;
38637
38750
  }
@@ -38643,6 +38756,15 @@ class GodotServer {
38643
38756
  }
38644
38757
  return client;
38645
38758
  }
38759
+ breakpointsAtRisk() {
38760
+ const status = this.godotBridge.getStatus();
38761
+ if (!status.connected || status.syncsBreakpoints === true) {
38762
+ return {};
38763
+ }
38764
+ return {
38765
+ 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."
38766
+ };
38767
+ }
38646
38768
  breakpointsAsProjectSpells(projectPath, held) {
38647
38769
  return held.map((one) => ({
38648
38770
  scriptPath: projectSpelling(projectPath, one.scriptPath),
@@ -38705,6 +38827,7 @@ class GodotServer {
38705
38827
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
38706
38828
  startupError: this.bridgeStartupError,
38707
38829
  staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
38830
+ ...this.breakpointsAtRisk(),
38708
38831
  retryingBridge: this.bridgeRetry === null ? undefined : true,
38709
38832
  announcedAt: this.announcedAt ?? undefined,
38710
38833
  projectIs: this.projectHasMovedOn() ?? undefined,
@@ -38756,9 +38879,14 @@ class GodotServer {
38756
38879
  const playing = await this.editorPlayingState();
38757
38880
  await this.pickUpWhatTheEditorIsPlaying();
38758
38881
  const current = this.currentRun();
38759
- return { playing, hold: current === null ? {} : await this.holdOf(current) };
38882
+ const active = current === null ? false : await this.runStillGoing(current, playing?.playing ?? null);
38883
+ return {
38884
+ playing,
38885
+ active,
38886
+ hold: current === null ? {} : await this.holdOf(current, active)
38887
+ };
38760
38888
  })();
38761
- const [games, { playing, hold }] = await Promise.all([pinged, asked]);
38889
+ const [games, { playing, active, hold }] = await Promise.all([pinged, asked]);
38762
38890
  return this.jsonTextResponse({
38763
38891
  editor: {
38764
38892
  ...this.getEditorStatusPayload(),
@@ -38770,7 +38898,7 @@ class GodotServer {
38770
38898
  problem: located.ok ? undefined : GodotLocator2.refusal(located).message
38771
38899
  },
38772
38900
  game: {
38773
- processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
38901
+ processActive: active,
38774
38902
  playingInEditor: playing,
38775
38903
  ...hold,
38776
38904
  runtimeConnected: games.some((game) => game.reachable),
@@ -38983,10 +39111,12 @@ class GodotServer {
38983
39111
  if (op === "check") {
38984
39112
  return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
38985
39113
  }
39114
+ await this.pickUpWhatTheEditorIsPlaying();
38986
39115
  let ended = null;
38987
- if (stillRunning(this.currentRun())) {
39116
+ const before = this.currentRun();
39117
+ if (before !== null && await this.runStillGoing(before)) {
38988
39118
  this.logDebug("Ending the running game before starting another");
38989
- ended = this.currentRun()?.pid ?? null;
39119
+ ended = { pid: before.pid ?? this.announcedPidOf(before) ?? null };
38990
39120
  await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
38991
39121
  }
38992
39122
  const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
@@ -39010,8 +39140,8 @@ class GodotServer {
39010
39140
  const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
39011
39141
  this.activeProcess = started;
39012
39142
  const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.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." : "";
39013
- const endedForThis = ended === null ? "" : ` The run that was going, pid ${ended}, was ended to start this one; its output is no longer what editor_output answers about.`;
39014
- const stranded = wereHereFirst.filter((one) => one.pid !== ended && alive(one.pid));
39143
+ const endedForThis = endedToStartThis(ended);
39144
+ const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
39015
39145
  const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
39016
39146
  const alsoRunning = stranded.length === 0 ? "" : ` A game of this project was already running and still is: ${named}. Starting this one did not end it and nothing here can, because this server did not start it. End it yourself, or bring the halves to one version where a protocol is named.`;
39017
39147
  return this.jsonTextResponse({
@@ -39022,23 +39152,38 @@ class GodotServer {
39022
39152
  arguments: cmdArgs,
39023
39153
  transcript: started.transcript,
39024
39154
  refreshedClasses: refreshed.value,
39025
- endedPreviousRun: ended ?? undefined,
39155
+ endedPreviousRun: endedPreviousRun(ended),
39026
39156
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
39027
39157
  message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
39028
39158
  });
39029
39159
  }
39030
- async gameIsUp() {
39031
- const run = this.currentRun();
39032
- const editorSays = run?.throughEditor === true ? (await this.editorPlayingState())?.playing ?? null : null;
39033
- return runIsUp(run, editorSays);
39034
- }
39035
39160
  async runtimeUp(projectPath, before, budgetMs, withArgs) {
39036
39161
  if (!existsSync14(join19(projectPath, RUNTIME_AUTOLOAD2.path))) {
39037
39162
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
39038
39163
  }
39164
+ let asking = null;
39165
+ let editorAskedAt = 0;
39166
+ let editorSaysGoing = true;
39039
39167
  const endpoint = await announcedSince(projectPath, before, {
39040
39168
  budgetMs,
39041
- giveUp: () => this.dapClient?.isStopped() === true || !stillRunning(this.currentRun())
39169
+ giveUp: () => {
39170
+ if (this.dapClient?.isStopped() === true) {
39171
+ return true;
39172
+ }
39173
+ const run = this.currentRun();
39174
+ if (run === null || !run.throughEditor) {
39175
+ return !stillRunning(run);
39176
+ }
39177
+ if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
39178
+ editorAskedAt = Date.now();
39179
+ asking = this.runStillGoing(run).then((going) => {
39180
+ editorSaysGoing = going;
39181
+ }).finally(() => {
39182
+ asking = null;
39183
+ });
39184
+ }
39185
+ return !editorSaysGoing;
39186
+ }
39042
39187
  });
39043
39188
  const going = this.currentRun();
39044
39189
  if (endpoint !== null && going?.throughEditor === true && going.announcedPid === undefined) {
@@ -39048,7 +39193,7 @@ class GodotServer {
39048
39193
  addon: true,
39049
39194
  budgetMs,
39050
39195
  heldAt: this.dapClient?.whereItStopped() ?? null,
39051
- running: await this.gameIsUp(),
39196
+ running: endpoint !== null || going !== null && await this.runStillGoing(going),
39052
39197
  withArgs
39053
39198
  });
39054
39199
  }
@@ -39089,7 +39234,8 @@ class GodotServer {
39089
39234
  startedAt,
39090
39235
  exitCode: null,
39091
39236
  throughEditor: true,
39092
- brokeOn: null
39237
+ brokeOn: null,
39238
+ announcedBefore: alreadyPlaying
39093
39239
  };
39094
39240
  this.activeProcess = played;
39095
39241
  writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
@@ -39100,14 +39246,14 @@ class GodotServer {
39100
39246
  scene: scene === null ? "the main scene" : `res://${scene}`,
39101
39247
  refreshedClasses,
39102
39248
  debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
39103
- endedPreviousRun: ended ?? undefined,
39249
+ endedPreviousRun: endedPreviousRun(ended),
39104
39250
  breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
39105
39251
  breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
39106
39252
  scriptPath: projectSpelling(projectPath, one.scriptPath),
39107
39253
  reason: one.reason
39108
39254
  })),
39109
39255
  runtime: await this.runtimeUp(projectPath, alreadyPlaying, runtimeWaitMs, false),
39110
- message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it." + (ended === null ? "" : ` The run that was going, pid ${ended}, was ended to start this one; its output is no longer what editor_output answers about.`)
39256
+ message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it." + endedToStartThis(ended)
39111
39257
  });
39112
39258
  }
39113
39259
  drainEditorOutput(game) {
@@ -39323,15 +39469,19 @@ class GodotServer {
39323
39469
  brokeOn: null,
39324
39470
  pickedUpPlaying: true
39325
39471
  };
39326
- const announced = this.theOneAnnouncedForOurProject();
39327
- if (announced !== undefined) {
39328
- picked.announcedPid = announced.pid;
39329
- }
39330
39472
  this.activeProcess = picked;
39331
39473
  }
39332
- theOneAnnouncedForOurProject() {
39333
- const ofProject = this.allAnnouncedForOurProject(runtimesAnnounced().running);
39334
- return ofProject.length === 1 ? ofProject[0] : undefined;
39474
+ announcedPidOf(run) {
39475
+ if (!run.throughEditor || run.announcedPid !== undefined) {
39476
+ return run.announcedPid;
39477
+ }
39478
+ const before = run.announcedBefore ?? new Set;
39479
+ const fresh = this.allAnnouncedForOurProject(runtimesAnnounced().running).filter((one) => !before.has(one.pid));
39480
+ const theOne = fresh.length === 1 ? fresh[0] : undefined;
39481
+ if (theOne !== undefined) {
39482
+ run.announcedPid = theOne.pid;
39483
+ }
39484
+ return run.announcedPid;
39335
39485
  }
39336
39486
  adoptRecordedRun() {
39337
39487
  const record = readRunRecord();
@@ -39461,12 +39611,22 @@ class GodotServer {
39461
39611
  }
39462
39612
  const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
39463
39613
  const until = Date.now() + budgetMs;
39464
- while (stillRunning(run) && Date.now() < until) {
39614
+ while (await this.runStillGoing(run) && Date.now() < until) {
39465
39615
  this.drainEditorOutput(run);
39466
39616
  this.drainTranscript(run);
39467
39617
  await delay3(RUN_POLL_MS);
39468
39618
  }
39469
- return await this.handleGetDebugOutput(args, stillRunning(run) ? budgetMs : undefined);
39619
+ return await this.handleGetDebugOutput(args, await this.runStillGoing(run) ? budgetMs : undefined);
39620
+ }
39621
+ async runStillGoing(run, editorSays) {
39622
+ if (!run.throughEditor) {
39623
+ return stillRunning(run);
39624
+ }
39625
+ const announced = this.announcedPidOf(run);
39626
+ if (announced !== undefined && !alive(announced)) {
39627
+ return false;
39628
+ }
39629
+ return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
39470
39630
  }
39471
39631
  async handleGetDebugOutput(args, waitedMs) {
39472
39632
  await this.pickUpWhatTheEditorIsPlaying();
@@ -39476,7 +39636,8 @@ class GodotServer {
39476
39636
  }
39477
39637
  this.drainEditorOutput(run);
39478
39638
  this.drainTranscript(run);
39479
- if (run.endedUnwatched !== true && !stillRunning(run)) {
39639
+ const going = await this.runStillGoing(run);
39640
+ if (run.endedUnwatched !== true && !going) {
39480
39641
  run.log.finish();
39481
39642
  run.endedUnwatched = run.exitCode === null;
39482
39643
  }
@@ -39487,12 +39648,13 @@ class GodotServer {
39487
39648
  contains: readNonEmptyString(args, "contains"),
39488
39649
  limit: readPositiveNumber(args, "limit") ?? 200
39489
39650
  });
39490
- const hold = await this.holdOf(run);
39651
+ const hold = await this.holdOf(run, going);
39491
39652
  const wanted = readBoolean(args, "cpu") ?? false;
39492
- const cpuSeconds = wanted && run.pid !== null && stillRunning(run) ? await cpuSecondsOf(run.pid) : undefined;
39653
+ const processToAsk = run.pid ?? this.announcedPidOf(run) ?? null;
39654
+ const cpuSeconds = wanted && processToAsk !== null && stillRunning(run) ? await cpuSecondsOf(processToAsk) : undefined;
39493
39655
  const notes = [];
39494
39656
  if (wanted && cpuSeconds === undefined && stillRunning(run)) {
39495
- notes.push(run.pid === null ? "cpu was asked for and there is no process to ask: the editor is playing this run and holds it, so nothing here has its process id. editor_run start without a connected editor, or with args, starts the game here instead and that run answers cpuSeconds." : "cpu was asked for and this platform would not say what the run has used.");
39657
+ notes.push(processToAsk === null ? "cpu was asked for and there is no process to ask: the editor is playing this run and holds it, and the game announced no runtime, so nothing here has its process id. A game with the runtime addon announces one; editor_run start without a connected editor, or with args, starts the game here instead and that run answers cpuSeconds either way." : "cpu was asked for and this platform would not say what the run has used.");
39496
39658
  }
39497
39659
  if (waitedMs !== undefined) {
39498
39660
  notes.push(`The wait of ${waitedMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
@@ -39516,10 +39678,10 @@ class GodotServer {
39516
39678
  notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.`);
39517
39679
  }
39518
39680
  return this.jsonTextResponse({
39519
- running: stillRunning(run),
39681
+ running: going,
39520
39682
  exitCode: run.exitCode,
39521
39683
  through: run.throughEditor ? "editor" : "gdharness",
39522
- pid: run.pid,
39684
+ pid: run.pid ?? this.announcedPidOf(run) ?? null,
39523
39685
  errors: run.log.count("error"),
39524
39686
  warnings: run.log.count("warning"),
39525
39687
  clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
@@ -39545,18 +39707,19 @@ class GodotServer {
39545
39707
  }
39546
39708
  this.drainEditorOutput(stopped);
39547
39709
  this.drainTranscript(stopped);
39710
+ const wasRunning = await this.runStillGoing(stopped);
39548
39711
  this.logDebug("Stopping the running game");
39549
39712
  await this.endActiveGame("editor_run stop");
39550
39713
  return this.jsonTextResponse({
39551
39714
  stopped: true,
39552
39715
  through: stopped.throughEditor ? "editor" : "gdharness",
39553
- endedPid: stopped.pid,
39554
- exitedBeforeStop: stopped.exitCode !== null,
39716
+ endedPid: stopped.pid ?? this.announcedPidOf(stopped) ?? null,
39717
+ exitedBeforeStop: stopped.exitCode !== null || !wasRunning,
39555
39718
  exitCode: stopped.exitCode,
39556
39719
  errors: stopped.log.count("error"),
39557
39720
  warnings: stopped.log.count("warning"),
39558
39721
  clean: stopped.log.count("error") === 0,
39559
- note: stopped.throughEditor ? "The editor was asked to stop the scene it is playing. Anything that game started for itself, with OS.create_process or otherwise, is not the editor's to stop and is still running." : "The process named under endedPid was ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and is still running.",
39722
+ note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : stopped.throughEditor ? "The editor was asked to stop the scene it is playing. Anything that game started for itself, with OS.create_process or otherwise, is not the editor's to stop and is still running." : "The process named under endedPid was ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and is still running.",
39560
39723
  entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
39561
39724
  });
39562
39725
  }
@@ -39713,21 +39876,21 @@ class GodotServer {
39713
39876
  if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
39714
39877
  return null;
39715
39878
  }
39716
- const its = run.announcedPid ?? this.theOneAnnouncedForOurProject()?.pid;
39717
- if (its !== endpoint.pid || !session.holdIsKnown()) {
39879
+ if (this.announcedPidOf(run) !== endpoint.pid || !session.holdIsKnown()) {
39718
39880
  return null;
39719
39881
  }
39720
39882
  return session.whereItStopped();
39721
39883
  }
39722
- async holdOf(run) {
39723
- if (!run.throughEditor || !stillRunning(run)) {
39884
+ async holdOf(run, going) {
39885
+ if (!run.throughEditor || !(going ?? await this.runStillGoing(run))) {
39724
39886
  return { heldAt: null };
39725
39887
  }
39726
39888
  const session = this.dapClient;
39727
39889
  if (session?.holdIsKnown()) {
39728
39890
  return { heldAt: session.whereItStopped() };
39729
39891
  }
39730
- const endpoint = run.announcedPid !== undefined ? runtimesAnnounced().running.find((one) => one.pid === run.announcedPid) : this.theOneAnnouncedForOurProject();
39892
+ const its = this.announcedPidOf(run);
39893
+ const endpoint = its === undefined ? undefined : runtimesAnnounced().running.find((one) => one.pid === its);
39731
39894
  if (endpoint === undefined) {
39732
39895
  return {
39733
39896
  heldAt: undefined,
@@ -39752,9 +39915,9 @@ class GodotServer {
39752
39915
  return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
39753
39916
  }
39754
39917
  async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
39755
- const { op: _op, projectPath, ...params } = asParams(args);
39918
+ const { op: _op, projectPath, pid, ...params } = asParams(args);
39756
39919
  const announced = runtimesAnnounced();
39757
- const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken);
39920
+ const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken, typeof pid === "number" ? pid : undefined);
39758
39921
  if ("problem" in choice) {
39759
39922
  return this.createErrorResponse(choice.problem);
39760
39923
  }
@@ -39820,7 +39983,7 @@ class GodotServer {
39820
39983
  const includeHidden = readBoolean(args, "includeHidden");
39821
39984
  return await this.handleRuntimeCommand("find_nodes", {
39822
39985
  ...filters,
39823
- projectPath: args["projectPath"],
39986
+ ...whichGame(args),
39824
39987
  root: readNonEmptyString(args, "nodePath") ?? "/root",
39825
39988
  limit: readPositiveNumber(args, "limit") ?? 100,
39826
39989
  ...property === undefined ? {} : { property },
@@ -39831,18 +39994,18 @@ class GodotServer {
39831
39994
  if (op === "frames") {
39832
39995
  const frames = readPositiveNumber(args, "frames") ?? 1;
39833
39996
  const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
39834
- return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
39997
+ return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
39835
39998
  }
39836
39999
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
39837
40000
  const nodePath = readNonEmptyString(args, "nodePath") ?? "";
39838
40001
  const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
39839
40002
  return op === "signal" ? await this.handleRuntimeCommand("wait_signal", {
39840
- projectPath: args["projectPath"],
40003
+ ...whichGame(args),
39841
40004
  path: nodePath,
39842
40005
  signal: readString(args, "signal") ?? "",
39843
40006
  timeout_ms: timeoutMs
39844
40007
  }, patience) : await this.handleRuntimeCommand("wait_until", {
39845
- projectPath: args["projectPath"],
40008
+ ...whichGame(args),
39846
40009
  path: nodePath,
39847
40010
  property: readString(args, "property") ?? "",
39848
40011
  value: args["value"],
@@ -41397,7 +41560,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
41397
41560
  ## Start here
41398
41561
 
41399
41562
  - ${projectPathSentence()}. The \`runtime_*\` and \`debug_*\` ones pick between running
41400
- games instead; the others answer about the editor this server is connected to. There is no
41563
+ games instead, by \`projectPath\`, or by \`pid\` when several run from one project, as a bench
41564
+ and its workers do; the others answer about the editor this server is connected to. There is no
41401
41565
  ambient project, and an argument a tool does not declare is refused rather than ignored.
41402
41566
  - \`editor_status\` says whether an editor is connected and whether its addon matches the server.
41403
41567
  \`addonIsStale\` means they differ, and \`staleNote\` says which half is behind: an editor that
@@ -41469,6 +41633,12 @@ The autoload reaches an export. \`gdharness runtime off\` in the project before
41469
41633
  move. There is no pause and no step out: Godot's debug adapter implements neither, so use a
41470
41634
  breakpoint on the line you want instead.
41471
41635
 
41636
+ A held game draws nothing and answers no \`runtime_*\` call. The server says so at once when it
41637
+ knows, naming why the game is held and \`debug_control continue\` as what lets it go, and
41638
+ \`editor_output\` and \`editor_status\` carry the same under \`heldAt\`. A breakpoint set with
41639
+ \`debug_breakpoint\` holds for every start until \`debug_breakpoint remove\`, across a reconnect,
41640
+ so take it off when the question it was for is answered.
41641
+
41472
41642
  ## Editing
41473
41643
 
41474
41644
  \`scene_tree\` for what a scene holds, rather than what the \`.tscn\` text implies. \`scene_node\`,