gdharness 0.13.39 → 0.13.41

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".'
@@ -16057,7 +16061,7 @@ var init_tool_definitions = __esm(() => {
16057
16061
  },
16058
16062
  {
16059
16063
  name: "editor_status",
16060
- description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
16064
+ description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
16061
16065
  parameters: {},
16062
16066
  requires: []
16063
16067
  },
@@ -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
  }
@@ -36386,7 +36447,7 @@ function secondsFromClock(printed) {
36386
36447
  const extra = days === null ? 0 : Number(days) * 86400;
36387
36448
  return Number.isFinite(extra) ? seconds + extra : undefined;
36388
36449
  }
36389
- var run5, ASK_TIMEOUT_MS = 2000;
36450
+ var run5, ASK_TIMEOUT_MS = 6000;
36390
36451
  var init_process_time = __esm(() => {
36391
36452
  run5 = promisify5(execFile5);
36392
36453
  });
@@ -36615,7 +36676,18 @@ function parseAnnouncement(file, pid) {
36615
36676
  if (protocol !== RUNTIME_PROTOCOL) {
36616
36677
  return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
36617
36678
  }
36618
- return { kind: "runtime", endpoint: { pid, port, address, project: named, file } };
36679
+ const editorPid = readNumber(fields, "editor_pid");
36680
+ return {
36681
+ kind: "runtime",
36682
+ endpoint: {
36683
+ pid,
36684
+ port,
36685
+ address,
36686
+ project: named,
36687
+ file,
36688
+ ...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
36689
+ }
36690
+ };
36619
36691
  }
36620
36692
  function runtimesAnnounced(directories = runtimeDirectories()) {
36621
36693
  const running = new Map;
@@ -36689,7 +36761,7 @@ async function announcedSince(projectPath, before, waiting = {}) {
36689
36761
  const directories = waiting.directories ?? runtimeDirectories();
36690
36762
  const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
36691
36763
  for (;; ) {
36692
- const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted);
36764
+ const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted && (waiting.accept?.(endpoint) ?? true));
36693
36765
  if (fresh !== undefined) {
36694
36766
  return fresh;
36695
36767
  }
@@ -36708,7 +36780,25 @@ function tooNew(unspoken) {
36708
36780
  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
36781
  return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
36710
36782
  }
36711
- function chooseRuntime(endpoints, projectPath, unspoken = []) {
36783
+ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
36784
+ if (pid !== undefined) {
36785
+ const named = endpoints.find((endpoint) => endpoint.pid === pid);
36786
+ if (named !== undefined) {
36787
+ if (projectPath !== undefined && resolve4(named.project.path) !== resolve4(projectPath)) {
36788
+ return {
36789
+ problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve4(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
36790
+ };
36791
+ }
36792
+ return { endpoint: named };
36793
+ }
36794
+ const tooNewToo = unspoken.find((game) => game.pid === pid);
36795
+ if (tooNewToo !== undefined) {
36796
+ return { problem: tooNew([tooNewToo]) };
36797
+ }
36798
+ return {
36799
+ 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.`
36800
+ };
36801
+ }
36712
36802
  if (endpoints.length === 0) {
36713
36803
  if (unspoken.length > 0) {
36714
36804
  return { problem: tooNew(unspoken) };
@@ -36739,14 +36829,14 @@ function chooseRuntime(endpoints, projectPath, unspoken = []) {
36739
36829
  return { endpoint: matching[0] };
36740
36830
  }
36741
36831
  return {
36742
- problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Stop all but one.`
36832
+ problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
36743
36833
  };
36744
36834
  }
36745
36835
  if (endpoints.length === 1 && endpoints[0]) {
36746
36836
  return { endpoint: endpoints[0] };
36747
36837
  }
36748
36838
  return {
36749
- problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one.`
36839
+ problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
36750
36840
  };
36751
36841
  }
36752
36842
  function runtimeRequest(endpoint, command, params, timeoutMs) {
@@ -37151,6 +37241,9 @@ function patienceForFrames(frames, atLeast) {
37151
37241
  function projectSpelling(projectPath, absolutePath) {
37152
37242
  return `res://${relative3(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
37153
37243
  }
37244
+ function whichGame(args) {
37245
+ return { projectPath: args["projectPath"], pid: args["pid"] };
37246
+ }
37154
37247
  function describeHalt(halt) {
37155
37248
  switch (halt.reason) {
37156
37249
  case "breakpoint":
@@ -37186,6 +37279,19 @@ function runIsUp(run, editorSays) {
37186
37279
  }
37187
37280
  return editorSays ?? stillRunning(run);
37188
37281
  }
37282
+ function endedPreviousRun(ended) {
37283
+ if (ended === null) {
37284
+ return;
37285
+ }
37286
+ return ended.pid ?? true;
37287
+ }
37288
+ function endedToStartThis(ended) {
37289
+ if (ended === null) {
37290
+ return "";
37291
+ }
37292
+ const which = ended.pid === null ? "The game the editor was playing" : `The run that was going, pid ${ended.pid},`;
37293
+ return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
37294
+ }
37189
37295
  function runtimeVerdict(endpoint, after) {
37190
37296
  if (endpoint !== null) {
37191
37297
  return { listening: true, pid: endpoint.pid, port: endpoint.port };
@@ -37330,6 +37436,16 @@ function hasMainScene(projectFile) {
37330
37436
  const scene = parseProjectGodot(readFileSync17(projectFile, "utf8"))["application"]?.["run/main_scene"];
37331
37437
  return typeof scene === "string" && scene !== "";
37332
37438
  }
37439
+ function runtimeAddonAnnouncesEditors(projectPath) {
37440
+ if (projectPath === null) {
37441
+ return false;
37442
+ }
37443
+ try {
37444
+ return readFileSync17(join19(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
37445
+ } catch {
37446
+ return false;
37447
+ }
37448
+ }
37333
37449
  function editorPlaysHeadless(projectFile) {
37334
37450
  const runArgs = parseProjectGodot(readFileSync17(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
37335
37451
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
@@ -37884,7 +38000,7 @@ class GodotServer {
37884
38000
  switch (op) {
37885
38001
  case "tree":
37886
38002
  return await this.handleRuntimeCommand("get_tree", {
37887
- projectPath: args["projectPath"],
38003
+ ...whichGame(args),
37888
38004
  root: readNonEmptyString(args, "nodePath") ?? "/root",
37889
38005
  depth: readPositiveNumber(args, "depth") ?? 3,
37890
38006
  include_properties: readBoolean(args, "includeProperties") ?? false
@@ -37893,36 +38009,36 @@ class GodotServer {
37893
38009
  return await this.handleFindRuntimeNodes(args);
37894
38010
  case "text":
37895
38011
  return await this.handleRuntimeCommand("read_text", {
37896
- projectPath: args["projectPath"],
38012
+ ...whichGame(args),
37897
38013
  root: readNonEmptyString(args, "nodePath") ?? "/root",
37898
38014
  include_hidden: readBoolean(args, "includeHidden") ?? false,
37899
38015
  limit: readPositiveNumber(args, "limit") ?? 500
37900
38016
  });
37901
38017
  case "rect":
37902
38018
  return await this.handleRuntimeCommand("get_rect", {
37903
- projectPath: args["projectPath"],
38019
+ ...whichGame(args),
37904
38020
  path: readNonEmptyString(args, "nodePath") ?? ""
37905
38021
  });
37906
38022
  case "property":
37907
38023
  return await this.handleRuntimeCommand("get_property", {
37908
- projectPath: args["projectPath"],
38024
+ ...whichGame(args),
37909
38025
  path: readNonEmptyString(args, "nodePath") ?? "",
37910
38026
  property: readNonEmptyString(args, "property") ?? ""
37911
38027
  });
37912
38028
  default:
37913
38029
  return await this.handleRuntimeCommand("get_metrics", {
37914
- projectPath: args["projectPath"],
38030
+ ...whichGame(args),
37915
38031
  metrics: readArray(args, "metrics") ?? []
37916
38032
  });
37917
38033
  }
37918
38034
  case "runtime_invoke":
37919
38035
  return op === "set" ? await this.handleRuntimeCommand("set_property", {
37920
- projectPath: args["projectPath"],
38036
+ ...whichGame(args),
37921
38037
  path: readNonEmptyString(args, "nodePath") ?? "",
37922
38038
  property: readString(args, "property") ?? "",
37923
38039
  value: args["value"]
37924
38040
  }) : await this.handleRuntimeCommand("call_method", {
37925
- projectPath: args["projectPath"],
38041
+ ...whichGame(args),
37926
38042
  path: readNonEmptyString(args, "nodePath") ?? "",
37927
38043
  method: readString(args, "method") ?? "",
37928
38044
  args: readArray(args, "args") ?? []
@@ -37932,7 +38048,7 @@ class GodotServer {
37932
38048
  case "runtime_input":
37933
38049
  if (op === "click") {
37934
38050
  return await this.handleRuntimeCommand("click", {
37935
- projectPath: args["projectPath"],
38051
+ ...whichGame(args),
37936
38052
  path: readNonEmptyString(args, "nodePath") ?? "",
37937
38053
  button: readString(args, "button") ?? "left",
37938
38054
  double: readBoolean(args, "doubleClick") ?? false
@@ -37940,7 +38056,7 @@ class GodotServer {
37940
38056
  }
37941
38057
  if (op === "choose") {
37942
38058
  return await this.handleRuntimeCommand("choose", {
37943
- projectPath: args["projectPath"],
38059
+ ...whichGame(args),
37944
38060
  path: readNonEmptyString(args, "nodePath") ?? "",
37945
38061
  text: readString(args, "text") ?? "",
37946
38062
  ...args["index"] === undefined ? {} : { index: args["index"] }
@@ -37971,7 +38087,9 @@ class GodotServer {
37971
38087
  return this.jsonTextResponse({
37972
38088
  ...asParams(JSON.parse(answered.content[0]?.text ?? "{}")),
37973
38089
  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."
38090
+ setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor()),
38091
+ 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.",
38092
+ ...this.breakpointsAtRisk()
37975
38093
  });
37976
38094
  }
37977
38095
  case "debug_control":
@@ -38045,6 +38163,16 @@ class GodotServer {
38045
38163
  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
38164
  };
38047
38165
  }
38166
+ const going = await this.runStillGoing(game);
38167
+ if (!going) {
38168
+ return {
38169
+ ok: false,
38170
+ response: this.createErrorResponse("The last run has already ended, so there is no debug session to answer for.", [
38171
+ "editor_output still reads what it printed, until the next run starts",
38172
+ "editor_run start plays another, which the debugger can hold"
38173
+ ])
38174
+ };
38175
+ }
38048
38176
  if (!game.throughEditor) {
38049
38177
  return {
38050
38178
  ok: false,
@@ -38066,7 +38194,7 @@ class GodotServer {
38066
38194
  };
38067
38195
  }
38068
38196
  if (!this.dap().isStopped()) {
38069
- const hold = await this.holdOf(game);
38197
+ const hold = await this.holdOf(game, going);
38070
38198
  if (hold.heldAt !== null) {
38071
38199
  if (intent === "move") {
38072
38200
  return { ok: true, value: game };
@@ -38632,6 +38760,12 @@ class GodotServer {
38632
38760
  this.dapClient.setOutputSink((line) => {
38633
38761
  this.writeToTranscript(line);
38634
38762
  });
38763
+ this.dapClient.setBreakpointsSink((held) => {
38764
+ const project = this.godotBridge.getStatus().projectPath ?? this.ownProject;
38765
+ if (project !== null) {
38766
+ writeBreakpointNote(project, held);
38767
+ }
38768
+ });
38635
38769
  }
38636
38770
  return this.dapClient;
38637
38771
  }
@@ -38643,6 +38777,15 @@ class GodotServer {
38643
38777
  }
38644
38778
  return client;
38645
38779
  }
38780
+ breakpointsAtRisk() {
38781
+ const status = this.godotBridge.getStatus();
38782
+ if (!status.connected || status.syncsBreakpoints === true) {
38783
+ return {};
38784
+ }
38785
+ return {
38786
+ 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."
38787
+ };
38788
+ }
38646
38789
  breakpointsAsProjectSpells(projectPath, held) {
38647
38790
  return held.map((one) => ({
38648
38791
  scriptPath: projectSpelling(projectPath, one.scriptPath),
@@ -38705,6 +38848,7 @@ class GodotServer {
38705
38848
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
38706
38849
  startupError: this.bridgeStartupError,
38707
38850
  staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
38851
+ ...this.breakpointsAtRisk(),
38708
38852
  retryingBridge: this.bridgeRetry === null ? undefined : true,
38709
38853
  announcedAt: this.announcedAt ?? undefined,
38710
38854
  projectIs: this.projectHasMovedOn() ?? undefined,
@@ -38739,7 +38883,7 @@ class GodotServer {
38739
38883
  const located = await this.locator.find();
38740
38884
  const announced = runtimesAnnounced();
38741
38885
  const pinged = Promise.all(announced.running.map(async (endpoint) => {
38742
- const held = this.knownHoldOn(endpoint);
38886
+ const held = this.knownHoldOn(endpoint, announced.running);
38743
38887
  const reply = held === null ? await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS) : {
38744
38888
  ok: false,
38745
38889
  message: `The game is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is. debug_control continue lets it go.`
@@ -38748,6 +38892,7 @@ class GodotServer {
38748
38892
  pid: endpoint.pid,
38749
38893
  port: endpoint.port,
38750
38894
  project: endpoint.project,
38895
+ editorPid: endpoint.editorPid,
38751
38896
  reachable: reply.ok,
38752
38897
  problem: reply.ok ? null : reply.message
38753
38898
  };
@@ -38756,9 +38901,14 @@ class GodotServer {
38756
38901
  const playing = await this.editorPlayingState();
38757
38902
  await this.pickUpWhatTheEditorIsPlaying();
38758
38903
  const current = this.currentRun();
38759
- return { playing, hold: current === null ? {} : await this.holdOf(current) };
38904
+ const active = current === null ? false : await this.runStillGoing(current, playing?.playing ?? null);
38905
+ return {
38906
+ playing,
38907
+ active,
38908
+ hold: current === null ? {} : await this.holdOf(current, active)
38909
+ };
38760
38910
  })();
38761
- const [games, { playing, hold }] = await Promise.all([pinged, asked]);
38911
+ const [games, { playing, active, hold }] = await Promise.all([pinged, asked]);
38762
38912
  return this.jsonTextResponse({
38763
38913
  editor: {
38764
38914
  ...this.getEditorStatusPayload(),
@@ -38770,7 +38920,7 @@ class GodotServer {
38770
38920
  problem: located.ok ? undefined : GodotLocator2.refusal(located).message
38771
38921
  },
38772
38922
  game: {
38773
- processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
38923
+ processActive: active,
38774
38924
  playingInEditor: playing,
38775
38925
  ...hold,
38776
38926
  runtimeConnected: games.some((game) => game.reachable),
@@ -38983,10 +39133,12 @@ class GodotServer {
38983
39133
  if (op === "check") {
38984
39134
  return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
38985
39135
  }
39136
+ await this.pickUpWhatTheEditorIsPlaying();
38986
39137
  let ended = null;
38987
- if (stillRunning(this.currentRun())) {
39138
+ const before = this.currentRun();
39139
+ if (before !== null && await this.runStillGoing(before)) {
38988
39140
  this.logDebug("Ending the running game before starting another");
38989
- ended = this.currentRun()?.pid ?? null;
39141
+ ended = { pid: before.pid ?? this.announcedPidOf(before) ?? null };
38990
39142
  await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
38991
39143
  }
38992
39144
  const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
@@ -39010,8 +39162,8 @@ class GodotServer {
39010
39162
  const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
39011
39163
  this.activeProcess = started;
39012
39164
  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));
39165
+ const endedForThis = endedToStartThis(ended);
39166
+ const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
39015
39167
  const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
39016
39168
  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
39169
  return this.jsonTextResponse({
@@ -39022,23 +39174,39 @@ class GodotServer {
39022
39174
  arguments: cmdArgs,
39023
39175
  transcript: started.transcript,
39024
39176
  refreshedClasses: refreshed.value,
39025
- endedPreviousRun: ended ?? undefined,
39177
+ endedPreviousRun: endedPreviousRun(ended),
39026
39178
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
39027
39179
  message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
39028
39180
  });
39029
39181
  }
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
39182
  async runtimeUp(projectPath, before, budgetMs, withArgs) {
39036
39183
  if (!existsSync14(join19(projectPath, RUNTIME_AUTOLOAD2.path))) {
39037
39184
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
39038
39185
  }
39186
+ let asking = null;
39187
+ let editorAskedAt = 0;
39188
+ let editorSaysGoing = true;
39039
39189
  const endpoint = await announcedSince(projectPath, before, {
39040
39190
  budgetMs,
39041
- giveUp: () => this.dapClient?.isStopped() === true || !stillRunning(this.currentRun())
39191
+ accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
39192
+ giveUp: () => {
39193
+ if (this.dapClient?.isStopped() === true) {
39194
+ return true;
39195
+ }
39196
+ const run = this.currentRun();
39197
+ if (run === null || !run.throughEditor) {
39198
+ return !stillRunning(run);
39199
+ }
39200
+ if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
39201
+ editorAskedAt = Date.now();
39202
+ asking = this.runStillGoing(run).then((going) => {
39203
+ editorSaysGoing = going;
39204
+ }).finally(() => {
39205
+ asking = null;
39206
+ });
39207
+ }
39208
+ return !editorSaysGoing;
39209
+ }
39042
39210
  });
39043
39211
  const going = this.currentRun();
39044
39212
  if (endpoint !== null && going?.throughEditor === true && going.announcedPid === undefined) {
@@ -39048,7 +39216,7 @@ class GodotServer {
39048
39216
  addon: true,
39049
39217
  budgetMs,
39050
39218
  heldAt: this.dapClient?.whereItStopped() ?? null,
39051
- running: await this.gameIsUp(),
39219
+ running: endpoint !== null || going !== null && await this.runStillGoing(going),
39052
39220
  withArgs
39053
39221
  });
39054
39222
  }
@@ -39089,7 +39257,8 @@ class GodotServer {
39089
39257
  startedAt,
39090
39258
  exitCode: null,
39091
39259
  throughEditor: true,
39092
- brokeOn: null
39260
+ brokeOn: null,
39261
+ announcedBefore: alreadyPlaying
39093
39262
  };
39094
39263
  this.activeProcess = played;
39095
39264
  writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
@@ -39100,14 +39269,14 @@ class GodotServer {
39100
39269
  scene: scene === null ? "the main scene" : `res://${scene}`,
39101
39270
  refreshedClasses,
39102
39271
  debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
39103
- endedPreviousRun: ended ?? undefined,
39272
+ endedPreviousRun: endedPreviousRun(ended),
39104
39273
  breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
39105
39274
  breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
39106
39275
  scriptPath: projectSpelling(projectPath, one.scriptPath),
39107
39276
  reason: one.reason
39108
39277
  })),
39109
39278
  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.`)
39279
+ 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
39280
  });
39112
39281
  }
39113
39282
  drainEditorOutput(game) {
@@ -39323,15 +39492,40 @@ class GodotServer {
39323
39492
  brokeOn: null,
39324
39493
  pickedUpPlaying: true
39325
39494
  };
39326
- const announced = this.theOneAnnouncedForOurProject();
39327
- if (announced !== undefined) {
39328
- picked.announcedPid = announced.pid;
39329
- }
39330
39495
  this.activeProcess = picked;
39331
39496
  }
39332
- theOneAnnouncedForOurProject() {
39333
- const ofProject = this.allAnnouncedForOurProject(runtimesAnnounced().running);
39334
- return ofProject.length === 1 ? ofProject[0] : undefined;
39497
+ announcedPidOf(run, announced) {
39498
+ if (!run.throughEditor || run.announcedPid !== undefined) {
39499
+ return run.announcedPid;
39500
+ }
39501
+ const before = run.announcedBefore ?? new Set;
39502
+ const fresh = this.allAnnouncedForOurProject(announced ?? runtimesAnnounced().running).filter((one) => !before.has(one.pid));
39503
+ const candidates = fresh.filter((one) => this.playedByOurEditor(one, run.projectPath));
39504
+ const theOne = candidates.length === 1 ? candidates[0] : undefined;
39505
+ if (theOne !== undefined) {
39506
+ run.announcedPid = theOne.pid;
39507
+ }
39508
+ return run.announcedPid;
39509
+ }
39510
+ playedByOurEditor(endpoint, projectPath) {
39511
+ const editor = this.godotBridge.getStatus().editorPid;
39512
+ if (editor === undefined) {
39513
+ return true;
39514
+ }
39515
+ if (endpoint.editorPid !== undefined) {
39516
+ return endpoint.editorPid === editor;
39517
+ }
39518
+ return !runtimeAddonAnnouncesEditors(projectPath ?? this.ownProject);
39519
+ }
39520
+ isTheGameOf(run, endpoint, projectPath) {
39521
+ if (run === null) {
39522
+ return false;
39523
+ }
39524
+ if (run.throughEditor) {
39525
+ return this.playedByOurEditor(endpoint, projectPath);
39526
+ }
39527
+ const editor = this.godotBridge.getStatus().editorPid;
39528
+ return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
39335
39529
  }
39336
39530
  adoptRecordedRun() {
39337
39531
  const record = readRunRecord();
@@ -39461,12 +39655,25 @@ class GodotServer {
39461
39655
  }
39462
39656
  const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
39463
39657
  const until = Date.now() + budgetMs;
39464
- while (stillRunning(run) && Date.now() < until) {
39658
+ while (await this.runStillGoing(run) && Date.now() < until) {
39465
39659
  this.drainEditorOutput(run);
39466
39660
  this.drainTranscript(run);
39467
39661
  await delay3(RUN_POLL_MS);
39468
39662
  }
39469
- return await this.handleGetDebugOutput(args, stillRunning(run) ? budgetMs : undefined);
39663
+ return await this.handleGetDebugOutput(args, await this.runStillGoing(run) ? budgetMs : undefined);
39664
+ }
39665
+ async runStillGoing(run, editorSays) {
39666
+ if (!run.throughEditor) {
39667
+ return stillRunning(run);
39668
+ }
39669
+ const announced = this.announcedPidOf(run);
39670
+ if (announced !== undefined && !alive(announced)) {
39671
+ return false;
39672
+ }
39673
+ if (announced !== undefined && editorSays === undefined) {
39674
+ return stillRunning(run);
39675
+ }
39676
+ return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
39470
39677
  }
39471
39678
  async handleGetDebugOutput(args, waitedMs) {
39472
39679
  await this.pickUpWhatTheEditorIsPlaying();
@@ -39476,7 +39683,8 @@ class GodotServer {
39476
39683
  }
39477
39684
  this.drainEditorOutput(run);
39478
39685
  this.drainTranscript(run);
39479
- if (run.endedUnwatched !== true && !stillRunning(run)) {
39686
+ const going = await this.runStillGoing(run);
39687
+ if (run.endedUnwatched !== true && !going) {
39480
39688
  run.log.finish();
39481
39689
  run.endedUnwatched = run.exitCode === null;
39482
39690
  }
@@ -39487,12 +39695,13 @@ class GodotServer {
39487
39695
  contains: readNonEmptyString(args, "contains"),
39488
39696
  limit: readPositiveNumber(args, "limit") ?? 200
39489
39697
  });
39490
- const hold = await this.holdOf(run);
39698
+ const hold = await this.holdOf(run, going);
39491
39699
  const wanted = readBoolean(args, "cpu") ?? false;
39492
- const cpuSeconds = wanted && run.pid !== null && stillRunning(run) ? await cpuSecondsOf(run.pid) : undefined;
39700
+ const processToAsk = run.pid ?? this.announcedPidOf(run) ?? null;
39701
+ const cpuSeconds = wanted && processToAsk !== null && stillRunning(run) ? await cpuSecondsOf(processToAsk) : undefined;
39493
39702
  const notes = [];
39494
39703
  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.");
39704
+ 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
39705
  }
39497
39706
  if (waitedMs !== undefined) {
39498
39707
  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 +39725,10 @@ class GodotServer {
39516
39725
  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
39726
  }
39518
39727
  return this.jsonTextResponse({
39519
- running: stillRunning(run),
39728
+ running: going,
39520
39729
  exitCode: run.exitCode,
39521
39730
  through: run.throughEditor ? "editor" : "gdharness",
39522
- pid: run.pid,
39731
+ pid: run.pid ?? this.announcedPidOf(run) ?? null,
39523
39732
  errors: run.log.count("error"),
39524
39733
  warnings: run.log.count("warning"),
39525
39734
  clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
@@ -39545,18 +39754,19 @@ class GodotServer {
39545
39754
  }
39546
39755
  this.drainEditorOutput(stopped);
39547
39756
  this.drainTranscript(stopped);
39757
+ const wasRunning = await this.runStillGoing(stopped);
39548
39758
  this.logDebug("Stopping the running game");
39549
39759
  await this.endActiveGame("editor_run stop");
39550
39760
  return this.jsonTextResponse({
39551
39761
  stopped: true,
39552
39762
  through: stopped.throughEditor ? "editor" : "gdharness",
39553
- endedPid: stopped.pid,
39554
- exitedBeforeStop: stopped.exitCode !== null,
39763
+ endedPid: stopped.pid ?? this.announcedPidOf(stopped) ?? null,
39764
+ exitedBeforeStop: stopped.exitCode !== null || !wasRunning,
39555
39765
  exitCode: stopped.exitCode,
39556
39766
  errors: stopped.log.count("error"),
39557
39767
  warnings: stopped.log.count("warning"),
39558
39768
  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.",
39769
+ 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
39770
  entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
39561
39771
  });
39562
39772
  }
@@ -39707,27 +39917,27 @@ class GodotServer {
39707
39917
  const override = Number.parseInt(envValue("GDHARNESS_RUNTIME_TIMEOUT_MS") ?? "", 10);
39708
39918
  return Number.isInteger(override) && override > 0 ? override : 1e4;
39709
39919
  }
39710
- knownHoldOn(endpoint) {
39920
+ knownHoldOn(endpoint, announced) {
39711
39921
  const run = this.activeProcess;
39712
39922
  const session = this.dapClient;
39713
39923
  if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
39714
39924
  return null;
39715
39925
  }
39716
- const its = run.announcedPid ?? this.theOneAnnouncedForOurProject()?.pid;
39717
- if (its !== endpoint.pid || !session.holdIsKnown()) {
39926
+ if (this.announcedPidOf(run, announced) !== endpoint.pid || !session.holdIsKnown()) {
39718
39927
  return null;
39719
39928
  }
39720
39929
  return session.whereItStopped();
39721
39930
  }
39722
- async holdOf(run) {
39723
- if (!run.throughEditor || !stillRunning(run)) {
39931
+ async holdOf(run, going) {
39932
+ if (!run.throughEditor || !(going ?? await this.runStillGoing(run))) {
39724
39933
  return { heldAt: null };
39725
39934
  }
39726
39935
  const session = this.dapClient;
39727
39936
  if (session?.holdIsKnown()) {
39728
39937
  return { heldAt: session.whereItStopped() };
39729
39938
  }
39730
- const endpoint = run.announcedPid !== undefined ? runtimesAnnounced().running.find((one) => one.pid === run.announcedPid) : this.theOneAnnouncedForOurProject();
39939
+ const its = this.announcedPidOf(run);
39940
+ const endpoint = its === undefined ? undefined : runtimesAnnounced().running.find((one) => one.pid === its);
39731
39941
  if (endpoint === undefined) {
39732
39942
  return {
39733
39943
  heldAt: undefined,
@@ -39752,13 +39962,13 @@ class GodotServer {
39752
39962
  return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
39753
39963
  }
39754
39964
  async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
39755
- const { op: _op, projectPath, ...params } = asParams(args);
39965
+ const { op: _op, projectPath, pid, ...params } = asParams(args);
39756
39966
  const announced = runtimesAnnounced();
39757
- const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken);
39967
+ const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken, typeof pid === "number" ? pid : undefined);
39758
39968
  if ("problem" in choice) {
39759
39969
  return this.createErrorResponse(choice.problem);
39760
39970
  }
39761
- const held = this.knownHoldOn(choice.endpoint);
39971
+ const held = this.knownHoldOn(choice.endpoint, announced.running);
39762
39972
  if (held !== null) {
39763
39973
  return this.createErrorResponse(`The game (pid ${choice.endpoint.pid}) is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is.`, [
39764
39974
  "debug_control continue lets it go, after which this call answers",
@@ -39820,7 +40030,7 @@ class GodotServer {
39820
40030
  const includeHidden = readBoolean(args, "includeHidden");
39821
40031
  return await this.handleRuntimeCommand("find_nodes", {
39822
40032
  ...filters,
39823
- projectPath: args["projectPath"],
40033
+ ...whichGame(args),
39824
40034
  root: readNonEmptyString(args, "nodePath") ?? "/root",
39825
40035
  limit: readPositiveNumber(args, "limit") ?? 100,
39826
40036
  ...property === undefined ? {} : { property },
@@ -39831,18 +40041,18 @@ class GodotServer {
39831
40041
  if (op === "frames") {
39832
40042
  const frames = readPositiveNumber(args, "frames") ?? 1;
39833
40043
  const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
39834
- return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
40044
+ return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
39835
40045
  }
39836
40046
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
39837
40047
  const nodePath = readNonEmptyString(args, "nodePath") ?? "";
39838
40048
  const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
39839
40049
  return op === "signal" ? await this.handleRuntimeCommand("wait_signal", {
39840
- projectPath: args["projectPath"],
40050
+ ...whichGame(args),
39841
40051
  path: nodePath,
39842
40052
  signal: readString(args, "signal") ?? "",
39843
40053
  timeout_ms: timeoutMs
39844
40054
  }, patience) : await this.handleRuntimeCommand("wait_until", {
39845
- projectPath: args["projectPath"],
40055
+ ...whichGame(args),
39846
40056
  path: nodePath,
39847
40057
  property: readString(args, "property") ?? "",
39848
40058
  value: args["value"],
@@ -41397,7 +41607,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
41397
41607
  ## Start here
41398
41608
 
41399
41609
  - ${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
41610
+ games instead, by \`projectPath\`, or by \`pid\` when several run from one project, as a bench
41611
+ and its workers do; the others answer about the editor this server is connected to. There is no
41401
41612
  ambient project, and an argument a tool does not declare is refused rather than ignored.
41402
41613
  - \`editor_status\` says whether an editor is connected and whether its addon matches the server.
41403
41614
  \`addonIsStale\` means they differ, and \`staleNote\` says which half is behind: an editor that
@@ -41469,6 +41680,12 @@ The autoload reaches an export. \`gdharness runtime off\` in the project before
41469
41680
  move. There is no pause and no step out: Godot's debug adapter implements neither, so use a
41470
41681
  breakpoint on the line you want instead.
41471
41682
 
41683
+ A held game draws nothing and answers no \`runtime_*\` call. The server says so at once when it
41684
+ knows, naming why the game is held and \`debug_control continue\` as what lets it go, and
41685
+ \`editor_output\` and \`editor_status\` carry the same under \`heldAt\`. A breakpoint set with
41686
+ \`debug_breakpoint\` holds for every start until \`debug_breakpoint remove\`, across a reconnect,
41687
+ so take it off when the question it was for is answered.
41688
+
41472
41689
  ## Editing
41473
41690
 
41474
41691
  \`scene_tree\` for what a scene holds, rather than what the \`.tscn\` text implies. \`scene_node\`,