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/index.js CHANGED
@@ -25248,6 +25248,8 @@ class GodotDAPClient {
25248
25248
  halt = null;
25249
25249
  holdKnown = false;
25250
25250
  breakpoints = new Map;
25251
+ inEditor = new Map;
25252
+ onBreakpointsChanged = null;
25251
25253
  constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
25252
25254
  this.port = port;
25253
25255
  this.host = host;
@@ -25337,6 +25339,7 @@ class GodotDAPClient {
25337
25339
  this.attached = false;
25338
25340
  this.halt = null;
25339
25341
  this.holdKnown = false;
25342
+ this.inEditor.clear();
25340
25343
  }
25341
25344
  async ensureConnected() {
25342
25345
  if (!this.connected) {
@@ -25433,6 +25436,31 @@ class GodotDAPClient {
25433
25436
  this.holdKnown = true;
25434
25437
  return;
25435
25438
  }
25439
+ if (eventName === "breakpoint") {
25440
+ const body = event.body;
25441
+ const breakpoint = body?.["breakpoint"];
25442
+ const source = typeof breakpoint === "object" && breakpoint !== null ? breakpoint["source"] : undefined;
25443
+ const path = typeof source === "object" && source !== null ? source["path"] : undefined;
25444
+ const line = typeof breakpoint === "object" && breakpoint !== null ? breakpoint["line"] : undefined;
25445
+ if (typeof path !== "string" || typeof line !== "number") {
25446
+ return;
25447
+ }
25448
+ if (said(body, "reason") === "removed") {
25449
+ this.inEditor.get(path)?.delete(line);
25450
+ const held = this.breakpoints.get(path);
25451
+ if (held?.delete(line) === true) {
25452
+ if (held.size === 0) {
25453
+ this.breakpoints.delete(path);
25454
+ }
25455
+ this.onBreakpointsChanged?.(this.breakpointsHeld());
25456
+ }
25457
+ return;
25458
+ }
25459
+ const known = this.inEditor.get(path) ?? new Set;
25460
+ known.add(line);
25461
+ this.inEditor.set(path, known);
25462
+ return;
25463
+ }
25436
25464
  if (eventName === "terminated" || eventName === "exited") {
25437
25465
  this.attached = false;
25438
25466
  this.halt = null;
@@ -25525,23 +25553,46 @@ class GodotDAPClient {
25525
25553
  async removeBreakpoint(filePath, line) {
25526
25554
  const wanted = new Set(this.breakpoints.get(filePath) ?? []);
25527
25555
  wanted.delete(line);
25528
- return await this.sendBreakpoints(filePath, wanted);
25556
+ return await this.sendBreakpoints(filePath, wanted, line);
25529
25557
  }
25530
- async sendBreakpoints(filePath, lines) {
25558
+ async sendBreakpoints(filePath, mine, clearing) {
25531
25559
  await this.ensureConnected();
25532
25560
  await this.initialize();
25561
+ const lines = new Set([...mine, ...this.inEditor.get(filePath) ?? []]);
25562
+ if (clearing !== undefined) {
25563
+ lines.delete(clearing);
25564
+ }
25533
25565
  const sorted = Array.from(lines).sort((a, b) => a - b);
25534
25566
  const answer = await this.sendRequest("setBreakpoints", {
25535
25567
  source: { path: filePath },
25536
25568
  breakpoints: sorted.map((breakpointLine) => ({ line: breakpointLine }))
25537
25569
  });
25538
- if (lines.size === 0) {
25570
+ if (mine.size === 0) {
25539
25571
  this.breakpoints.delete(filePath);
25540
25572
  } else {
25541
- this.breakpoints.set(filePath, new Set(lines));
25573
+ this.breakpoints.set(filePath, new Set(mine));
25574
+ }
25575
+ if (lines.size === 0) {
25576
+ this.inEditor.delete(filePath);
25577
+ } else {
25578
+ this.inEditor.set(filePath, lines);
25542
25579
  }
25543
25580
  return answer;
25544
25581
  }
25582
+ setBreakpointsSink(sink) {
25583
+ this.onBreakpointsChanged = sink;
25584
+ }
25585
+ breakpointsInEditor() {
25586
+ const theirs = [];
25587
+ for (const [scriptPath, lines] of this.inEditor) {
25588
+ const mine = this.breakpoints.get(scriptPath);
25589
+ const rest = Array.from(lines).filter((line) => !mine?.has(line)).sort((a, b) => a - b);
25590
+ if (rest.length > 0) {
25591
+ theirs.push({ scriptPath, lines: rest });
25592
+ }
25593
+ }
25594
+ return theirs;
25595
+ }
25545
25596
  async reapplyBreakpoints() {
25546
25597
  const applied = [];
25547
25598
  const refused = [];
@@ -25557,6 +25608,9 @@ class GodotDAPClient {
25557
25608
  });
25558
25609
  }
25559
25610
  }
25611
+ if (refused.length > 0) {
25612
+ this.onBreakpointsChanged?.(this.breakpointsHeld());
25613
+ }
25560
25614
  return { applied, refused };
25561
25615
  }
25562
25616
  breakpointsHeld() {
@@ -25759,14 +25813,10 @@ async function handleDAPTool(client, toolName, args) {
25759
25813
  }
25760
25814
  } catch (error) {
25761
25815
  const message = error instanceof Error ? error.message : String(error);
25816
+ 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.";
25762
25817
  return {
25763
25818
  isError: true,
25764
- content: [
25765
- {
25766
- type: "text",
25767
- text: `DAP tool '${toolName}' failed: ${message}. ` + "The debug tools answer for a game the editor is playing: start one with editor_run."
25768
- }
25769
- ]
25819
+ content: [{ type: "text", text: `DAP tool '${toolName}' failed: ${message}. ${cure}` }]
25770
25820
  };
25771
25821
  }
25772
25822
  }
@@ -26071,6 +26121,7 @@ class GodotBridge extends EventEmitter {
26071
26121
  dapPort: this.connectionInfo?.dapPort,
26072
26122
  debugPort: this.connectionInfo?.debugPort,
26073
26123
  openedByAServer: this.connectionInfo?.openedByAServer,
26124
+ syncsBreakpoints: this.connectionInfo?.syncsBreakpoints,
26074
26125
  pendingRequests: this.pendingRequests.size,
26075
26126
  queuedResources: this.resourceQueues.size,
26076
26127
  listeningSince: this.listeningSince ?? undefined
@@ -26213,6 +26264,7 @@ class GodotBridge extends EventEmitter {
26213
26264
  this.connectionInfo.dapPort = servedPort(message.dap_port);
26214
26265
  this.connectionInfo.debugPort = servedPort(message.debug_port);
26215
26266
  this.connectionInfo.openedByAServer = message.opened_by_a_server === true;
26267
+ this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
26216
26268
  this.log("info", `Godot ready: ${message.project_path}`);
26217
26269
  this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
26218
26270
  }
@@ -27661,7 +27713,7 @@ async function handleLSPTool(client, toolName, args) {
27661
27713
  import { execFile as execFile3 } from "node:child_process";
27662
27714
  import { promisify as promisify3 } from "node:util";
27663
27715
  var run3 = promisify3(execFile3);
27664
- var ASK_TIMEOUT_MS = 2000;
27716
+ var ASK_TIMEOUT_MS = 6000;
27665
27717
  async function cpuSecondsOf(pid) {
27666
27718
  try {
27667
27719
  return process.platform === "win32" ? await askWindows(pid) : await askPosix(pid);
@@ -28218,7 +28270,18 @@ function parseAnnouncement(file, pid) {
28218
28270
  if (protocol !== RUNTIME_PROTOCOL) {
28219
28271
  return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
28220
28272
  }
28221
- return { kind: "runtime", endpoint: { pid, port, address, project: named, file } };
28273
+ const editorPid = readNumber(fields, "editor_pid");
28274
+ return {
28275
+ kind: "runtime",
28276
+ endpoint: {
28277
+ pid,
28278
+ port,
28279
+ address,
28280
+ project: named,
28281
+ file,
28282
+ ...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
28283
+ }
28284
+ };
28222
28285
  }
28223
28286
  function runtimesAnnounced(directories = runtimeDirectories()) {
28224
28287
  const running = new Map;
@@ -28297,7 +28360,7 @@ async function announcedSince(projectPath, before, waiting = {}) {
28297
28360
  const directories = waiting.directories ?? runtimeDirectories();
28298
28361
  const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
28299
28362
  for (;; ) {
28300
- const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted);
28363
+ const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted && (waiting.accept?.(endpoint) ?? true));
28301
28364
  if (fresh !== undefined) {
28302
28365
  return fresh;
28303
28366
  }
@@ -28316,7 +28379,25 @@ function tooNew(unspoken) {
28316
28379
  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";
28317
28380
  return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
28318
28381
  }
28319
- function chooseRuntime(endpoints, projectPath, unspoken = []) {
28382
+ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
28383
+ if (pid !== undefined) {
28384
+ const named = endpoints.find((endpoint) => endpoint.pid === pid);
28385
+ if (named !== undefined) {
28386
+ if (projectPath !== undefined && resolve4(named.project.path) !== resolve4(projectPath)) {
28387
+ return {
28388
+ problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve4(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
28389
+ };
28390
+ }
28391
+ return { endpoint: named };
28392
+ }
28393
+ const tooNewToo = unspoken.find((game) => game.pid === pid);
28394
+ if (tooNewToo !== undefined) {
28395
+ return { problem: tooNew([tooNewToo]) };
28396
+ }
28397
+ return {
28398
+ 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.`
28399
+ };
28400
+ }
28320
28401
  if (endpoints.length === 0) {
28321
28402
  if (unspoken.length > 0) {
28322
28403
  return { problem: tooNew(unspoken) };
@@ -28347,14 +28428,14 @@ function chooseRuntime(endpoints, projectPath, unspoken = []) {
28347
28428
  return { endpoint: matching[0] };
28348
28429
  }
28349
28430
  return {
28350
- problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Stop all but one.`
28431
+ problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
28351
28432
  };
28352
28433
  }
28353
28434
  if (endpoints.length === 1 && endpoints[0]) {
28354
28435
  return { endpoint: endpoints[0] };
28355
28436
  }
28356
28437
  return {
28357
- problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one.`
28438
+ problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
28358
28439
  };
28359
28440
  }
28360
28441
  var nextRequestId = 1;
@@ -28765,6 +28846,10 @@ var RUNNING_PROJECT_PATH = {
28765
28846
  type: "string",
28766
28847
  description: "Which game, when more than one is running: the project directory it was started from. Not needed with one game."
28767
28848
  };
28849
+ var RUNNING_PID = {
28850
+ type: "number",
28851
+ 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."
28852
+ };
28768
28853
  var SCENE_PATH = {
28769
28854
  type: "string",
28770
28855
  description: 'Scene file inside the project, such as "scenes/main.tscn" or "res://scenes/main.tscn".'
@@ -29431,7 +29516,7 @@ var TOOL_SPECS = [
29431
29516
  },
29432
29517
  {
29433
29518
  name: "editor_status",
29434
- 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.",
29519
+ 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.",
29435
29520
  parameters: {},
29436
29521
  requires: []
29437
29522
  },
@@ -29488,6 +29573,7 @@ var TOOL_SPECS = [
29488
29573
  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.",
29489
29574
  parameters: {
29490
29575
  projectPath: RUNNING_PROJECT_PATH,
29576
+ pid: RUNNING_PID,
29491
29577
  nodePath: {
29492
29578
  type: "string",
29493
29579
  ops: ["tree", "text", "find", "rect", "property"],
@@ -29566,6 +29652,7 @@ var TOOL_SPECS = [
29566
29652
  description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon.",
29567
29653
  parameters: {
29568
29654
  projectPath: RUNNING_PROJECT_PATH,
29655
+ pid: RUNNING_PID,
29569
29656
  nodePath: { type: "string", description: 'Absolute node path, such as "/root/Main/Player".' },
29570
29657
  property: {
29571
29658
  type: "string",
@@ -29605,6 +29692,7 @@ var TOOL_SPECS = [
29605
29692
  description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window.",
29606
29693
  parameters: {
29607
29694
  projectPath: RUNNING_PROJECT_PATH,
29695
+ pid: RUNNING_PID,
29608
29696
  viewportPath: {
29609
29697
  type: "string",
29610
29698
  ops: ["viewport"],
@@ -29625,6 +29713,7 @@ var TOOL_SPECS = [
29625
29713
  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.",
29626
29714
  parameters: {
29627
29715
  projectPath: RUNNING_PROJECT_PATH,
29716
+ pid: RUNNING_PID,
29628
29717
  nodePath: {
29629
29718
  type: "string",
29630
29719
  ops: ["click", "choose"],
@@ -29721,6 +29810,7 @@ var TOOL_SPECS = [
29721
29810
  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.",
29722
29811
  parameters: {
29723
29812
  projectPath: RUNNING_PROJECT_PATH,
29813
+ pid: RUNNING_PID,
29724
29814
  frames: {
29725
29815
  type: "number",
29726
29816
  ops: ["frames"],
@@ -29756,7 +29846,7 @@ var TOOL_SPECS = [
29756
29846
  },
29757
29847
  {
29758
29848
  name: "debug_breakpoint",
29759
- 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.",
29849
+ 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.",
29760
29850
  parameters: {
29761
29851
  projectPath: PROJECT_PATH,
29762
29852
  scriptPath: SCRIPT_PATH,
@@ -29858,6 +29948,9 @@ function patienceForFrames(frames, atLeast) {
29858
29948
  function projectSpelling(projectPath, absolutePath) {
29859
29949
  return `res://${relative3(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
29860
29950
  }
29951
+ function whichGame(args) {
29952
+ return { projectPath: args["projectPath"], pid: args["pid"] };
29953
+ }
29861
29954
  function describeHalt(halt) {
29862
29955
  switch (halt.reason) {
29863
29956
  case "breakpoint":
@@ -29893,6 +29986,19 @@ function runIsUp(run, editorSays) {
29893
29986
  }
29894
29987
  return editorSays ?? stillRunning(run);
29895
29988
  }
29989
+ function endedPreviousRun(ended) {
29990
+ if (ended === null) {
29991
+ return;
29992
+ }
29993
+ return ended.pid ?? true;
29994
+ }
29995
+ function endedToStartThis(ended) {
29996
+ if (ended === null) {
29997
+ return "";
29998
+ }
29999
+ const which = ended.pid === null ? "The game the editor was playing" : `The run that was going, pid ${ended.pid},`;
30000
+ return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
30001
+ }
29896
30002
  function runtimeVerdict(endpoint, after) {
29897
30003
  if (endpoint !== null) {
29898
30004
  return { listening: true, pid: endpoint.pid, port: endpoint.port };
@@ -30079,6 +30185,16 @@ function hasMainScene(projectFile) {
30079
30185
  const scene = parseProjectGodot(readFileSync13(projectFile, "utf8"))["application"]?.["run/main_scene"];
30080
30186
  return typeof scene === "string" && scene !== "";
30081
30187
  }
30188
+ function runtimeAddonAnnouncesEditors(projectPath) {
30189
+ if (projectPath === null) {
30190
+ return false;
30191
+ }
30192
+ try {
30193
+ return readFileSync13(join13(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
30194
+ } catch {
30195
+ return false;
30196
+ }
30197
+ }
30082
30198
  function editorPlaysHeadless(projectFile) {
30083
30199
  const runArgs = parseProjectGodot(readFileSync13(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
30084
30200
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
@@ -30633,7 +30749,7 @@ class GodotServer {
30633
30749
  switch (op) {
30634
30750
  case "tree":
30635
30751
  return await this.handleRuntimeCommand("get_tree", {
30636
- projectPath: args["projectPath"],
30752
+ ...whichGame(args),
30637
30753
  root: readNonEmptyString(args, "nodePath") ?? "/root",
30638
30754
  depth: readPositiveNumber(args, "depth") ?? 3,
30639
30755
  include_properties: readBoolean(args, "includeProperties") ?? false
@@ -30642,36 +30758,36 @@ class GodotServer {
30642
30758
  return await this.handleFindRuntimeNodes(args);
30643
30759
  case "text":
30644
30760
  return await this.handleRuntimeCommand("read_text", {
30645
- projectPath: args["projectPath"],
30761
+ ...whichGame(args),
30646
30762
  root: readNonEmptyString(args, "nodePath") ?? "/root",
30647
30763
  include_hidden: readBoolean(args, "includeHidden") ?? false,
30648
30764
  limit: readPositiveNumber(args, "limit") ?? 500
30649
30765
  });
30650
30766
  case "rect":
30651
30767
  return await this.handleRuntimeCommand("get_rect", {
30652
- projectPath: args["projectPath"],
30768
+ ...whichGame(args),
30653
30769
  path: readNonEmptyString(args, "nodePath") ?? ""
30654
30770
  });
30655
30771
  case "property":
30656
30772
  return await this.handleRuntimeCommand("get_property", {
30657
- projectPath: args["projectPath"],
30773
+ ...whichGame(args),
30658
30774
  path: readNonEmptyString(args, "nodePath") ?? "",
30659
30775
  property: readNonEmptyString(args, "property") ?? ""
30660
30776
  });
30661
30777
  default:
30662
30778
  return await this.handleRuntimeCommand("get_metrics", {
30663
- projectPath: args["projectPath"],
30779
+ ...whichGame(args),
30664
30780
  metrics: readArray(args, "metrics") ?? []
30665
30781
  });
30666
30782
  }
30667
30783
  case "runtime_invoke":
30668
30784
  return op === "set" ? await this.handleRuntimeCommand("set_property", {
30669
- projectPath: args["projectPath"],
30785
+ ...whichGame(args),
30670
30786
  path: readNonEmptyString(args, "nodePath") ?? "",
30671
30787
  property: readString(args, "property") ?? "",
30672
30788
  value: args["value"]
30673
30789
  }) : await this.handleRuntimeCommand("call_method", {
30674
- projectPath: args["projectPath"],
30790
+ ...whichGame(args),
30675
30791
  path: readNonEmptyString(args, "nodePath") ?? "",
30676
30792
  method: readString(args, "method") ?? "",
30677
30793
  args: readArray(args, "args") ?? []
@@ -30681,7 +30797,7 @@ class GodotServer {
30681
30797
  case "runtime_input":
30682
30798
  if (op === "click") {
30683
30799
  return await this.handleRuntimeCommand("click", {
30684
- projectPath: args["projectPath"],
30800
+ ...whichGame(args),
30685
30801
  path: readNonEmptyString(args, "nodePath") ?? "",
30686
30802
  button: readString(args, "button") ?? "left",
30687
30803
  double: readBoolean(args, "doubleClick") ?? false
@@ -30689,7 +30805,7 @@ class GodotServer {
30689
30805
  }
30690
30806
  if (op === "choose") {
30691
30807
  return await this.handleRuntimeCommand("choose", {
30692
- projectPath: args["projectPath"],
30808
+ ...whichGame(args),
30693
30809
  path: readNonEmptyString(args, "nodePath") ?? "",
30694
30810
  text: readString(args, "text") ?? "",
30695
30811
  ...args["index"] === undefined ? {} : { index: args["index"] }
@@ -30720,7 +30836,9 @@ class GodotServer {
30720
30836
  return this.jsonTextResponse({
30721
30837
  ...asParams(JSON.parse(answered.content[0]?.text ?? "{}")),
30722
30838
  held: this.breakpointsAsProjectSpells(project.value.path, held),
30723
- 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."
30839
+ setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor()),
30840
+ 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.",
30841
+ ...this.breakpointsAtRisk()
30724
30842
  });
30725
30843
  }
30726
30844
  case "debug_control":
@@ -30794,6 +30912,16 @@ class GodotServer {
30794
30912
  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"])
30795
30913
  };
30796
30914
  }
30915
+ const going = await this.runStillGoing(game);
30916
+ if (!going) {
30917
+ return {
30918
+ ok: false,
30919
+ response: this.createErrorResponse("The last run has already ended, so there is no debug session to answer for.", [
30920
+ "editor_output still reads what it printed, until the next run starts",
30921
+ "editor_run start plays another, which the debugger can hold"
30922
+ ])
30923
+ };
30924
+ }
30797
30925
  if (!game.throughEditor) {
30798
30926
  return {
30799
30927
  ok: false,
@@ -30815,7 +30943,7 @@ class GodotServer {
30815
30943
  };
30816
30944
  }
30817
30945
  if (!this.dap().isStopped()) {
30818
- const hold = await this.holdOf(game);
30946
+ const hold = await this.holdOf(game, going);
30819
30947
  if (hold.heldAt !== null) {
30820
30948
  if (intent === "move") {
30821
30949
  return { ok: true, value: game };
@@ -31381,6 +31509,12 @@ class GodotServer {
31381
31509
  this.dapClient.setOutputSink((line) => {
31382
31510
  this.writeToTranscript(line);
31383
31511
  });
31512
+ this.dapClient.setBreakpointsSink((held) => {
31513
+ const project = this.godotBridge.getStatus().projectPath ?? this.ownProject;
31514
+ if (project !== null) {
31515
+ writeBreakpointNote(project, held);
31516
+ }
31517
+ });
31384
31518
  }
31385
31519
  return this.dapClient;
31386
31520
  }
@@ -31392,6 +31526,15 @@ class GodotServer {
31392
31526
  }
31393
31527
  return client;
31394
31528
  }
31529
+ breakpointsAtRisk() {
31530
+ const status = this.godotBridge.getStatus();
31531
+ if (!status.connected || status.syncsBreakpoints === true) {
31532
+ return {};
31533
+ }
31534
+ return {
31535
+ 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."
31536
+ };
31537
+ }
31395
31538
  breakpointsAsProjectSpells(projectPath, held) {
31396
31539
  return held.map((one) => ({
31397
31540
  scriptPath: projectSpelling(projectPath, one.scriptPath),
@@ -31454,6 +31597,7 @@ class GodotServer {
31454
31597
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
31455
31598
  startupError: this.bridgeStartupError,
31456
31599
  staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
31600
+ ...this.breakpointsAtRisk(),
31457
31601
  retryingBridge: this.bridgeRetry === null ? undefined : true,
31458
31602
  announcedAt: this.announcedAt ?? undefined,
31459
31603
  projectIs: this.projectHasMovedOn() ?? undefined,
@@ -31488,7 +31632,7 @@ class GodotServer {
31488
31632
  const located = await this.locator.find();
31489
31633
  const announced = runtimesAnnounced();
31490
31634
  const pinged = Promise.all(announced.running.map(async (endpoint) => {
31491
- const held = this.knownHoldOn(endpoint);
31635
+ const held = this.knownHoldOn(endpoint, announced.running);
31492
31636
  const reply = held === null ? await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS) : {
31493
31637
  ok: false,
31494
31638
  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.`
@@ -31497,6 +31641,7 @@ class GodotServer {
31497
31641
  pid: endpoint.pid,
31498
31642
  port: endpoint.port,
31499
31643
  project: endpoint.project,
31644
+ editorPid: endpoint.editorPid,
31500
31645
  reachable: reply.ok,
31501
31646
  problem: reply.ok ? null : reply.message
31502
31647
  };
@@ -31505,9 +31650,14 @@ class GodotServer {
31505
31650
  const playing = await this.editorPlayingState();
31506
31651
  await this.pickUpWhatTheEditorIsPlaying();
31507
31652
  const current = this.currentRun();
31508
- return { playing, hold: current === null ? {} : await this.holdOf(current) };
31653
+ const active = current === null ? false : await this.runStillGoing(current, playing?.playing ?? null);
31654
+ return {
31655
+ playing,
31656
+ active,
31657
+ hold: current === null ? {} : await this.holdOf(current, active)
31658
+ };
31509
31659
  })();
31510
- const [games, { playing, hold }] = await Promise.all([pinged, asked]);
31660
+ const [games, { playing, active, hold }] = await Promise.all([pinged, asked]);
31511
31661
  return this.jsonTextResponse({
31512
31662
  editor: {
31513
31663
  ...this.getEditorStatusPayload(),
@@ -31519,7 +31669,7 @@ class GodotServer {
31519
31669
  problem: located.ok ? undefined : GodotLocator.refusal(located).message
31520
31670
  },
31521
31671
  game: {
31522
- processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
31672
+ processActive: active,
31523
31673
  playingInEditor: playing,
31524
31674
  ...hold,
31525
31675
  runtimeConnected: games.some((game) => game.reachable),
@@ -31732,10 +31882,12 @@ class GodotServer {
31732
31882
  if (op === "check") {
31733
31883
  return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
31734
31884
  }
31885
+ await this.pickUpWhatTheEditorIsPlaying();
31735
31886
  let ended = null;
31736
- if (stillRunning(this.currentRun())) {
31887
+ const before = this.currentRun();
31888
+ if (before !== null && await this.runStillGoing(before)) {
31737
31889
  this.logDebug("Ending the running game before starting another");
31738
- ended = this.currentRun()?.pid ?? null;
31890
+ ended = { pid: before.pid ?? this.announcedPidOf(before) ?? null };
31739
31891
  await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
31740
31892
  }
31741
31893
  const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
@@ -31759,8 +31911,8 @@ class GodotServer {
31759
31911
  const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
31760
31912
  this.activeProcess = started;
31761
31913
  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." : "";
31762
- 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.`;
31763
- const stranded = wereHereFirst.filter((one) => one.pid !== ended && alive(one.pid));
31914
+ const endedForThis = endedToStartThis(ended);
31915
+ const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
31764
31916
  const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
31765
31917
  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.`;
31766
31918
  return this.jsonTextResponse({
@@ -31771,23 +31923,39 @@ class GodotServer {
31771
31923
  arguments: cmdArgs,
31772
31924
  transcript: started.transcript,
31773
31925
  refreshedClasses: refreshed.value,
31774
- endedPreviousRun: ended ?? undefined,
31926
+ endedPreviousRun: endedPreviousRun(ended),
31775
31927
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
31776
31928
  message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
31777
31929
  });
31778
31930
  }
31779
- async gameIsUp() {
31780
- const run = this.currentRun();
31781
- const editorSays = run?.throughEditor === true ? (await this.editorPlayingState())?.playing ?? null : null;
31782
- return runIsUp(run, editorSays);
31783
- }
31784
31931
  async runtimeUp(projectPath, before, budgetMs, withArgs) {
31785
31932
  if (!existsSync9(join13(projectPath, RUNTIME_AUTOLOAD.path))) {
31786
31933
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
31787
31934
  }
31935
+ let asking = null;
31936
+ let editorAskedAt = 0;
31937
+ let editorSaysGoing = true;
31788
31938
  const endpoint = await announcedSince(projectPath, before, {
31789
31939
  budgetMs,
31790
- giveUp: () => this.dapClient?.isStopped() === true || !stillRunning(this.currentRun())
31940
+ accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
31941
+ giveUp: () => {
31942
+ if (this.dapClient?.isStopped() === true) {
31943
+ return true;
31944
+ }
31945
+ const run = this.currentRun();
31946
+ if (run === null || !run.throughEditor) {
31947
+ return !stillRunning(run);
31948
+ }
31949
+ if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
31950
+ editorAskedAt = Date.now();
31951
+ asking = this.runStillGoing(run).then((going) => {
31952
+ editorSaysGoing = going;
31953
+ }).finally(() => {
31954
+ asking = null;
31955
+ });
31956
+ }
31957
+ return !editorSaysGoing;
31958
+ }
31791
31959
  });
31792
31960
  const going = this.currentRun();
31793
31961
  if (endpoint !== null && going?.throughEditor === true && going.announcedPid === undefined) {
@@ -31797,7 +31965,7 @@ class GodotServer {
31797
31965
  addon: true,
31798
31966
  budgetMs,
31799
31967
  heldAt: this.dapClient?.whereItStopped() ?? null,
31800
- running: await this.gameIsUp(),
31968
+ running: endpoint !== null || going !== null && await this.runStillGoing(going),
31801
31969
  withArgs
31802
31970
  });
31803
31971
  }
@@ -31838,7 +32006,8 @@ class GodotServer {
31838
32006
  startedAt,
31839
32007
  exitCode: null,
31840
32008
  throughEditor: true,
31841
- brokeOn: null
32009
+ brokeOn: null,
32010
+ announcedBefore: alreadyPlaying
31842
32011
  };
31843
32012
  this.activeProcess = played;
31844
32013
  writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
@@ -31849,14 +32018,14 @@ class GodotServer {
31849
32018
  scene: scene === null ? "the main scene" : `res://${scene}`,
31850
32019
  refreshedClasses,
31851
32020
  debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
31852
- endedPreviousRun: ended ?? undefined,
32021
+ endedPreviousRun: endedPreviousRun(ended),
31853
32022
  breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
31854
32023
  breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
31855
32024
  scriptPath: projectSpelling(projectPath, one.scriptPath),
31856
32025
  reason: one.reason
31857
32026
  })),
31858
32027
  runtime: await this.runtimeUp(projectPath, alreadyPlaying, runtimeWaitMs, false),
31859
- 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.`)
32028
+ 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)
31860
32029
  });
31861
32030
  }
31862
32031
  drainEditorOutput(game) {
@@ -32072,15 +32241,40 @@ class GodotServer {
32072
32241
  brokeOn: null,
32073
32242
  pickedUpPlaying: true
32074
32243
  };
32075
- const announced = this.theOneAnnouncedForOurProject();
32076
- if (announced !== undefined) {
32077
- picked.announcedPid = announced.pid;
32078
- }
32079
32244
  this.activeProcess = picked;
32080
32245
  }
32081
- theOneAnnouncedForOurProject() {
32082
- const ofProject = this.allAnnouncedForOurProject(runtimesAnnounced().running);
32083
- return ofProject.length === 1 ? ofProject[0] : undefined;
32246
+ announcedPidOf(run, announced) {
32247
+ if (!run.throughEditor || run.announcedPid !== undefined) {
32248
+ return run.announcedPid;
32249
+ }
32250
+ const before = run.announcedBefore ?? new Set;
32251
+ const fresh = this.allAnnouncedForOurProject(announced ?? runtimesAnnounced().running).filter((one) => !before.has(one.pid));
32252
+ const candidates = fresh.filter((one) => this.playedByOurEditor(one, run.projectPath));
32253
+ const theOne = candidates.length === 1 ? candidates[0] : undefined;
32254
+ if (theOne !== undefined) {
32255
+ run.announcedPid = theOne.pid;
32256
+ }
32257
+ return run.announcedPid;
32258
+ }
32259
+ playedByOurEditor(endpoint, projectPath) {
32260
+ const editor = this.godotBridge.getStatus().editorPid;
32261
+ if (editor === undefined) {
32262
+ return true;
32263
+ }
32264
+ if (endpoint.editorPid !== undefined) {
32265
+ return endpoint.editorPid === editor;
32266
+ }
32267
+ return !runtimeAddonAnnouncesEditors(projectPath ?? this.ownProject);
32268
+ }
32269
+ isTheGameOf(run, endpoint, projectPath) {
32270
+ if (run === null) {
32271
+ return false;
32272
+ }
32273
+ if (run.throughEditor) {
32274
+ return this.playedByOurEditor(endpoint, projectPath);
32275
+ }
32276
+ const editor = this.godotBridge.getStatus().editorPid;
32277
+ return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
32084
32278
  }
32085
32279
  adoptRecordedRun() {
32086
32280
  const record = readRunRecord();
@@ -32210,12 +32404,25 @@ class GodotServer {
32210
32404
  }
32211
32405
  const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
32212
32406
  const until = Date.now() + budgetMs;
32213
- while (stillRunning(run) && Date.now() < until) {
32407
+ while (await this.runStillGoing(run) && Date.now() < until) {
32214
32408
  this.drainEditorOutput(run);
32215
32409
  this.drainTranscript(run);
32216
32410
  await delay3(RUN_POLL_MS);
32217
32411
  }
32218
- return await this.handleGetDebugOutput(args, stillRunning(run) ? budgetMs : undefined);
32412
+ return await this.handleGetDebugOutput(args, await this.runStillGoing(run) ? budgetMs : undefined);
32413
+ }
32414
+ async runStillGoing(run, editorSays) {
32415
+ if (!run.throughEditor) {
32416
+ return stillRunning(run);
32417
+ }
32418
+ const announced = this.announcedPidOf(run);
32419
+ if (announced !== undefined && !alive(announced)) {
32420
+ return false;
32421
+ }
32422
+ if (announced !== undefined && editorSays === undefined) {
32423
+ return stillRunning(run);
32424
+ }
32425
+ return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
32219
32426
  }
32220
32427
  async handleGetDebugOutput(args, waitedMs) {
32221
32428
  await this.pickUpWhatTheEditorIsPlaying();
@@ -32225,7 +32432,8 @@ class GodotServer {
32225
32432
  }
32226
32433
  this.drainEditorOutput(run);
32227
32434
  this.drainTranscript(run);
32228
- if (run.endedUnwatched !== true && !stillRunning(run)) {
32435
+ const going = await this.runStillGoing(run);
32436
+ if (run.endedUnwatched !== true && !going) {
32229
32437
  run.log.finish();
32230
32438
  run.endedUnwatched = run.exitCode === null;
32231
32439
  }
@@ -32236,12 +32444,13 @@ class GodotServer {
32236
32444
  contains: readNonEmptyString(args, "contains"),
32237
32445
  limit: readPositiveNumber(args, "limit") ?? 200
32238
32446
  });
32239
- const hold = await this.holdOf(run);
32447
+ const hold = await this.holdOf(run, going);
32240
32448
  const wanted = readBoolean(args, "cpu") ?? false;
32241
- const cpuSeconds = wanted && run.pid !== null && stillRunning(run) ? await cpuSecondsOf(run.pid) : undefined;
32449
+ const processToAsk = run.pid ?? this.announcedPidOf(run) ?? null;
32450
+ const cpuSeconds = wanted && processToAsk !== null && stillRunning(run) ? await cpuSecondsOf(processToAsk) : undefined;
32242
32451
  const notes = [];
32243
32452
  if (wanted && cpuSeconds === undefined && stillRunning(run)) {
32244
- 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.");
32453
+ 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.");
32245
32454
  }
32246
32455
  if (waitedMs !== undefined) {
32247
32456
  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.`);
@@ -32265,10 +32474,10 @@ class GodotServer {
32265
32474
  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.`);
32266
32475
  }
32267
32476
  return this.jsonTextResponse({
32268
- running: stillRunning(run),
32477
+ running: going,
32269
32478
  exitCode: run.exitCode,
32270
32479
  through: run.throughEditor ? "editor" : "gdharness",
32271
- pid: run.pid,
32480
+ pid: run.pid ?? this.announcedPidOf(run) ?? null,
32272
32481
  errors: run.log.count("error"),
32273
32482
  warnings: run.log.count("warning"),
32274
32483
  clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
@@ -32294,18 +32503,19 @@ class GodotServer {
32294
32503
  }
32295
32504
  this.drainEditorOutput(stopped);
32296
32505
  this.drainTranscript(stopped);
32506
+ const wasRunning = await this.runStillGoing(stopped);
32297
32507
  this.logDebug("Stopping the running game");
32298
32508
  await this.endActiveGame("editor_run stop");
32299
32509
  return this.jsonTextResponse({
32300
32510
  stopped: true,
32301
32511
  through: stopped.throughEditor ? "editor" : "gdharness",
32302
- endedPid: stopped.pid,
32303
- exitedBeforeStop: stopped.exitCode !== null,
32512
+ endedPid: stopped.pid ?? this.announcedPidOf(stopped) ?? null,
32513
+ exitedBeforeStop: stopped.exitCode !== null || !wasRunning,
32304
32514
  exitCode: stopped.exitCode,
32305
32515
  errors: stopped.log.count("error"),
32306
32516
  warnings: stopped.log.count("warning"),
32307
32517
  clean: stopped.log.count("error") === 0,
32308
- 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.",
32518
+ 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.",
32309
32519
  entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
32310
32520
  });
32311
32521
  }
@@ -32456,27 +32666,27 @@ class GodotServer {
32456
32666
  const override = Number.parseInt(envValue("GDHARNESS_RUNTIME_TIMEOUT_MS") ?? "", 10);
32457
32667
  return Number.isInteger(override) && override > 0 ? override : 1e4;
32458
32668
  }
32459
- knownHoldOn(endpoint) {
32669
+ knownHoldOn(endpoint, announced) {
32460
32670
  const run = this.activeProcess;
32461
32671
  const session = this.dapClient;
32462
32672
  if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
32463
32673
  return null;
32464
32674
  }
32465
- const its = run.announcedPid ?? this.theOneAnnouncedForOurProject()?.pid;
32466
- if (its !== endpoint.pid || !session.holdIsKnown()) {
32675
+ if (this.announcedPidOf(run, announced) !== endpoint.pid || !session.holdIsKnown()) {
32467
32676
  return null;
32468
32677
  }
32469
32678
  return session.whereItStopped();
32470
32679
  }
32471
- async holdOf(run) {
32472
- if (!run.throughEditor || !stillRunning(run)) {
32680
+ async holdOf(run, going) {
32681
+ if (!run.throughEditor || !(going ?? await this.runStillGoing(run))) {
32473
32682
  return { heldAt: null };
32474
32683
  }
32475
32684
  const session = this.dapClient;
32476
32685
  if (session?.holdIsKnown()) {
32477
32686
  return { heldAt: session.whereItStopped() };
32478
32687
  }
32479
- const endpoint = run.announcedPid !== undefined ? runtimesAnnounced().running.find((one) => one.pid === run.announcedPid) : this.theOneAnnouncedForOurProject();
32688
+ const its = this.announcedPidOf(run);
32689
+ const endpoint = its === undefined ? undefined : runtimesAnnounced().running.find((one) => one.pid === its);
32480
32690
  if (endpoint === undefined) {
32481
32691
  return {
32482
32692
  heldAt: undefined,
@@ -32501,13 +32711,13 @@ class GodotServer {
32501
32711
  return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
32502
32712
  }
32503
32713
  async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
32504
- const { op: _op, projectPath, ...params } = asParams(args);
32714
+ const { op: _op, projectPath, pid, ...params } = asParams(args);
32505
32715
  const announced = runtimesAnnounced();
32506
- const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken);
32716
+ const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken, typeof pid === "number" ? pid : undefined);
32507
32717
  if ("problem" in choice) {
32508
32718
  return this.createErrorResponse(choice.problem);
32509
32719
  }
32510
- const held = this.knownHoldOn(choice.endpoint);
32720
+ const held = this.knownHoldOn(choice.endpoint, announced.running);
32511
32721
  if (held !== null) {
32512
32722
  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.`, [
32513
32723
  "debug_control continue lets it go, after which this call answers",
@@ -32569,7 +32779,7 @@ class GodotServer {
32569
32779
  const includeHidden = readBoolean(args, "includeHidden");
32570
32780
  return await this.handleRuntimeCommand("find_nodes", {
32571
32781
  ...filters,
32572
- projectPath: args["projectPath"],
32782
+ ...whichGame(args),
32573
32783
  root: readNonEmptyString(args, "nodePath") ?? "/root",
32574
32784
  limit: readPositiveNumber(args, "limit") ?? 100,
32575
32785
  ...property === undefined ? {} : { property },
@@ -32580,18 +32790,18 @@ class GodotServer {
32580
32790
  if (op === "frames") {
32581
32791
  const frames = readPositiveNumber(args, "frames") ?? 1;
32582
32792
  const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
32583
- return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
32793
+ return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
32584
32794
  }
32585
32795
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
32586
32796
  const nodePath = readNonEmptyString(args, "nodePath") ?? "";
32587
32797
  const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
32588
32798
  return op === "signal" ? await this.handleRuntimeCommand("wait_signal", {
32589
- projectPath: args["projectPath"],
32799
+ ...whichGame(args),
32590
32800
  path: nodePath,
32591
32801
  signal: readString(args, "signal") ?? "",
32592
32802
  timeout_ms: timeoutMs
32593
32803
  }, patience) : await this.handleRuntimeCommand("wait_until", {
32594
- projectPath: args["projectPath"],
32804
+ ...whichGame(args),
32595
32805
  path: nodePath,
32596
32806
  property: readString(args, "property") ?? "",
32597
32807
  value: args["value"],