gdharness 0.13.39 → 0.13.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/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
  }
@@ -28316,7 +28368,25 @@ function tooNew(unspoken) {
28316
28368
  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
28369
  return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
28318
28370
  }
28319
- function chooseRuntime(endpoints, projectPath, unspoken = []) {
28371
+ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
28372
+ if (pid !== undefined) {
28373
+ const named = endpoints.find((endpoint) => endpoint.pid === pid);
28374
+ if (named !== undefined) {
28375
+ if (projectPath !== undefined && resolve4(named.project.path) !== resolve4(projectPath)) {
28376
+ return {
28377
+ problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve4(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
28378
+ };
28379
+ }
28380
+ return { endpoint: named };
28381
+ }
28382
+ const tooNewToo = unspoken.find((game) => game.pid === pid);
28383
+ if (tooNewToo !== undefined) {
28384
+ return { problem: tooNew([tooNewToo]) };
28385
+ }
28386
+ return {
28387
+ 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.`
28388
+ };
28389
+ }
28320
28390
  if (endpoints.length === 0) {
28321
28391
  if (unspoken.length > 0) {
28322
28392
  return { problem: tooNew(unspoken) };
@@ -28347,14 +28417,14 @@ function chooseRuntime(endpoints, projectPath, unspoken = []) {
28347
28417
  return { endpoint: matching[0] };
28348
28418
  }
28349
28419
  return {
28350
- problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Stop all but one.`
28420
+ problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
28351
28421
  };
28352
28422
  }
28353
28423
  if (endpoints.length === 1 && endpoints[0]) {
28354
28424
  return { endpoint: endpoints[0] };
28355
28425
  }
28356
28426
  return {
28357
- problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one.`
28427
+ problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
28358
28428
  };
28359
28429
  }
28360
28430
  var nextRequestId = 1;
@@ -28765,6 +28835,10 @@ var RUNNING_PROJECT_PATH = {
28765
28835
  type: "string",
28766
28836
  description: "Which game, when more than one is running: the project directory it was started from. Not needed with one game."
28767
28837
  };
28838
+ var RUNNING_PID = {
28839
+ type: "number",
28840
+ 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."
28841
+ };
28768
28842
  var SCENE_PATH = {
28769
28843
  type: "string",
28770
28844
  description: 'Scene file inside the project, such as "scenes/main.tscn" or "res://scenes/main.tscn".'
@@ -29488,6 +29562,7 @@ var TOOL_SPECS = [
29488
29562
  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
29563
  parameters: {
29490
29564
  projectPath: RUNNING_PROJECT_PATH,
29565
+ pid: RUNNING_PID,
29491
29566
  nodePath: {
29492
29567
  type: "string",
29493
29568
  ops: ["tree", "text", "find", "rect", "property"],
@@ -29566,6 +29641,7 @@ var TOOL_SPECS = [
29566
29641
  description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon.",
29567
29642
  parameters: {
29568
29643
  projectPath: RUNNING_PROJECT_PATH,
29644
+ pid: RUNNING_PID,
29569
29645
  nodePath: { type: "string", description: 'Absolute node path, such as "/root/Main/Player".' },
29570
29646
  property: {
29571
29647
  type: "string",
@@ -29605,6 +29681,7 @@ var TOOL_SPECS = [
29605
29681
  description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window.",
29606
29682
  parameters: {
29607
29683
  projectPath: RUNNING_PROJECT_PATH,
29684
+ pid: RUNNING_PID,
29608
29685
  viewportPath: {
29609
29686
  type: "string",
29610
29687
  ops: ["viewport"],
@@ -29625,6 +29702,7 @@ var TOOL_SPECS = [
29625
29702
  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
29703
  parameters: {
29627
29704
  projectPath: RUNNING_PROJECT_PATH,
29705
+ pid: RUNNING_PID,
29628
29706
  nodePath: {
29629
29707
  type: "string",
29630
29708
  ops: ["click", "choose"],
@@ -29721,6 +29799,7 @@ var TOOL_SPECS = [
29721
29799
  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
29800
  parameters: {
29723
29801
  projectPath: RUNNING_PROJECT_PATH,
29802
+ pid: RUNNING_PID,
29724
29803
  frames: {
29725
29804
  type: "number",
29726
29805
  ops: ["frames"],
@@ -29756,7 +29835,7 @@ var TOOL_SPECS = [
29756
29835
  },
29757
29836
  {
29758
29837
  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.",
29838
+ 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
29839
  parameters: {
29761
29840
  projectPath: PROJECT_PATH,
29762
29841
  scriptPath: SCRIPT_PATH,
@@ -29858,6 +29937,9 @@ function patienceForFrames(frames, atLeast) {
29858
29937
  function projectSpelling(projectPath, absolutePath) {
29859
29938
  return `res://${relative3(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
29860
29939
  }
29940
+ function whichGame(args) {
29941
+ return { projectPath: args["projectPath"], pid: args["pid"] };
29942
+ }
29861
29943
  function describeHalt(halt) {
29862
29944
  switch (halt.reason) {
29863
29945
  case "breakpoint":
@@ -29893,6 +29975,19 @@ function runIsUp(run, editorSays) {
29893
29975
  }
29894
29976
  return editorSays ?? stillRunning(run);
29895
29977
  }
29978
+ function endedPreviousRun(ended) {
29979
+ if (ended === null) {
29980
+ return;
29981
+ }
29982
+ return ended.pid ?? true;
29983
+ }
29984
+ function endedToStartThis(ended) {
29985
+ if (ended === null) {
29986
+ return "";
29987
+ }
29988
+ const which = ended.pid === null ? "The game the editor was playing" : `The run that was going, pid ${ended.pid},`;
29989
+ return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
29990
+ }
29896
29991
  function runtimeVerdict(endpoint, after) {
29897
29992
  if (endpoint !== null) {
29898
29993
  return { listening: true, pid: endpoint.pid, port: endpoint.port };
@@ -30633,7 +30728,7 @@ class GodotServer {
30633
30728
  switch (op) {
30634
30729
  case "tree":
30635
30730
  return await this.handleRuntimeCommand("get_tree", {
30636
- projectPath: args["projectPath"],
30731
+ ...whichGame(args),
30637
30732
  root: readNonEmptyString(args, "nodePath") ?? "/root",
30638
30733
  depth: readPositiveNumber(args, "depth") ?? 3,
30639
30734
  include_properties: readBoolean(args, "includeProperties") ?? false
@@ -30642,36 +30737,36 @@ class GodotServer {
30642
30737
  return await this.handleFindRuntimeNodes(args);
30643
30738
  case "text":
30644
30739
  return await this.handleRuntimeCommand("read_text", {
30645
- projectPath: args["projectPath"],
30740
+ ...whichGame(args),
30646
30741
  root: readNonEmptyString(args, "nodePath") ?? "/root",
30647
30742
  include_hidden: readBoolean(args, "includeHidden") ?? false,
30648
30743
  limit: readPositiveNumber(args, "limit") ?? 500
30649
30744
  });
30650
30745
  case "rect":
30651
30746
  return await this.handleRuntimeCommand("get_rect", {
30652
- projectPath: args["projectPath"],
30747
+ ...whichGame(args),
30653
30748
  path: readNonEmptyString(args, "nodePath") ?? ""
30654
30749
  });
30655
30750
  case "property":
30656
30751
  return await this.handleRuntimeCommand("get_property", {
30657
- projectPath: args["projectPath"],
30752
+ ...whichGame(args),
30658
30753
  path: readNonEmptyString(args, "nodePath") ?? "",
30659
30754
  property: readNonEmptyString(args, "property") ?? ""
30660
30755
  });
30661
30756
  default:
30662
30757
  return await this.handleRuntimeCommand("get_metrics", {
30663
- projectPath: args["projectPath"],
30758
+ ...whichGame(args),
30664
30759
  metrics: readArray(args, "metrics") ?? []
30665
30760
  });
30666
30761
  }
30667
30762
  case "runtime_invoke":
30668
30763
  return op === "set" ? await this.handleRuntimeCommand("set_property", {
30669
- projectPath: args["projectPath"],
30764
+ ...whichGame(args),
30670
30765
  path: readNonEmptyString(args, "nodePath") ?? "",
30671
30766
  property: readString(args, "property") ?? "",
30672
30767
  value: args["value"]
30673
30768
  }) : await this.handleRuntimeCommand("call_method", {
30674
- projectPath: args["projectPath"],
30769
+ ...whichGame(args),
30675
30770
  path: readNonEmptyString(args, "nodePath") ?? "",
30676
30771
  method: readString(args, "method") ?? "",
30677
30772
  args: readArray(args, "args") ?? []
@@ -30681,7 +30776,7 @@ class GodotServer {
30681
30776
  case "runtime_input":
30682
30777
  if (op === "click") {
30683
30778
  return await this.handleRuntimeCommand("click", {
30684
- projectPath: args["projectPath"],
30779
+ ...whichGame(args),
30685
30780
  path: readNonEmptyString(args, "nodePath") ?? "",
30686
30781
  button: readString(args, "button") ?? "left",
30687
30782
  double: readBoolean(args, "doubleClick") ?? false
@@ -30689,7 +30784,7 @@ class GodotServer {
30689
30784
  }
30690
30785
  if (op === "choose") {
30691
30786
  return await this.handleRuntimeCommand("choose", {
30692
- projectPath: args["projectPath"],
30787
+ ...whichGame(args),
30693
30788
  path: readNonEmptyString(args, "nodePath") ?? "",
30694
30789
  text: readString(args, "text") ?? "",
30695
30790
  ...args["index"] === undefined ? {} : { index: args["index"] }
@@ -30720,7 +30815,9 @@ class GodotServer {
30720
30815
  return this.jsonTextResponse({
30721
30816
  ...asParams(JSON.parse(answered.content[0]?.text ?? "{}")),
30722
30817
  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."
30818
+ setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor()),
30819
+ 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.",
30820
+ ...this.breakpointsAtRisk()
30724
30821
  });
30725
30822
  }
30726
30823
  case "debug_control":
@@ -30794,6 +30891,16 @@ class GodotServer {
30794
30891
  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
30892
  };
30796
30893
  }
30894
+ const going = await this.runStillGoing(game);
30895
+ if (!going) {
30896
+ return {
30897
+ ok: false,
30898
+ response: this.createErrorResponse("The last run has already ended, so there is no debug session to answer for.", [
30899
+ "editor_output still reads what it printed, until the next run starts",
30900
+ "editor_run start plays another, which the debugger can hold"
30901
+ ])
30902
+ };
30903
+ }
30797
30904
  if (!game.throughEditor) {
30798
30905
  return {
30799
30906
  ok: false,
@@ -30815,7 +30922,7 @@ class GodotServer {
30815
30922
  };
30816
30923
  }
30817
30924
  if (!this.dap().isStopped()) {
30818
- const hold = await this.holdOf(game);
30925
+ const hold = await this.holdOf(game, going);
30819
30926
  if (hold.heldAt !== null) {
30820
30927
  if (intent === "move") {
30821
30928
  return { ok: true, value: game };
@@ -31381,6 +31488,12 @@ class GodotServer {
31381
31488
  this.dapClient.setOutputSink((line) => {
31382
31489
  this.writeToTranscript(line);
31383
31490
  });
31491
+ this.dapClient.setBreakpointsSink((held) => {
31492
+ const project = this.godotBridge.getStatus().projectPath ?? this.ownProject;
31493
+ if (project !== null) {
31494
+ writeBreakpointNote(project, held);
31495
+ }
31496
+ });
31384
31497
  }
31385
31498
  return this.dapClient;
31386
31499
  }
@@ -31392,6 +31505,15 @@ class GodotServer {
31392
31505
  }
31393
31506
  return client;
31394
31507
  }
31508
+ breakpointsAtRisk() {
31509
+ const status = this.godotBridge.getStatus();
31510
+ if (!status.connected || status.syncsBreakpoints === true) {
31511
+ return {};
31512
+ }
31513
+ return {
31514
+ 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."
31515
+ };
31516
+ }
31395
31517
  breakpointsAsProjectSpells(projectPath, held) {
31396
31518
  return held.map((one) => ({
31397
31519
  scriptPath: projectSpelling(projectPath, one.scriptPath),
@@ -31454,6 +31576,7 @@ class GodotServer {
31454
31576
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
31455
31577
  startupError: this.bridgeStartupError,
31456
31578
  staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
31579
+ ...this.breakpointsAtRisk(),
31457
31580
  retryingBridge: this.bridgeRetry === null ? undefined : true,
31458
31581
  announcedAt: this.announcedAt ?? undefined,
31459
31582
  projectIs: this.projectHasMovedOn() ?? undefined,
@@ -31505,9 +31628,14 @@ class GodotServer {
31505
31628
  const playing = await this.editorPlayingState();
31506
31629
  await this.pickUpWhatTheEditorIsPlaying();
31507
31630
  const current = this.currentRun();
31508
- return { playing, hold: current === null ? {} : await this.holdOf(current) };
31631
+ const active = current === null ? false : await this.runStillGoing(current, playing?.playing ?? null);
31632
+ return {
31633
+ playing,
31634
+ active,
31635
+ hold: current === null ? {} : await this.holdOf(current, active)
31636
+ };
31509
31637
  })();
31510
- const [games, { playing, hold }] = await Promise.all([pinged, asked]);
31638
+ const [games, { playing, active, hold }] = await Promise.all([pinged, asked]);
31511
31639
  return this.jsonTextResponse({
31512
31640
  editor: {
31513
31641
  ...this.getEditorStatusPayload(),
@@ -31519,7 +31647,7 @@ class GodotServer {
31519
31647
  problem: located.ok ? undefined : GodotLocator.refusal(located).message
31520
31648
  },
31521
31649
  game: {
31522
- processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
31650
+ processActive: active,
31523
31651
  playingInEditor: playing,
31524
31652
  ...hold,
31525
31653
  runtimeConnected: games.some((game) => game.reachable),
@@ -31732,10 +31860,12 @@ class GodotServer {
31732
31860
  if (op === "check") {
31733
31861
  return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
31734
31862
  }
31863
+ await this.pickUpWhatTheEditorIsPlaying();
31735
31864
  let ended = null;
31736
- if (stillRunning(this.currentRun())) {
31865
+ const before = this.currentRun();
31866
+ if (before !== null && await this.runStillGoing(before)) {
31737
31867
  this.logDebug("Ending the running game before starting another");
31738
- ended = this.currentRun()?.pid ?? null;
31868
+ ended = { pid: before.pid ?? this.announcedPidOf(before) ?? null };
31739
31869
  await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
31740
31870
  }
31741
31871
  const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
@@ -31759,8 +31889,8 @@ class GodotServer {
31759
31889
  const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
31760
31890
  this.activeProcess = started;
31761
31891
  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));
31892
+ const endedForThis = endedToStartThis(ended);
31893
+ const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
31764
31894
  const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
31765
31895
  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
31896
  return this.jsonTextResponse({
@@ -31771,23 +31901,38 @@ class GodotServer {
31771
31901
  arguments: cmdArgs,
31772
31902
  transcript: started.transcript,
31773
31903
  refreshedClasses: refreshed.value,
31774
- endedPreviousRun: ended ?? undefined,
31904
+ endedPreviousRun: endedPreviousRun(ended),
31775
31905
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
31776
31906
  message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
31777
31907
  });
31778
31908
  }
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
31909
  async runtimeUp(projectPath, before, budgetMs, withArgs) {
31785
31910
  if (!existsSync9(join13(projectPath, RUNTIME_AUTOLOAD.path))) {
31786
31911
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
31787
31912
  }
31913
+ let asking = null;
31914
+ let editorAskedAt = 0;
31915
+ let editorSaysGoing = true;
31788
31916
  const endpoint = await announcedSince(projectPath, before, {
31789
31917
  budgetMs,
31790
- giveUp: () => this.dapClient?.isStopped() === true || !stillRunning(this.currentRun())
31918
+ giveUp: () => {
31919
+ if (this.dapClient?.isStopped() === true) {
31920
+ return true;
31921
+ }
31922
+ const run = this.currentRun();
31923
+ if (run === null || !run.throughEditor) {
31924
+ return !stillRunning(run);
31925
+ }
31926
+ if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
31927
+ editorAskedAt = Date.now();
31928
+ asking = this.runStillGoing(run).then((going) => {
31929
+ editorSaysGoing = going;
31930
+ }).finally(() => {
31931
+ asking = null;
31932
+ });
31933
+ }
31934
+ return !editorSaysGoing;
31935
+ }
31791
31936
  });
31792
31937
  const going = this.currentRun();
31793
31938
  if (endpoint !== null && going?.throughEditor === true && going.announcedPid === undefined) {
@@ -31797,7 +31942,7 @@ class GodotServer {
31797
31942
  addon: true,
31798
31943
  budgetMs,
31799
31944
  heldAt: this.dapClient?.whereItStopped() ?? null,
31800
- running: await this.gameIsUp(),
31945
+ running: endpoint !== null || going !== null && await this.runStillGoing(going),
31801
31946
  withArgs
31802
31947
  });
31803
31948
  }
@@ -31838,7 +31983,8 @@ class GodotServer {
31838
31983
  startedAt,
31839
31984
  exitCode: null,
31840
31985
  throughEditor: true,
31841
- brokeOn: null
31986
+ brokeOn: null,
31987
+ announcedBefore: alreadyPlaying
31842
31988
  };
31843
31989
  this.activeProcess = played;
31844
31990
  writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
@@ -31849,14 +31995,14 @@ class GodotServer {
31849
31995
  scene: scene === null ? "the main scene" : `res://${scene}`,
31850
31996
  refreshedClasses,
31851
31997
  debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
31852
- endedPreviousRun: ended ?? undefined,
31998
+ endedPreviousRun: endedPreviousRun(ended),
31853
31999
  breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
31854
32000
  breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
31855
32001
  scriptPath: projectSpelling(projectPath, one.scriptPath),
31856
32002
  reason: one.reason
31857
32003
  })),
31858
32004
  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.`)
32005
+ 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
32006
  });
31861
32007
  }
31862
32008
  drainEditorOutput(game) {
@@ -32072,15 +32218,19 @@ class GodotServer {
32072
32218
  brokeOn: null,
32073
32219
  pickedUpPlaying: true
32074
32220
  };
32075
- const announced = this.theOneAnnouncedForOurProject();
32076
- if (announced !== undefined) {
32077
- picked.announcedPid = announced.pid;
32078
- }
32079
32221
  this.activeProcess = picked;
32080
32222
  }
32081
- theOneAnnouncedForOurProject() {
32082
- const ofProject = this.allAnnouncedForOurProject(runtimesAnnounced().running);
32083
- return ofProject.length === 1 ? ofProject[0] : undefined;
32223
+ announcedPidOf(run) {
32224
+ if (!run.throughEditor || run.announcedPid !== undefined) {
32225
+ return run.announcedPid;
32226
+ }
32227
+ const before = run.announcedBefore ?? new Set;
32228
+ const fresh = this.allAnnouncedForOurProject(runtimesAnnounced().running).filter((one) => !before.has(one.pid));
32229
+ const theOne = fresh.length === 1 ? fresh[0] : undefined;
32230
+ if (theOne !== undefined) {
32231
+ run.announcedPid = theOne.pid;
32232
+ }
32233
+ return run.announcedPid;
32084
32234
  }
32085
32235
  adoptRecordedRun() {
32086
32236
  const record = readRunRecord();
@@ -32210,12 +32360,22 @@ class GodotServer {
32210
32360
  }
32211
32361
  const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
32212
32362
  const until = Date.now() + budgetMs;
32213
- while (stillRunning(run) && Date.now() < until) {
32363
+ while (await this.runStillGoing(run) && Date.now() < until) {
32214
32364
  this.drainEditorOutput(run);
32215
32365
  this.drainTranscript(run);
32216
32366
  await delay3(RUN_POLL_MS);
32217
32367
  }
32218
- return await this.handleGetDebugOutput(args, stillRunning(run) ? budgetMs : undefined);
32368
+ return await this.handleGetDebugOutput(args, await this.runStillGoing(run) ? budgetMs : undefined);
32369
+ }
32370
+ async runStillGoing(run, editorSays) {
32371
+ if (!run.throughEditor) {
32372
+ return stillRunning(run);
32373
+ }
32374
+ const announced = this.announcedPidOf(run);
32375
+ if (announced !== undefined && !alive(announced)) {
32376
+ return false;
32377
+ }
32378
+ return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
32219
32379
  }
32220
32380
  async handleGetDebugOutput(args, waitedMs) {
32221
32381
  await this.pickUpWhatTheEditorIsPlaying();
@@ -32225,7 +32385,8 @@ class GodotServer {
32225
32385
  }
32226
32386
  this.drainEditorOutput(run);
32227
32387
  this.drainTranscript(run);
32228
- if (run.endedUnwatched !== true && !stillRunning(run)) {
32388
+ const going = await this.runStillGoing(run);
32389
+ if (run.endedUnwatched !== true && !going) {
32229
32390
  run.log.finish();
32230
32391
  run.endedUnwatched = run.exitCode === null;
32231
32392
  }
@@ -32236,12 +32397,13 @@ class GodotServer {
32236
32397
  contains: readNonEmptyString(args, "contains"),
32237
32398
  limit: readPositiveNumber(args, "limit") ?? 200
32238
32399
  });
32239
- const hold = await this.holdOf(run);
32400
+ const hold = await this.holdOf(run, going);
32240
32401
  const wanted = readBoolean(args, "cpu") ?? false;
32241
- const cpuSeconds = wanted && run.pid !== null && stillRunning(run) ? await cpuSecondsOf(run.pid) : undefined;
32402
+ const processToAsk = run.pid ?? this.announcedPidOf(run) ?? null;
32403
+ const cpuSeconds = wanted && processToAsk !== null && stillRunning(run) ? await cpuSecondsOf(processToAsk) : undefined;
32242
32404
  const notes = [];
32243
32405
  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.");
32406
+ 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
32407
  }
32246
32408
  if (waitedMs !== undefined) {
32247
32409
  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 +32427,10 @@ class GodotServer {
32265
32427
  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
32428
  }
32267
32429
  return this.jsonTextResponse({
32268
- running: stillRunning(run),
32430
+ running: going,
32269
32431
  exitCode: run.exitCode,
32270
32432
  through: run.throughEditor ? "editor" : "gdharness",
32271
- pid: run.pid,
32433
+ pid: run.pid ?? this.announcedPidOf(run) ?? null,
32272
32434
  errors: run.log.count("error"),
32273
32435
  warnings: run.log.count("warning"),
32274
32436
  clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
@@ -32294,18 +32456,19 @@ class GodotServer {
32294
32456
  }
32295
32457
  this.drainEditorOutput(stopped);
32296
32458
  this.drainTranscript(stopped);
32459
+ const wasRunning = await this.runStillGoing(stopped);
32297
32460
  this.logDebug("Stopping the running game");
32298
32461
  await this.endActiveGame("editor_run stop");
32299
32462
  return this.jsonTextResponse({
32300
32463
  stopped: true,
32301
32464
  through: stopped.throughEditor ? "editor" : "gdharness",
32302
- endedPid: stopped.pid,
32303
- exitedBeforeStop: stopped.exitCode !== null,
32465
+ endedPid: stopped.pid ?? this.announcedPidOf(stopped) ?? null,
32466
+ exitedBeforeStop: stopped.exitCode !== null || !wasRunning,
32304
32467
  exitCode: stopped.exitCode,
32305
32468
  errors: stopped.log.count("error"),
32306
32469
  warnings: stopped.log.count("warning"),
32307
32470
  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.",
32471
+ 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
32472
  entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
32310
32473
  });
32311
32474
  }
@@ -32462,21 +32625,21 @@ class GodotServer {
32462
32625
  if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
32463
32626
  return null;
32464
32627
  }
32465
- const its = run.announcedPid ?? this.theOneAnnouncedForOurProject()?.pid;
32466
- if (its !== endpoint.pid || !session.holdIsKnown()) {
32628
+ if (this.announcedPidOf(run) !== endpoint.pid || !session.holdIsKnown()) {
32467
32629
  return null;
32468
32630
  }
32469
32631
  return session.whereItStopped();
32470
32632
  }
32471
- async holdOf(run) {
32472
- if (!run.throughEditor || !stillRunning(run)) {
32633
+ async holdOf(run, going) {
32634
+ if (!run.throughEditor || !(going ?? await this.runStillGoing(run))) {
32473
32635
  return { heldAt: null };
32474
32636
  }
32475
32637
  const session = this.dapClient;
32476
32638
  if (session?.holdIsKnown()) {
32477
32639
  return { heldAt: session.whereItStopped() };
32478
32640
  }
32479
- const endpoint = run.announcedPid !== undefined ? runtimesAnnounced().running.find((one) => one.pid === run.announcedPid) : this.theOneAnnouncedForOurProject();
32641
+ const its = this.announcedPidOf(run);
32642
+ const endpoint = its === undefined ? undefined : runtimesAnnounced().running.find((one) => one.pid === its);
32480
32643
  if (endpoint === undefined) {
32481
32644
  return {
32482
32645
  heldAt: undefined,
@@ -32501,9 +32664,9 @@ class GodotServer {
32501
32664
  return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
32502
32665
  }
32503
32666
  async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
32504
- const { op: _op, projectPath, ...params } = asParams(args);
32667
+ const { op: _op, projectPath, pid, ...params } = asParams(args);
32505
32668
  const announced = runtimesAnnounced();
32506
- const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken);
32669
+ const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken, typeof pid === "number" ? pid : undefined);
32507
32670
  if ("problem" in choice) {
32508
32671
  return this.createErrorResponse(choice.problem);
32509
32672
  }
@@ -32569,7 +32732,7 @@ class GodotServer {
32569
32732
  const includeHidden = readBoolean(args, "includeHidden");
32570
32733
  return await this.handleRuntimeCommand("find_nodes", {
32571
32734
  ...filters,
32572
- projectPath: args["projectPath"],
32735
+ ...whichGame(args),
32573
32736
  root: readNonEmptyString(args, "nodePath") ?? "/root",
32574
32737
  limit: readPositiveNumber(args, "limit") ?? 100,
32575
32738
  ...property === undefined ? {} : { property },
@@ -32580,18 +32743,18 @@ class GodotServer {
32580
32743
  if (op === "frames") {
32581
32744
  const frames = readPositiveNumber(args, "frames") ?? 1;
32582
32745
  const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
32583
- return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
32746
+ return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
32584
32747
  }
32585
32748
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
32586
32749
  const nodePath = readNonEmptyString(args, "nodePath") ?? "";
32587
32750
  const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
32588
32751
  return op === "signal" ? await this.handleRuntimeCommand("wait_signal", {
32589
- projectPath: args["projectPath"],
32752
+ ...whichGame(args),
32590
32753
  path: nodePath,
32591
32754
  signal: readString(args, "signal") ?? "",
32592
32755
  timeout_ms: timeoutMs
32593
32756
  }, patience) : await this.handleRuntimeCommand("wait_until", {
32594
- projectPath: args["projectPath"],
32757
+ ...whichGame(args),
32595
32758
  path: nodePath,
32596
32759
  property: readString(args, "property") ?? "",
32597
32760
  value: args["value"],