gdharness 0.13.40 → 0.13.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -16061,7 +16061,7 @@ var init_tool_definitions = __esm(() => {
16061
16061
  },
16062
16062
  {
16063
16063
  name: "editor_status",
16064
- description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
16064
+ description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
16065
16065
  parameters: {},
16066
16066
  requires: []
16067
16067
  },
@@ -36447,7 +36447,7 @@ function secondsFromClock(printed) {
36447
36447
  const extra = days === null ? 0 : Number(days) * 86400;
36448
36448
  return Number.isFinite(extra) ? seconds + extra : undefined;
36449
36449
  }
36450
- var run5, ASK_TIMEOUT_MS = 2000;
36450
+ var run5, ASK_TIMEOUT_MS = 6000;
36451
36451
  var init_process_time = __esm(() => {
36452
36452
  run5 = promisify5(execFile5);
36453
36453
  });
@@ -36676,7 +36676,18 @@ function parseAnnouncement(file, pid) {
36676
36676
  if (protocol !== RUNTIME_PROTOCOL) {
36677
36677
  return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
36678
36678
  }
36679
- return { kind: "runtime", endpoint: { pid, port, address, project: named, file } };
36679
+ const editorPid = readNumber(fields, "editor_pid");
36680
+ return {
36681
+ kind: "runtime",
36682
+ endpoint: {
36683
+ pid,
36684
+ port,
36685
+ address,
36686
+ project: named,
36687
+ file,
36688
+ ...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
36689
+ }
36690
+ };
36680
36691
  }
36681
36692
  function runtimesAnnounced(directories = runtimeDirectories()) {
36682
36693
  const running = new Map;
@@ -36750,7 +36761,7 @@ async function announcedSince(projectPath, before, waiting = {}) {
36750
36761
  const directories = waiting.directories ?? runtimeDirectories();
36751
36762
  const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
36752
36763
  for (;; ) {
36753
- const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted);
36764
+ const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted && (waiting.accept?.(endpoint) ?? true));
36754
36765
  if (fresh !== undefined) {
36755
36766
  return fresh;
36756
36767
  }
@@ -37425,6 +37436,16 @@ function hasMainScene(projectFile) {
37425
37436
  const scene = parseProjectGodot(readFileSync17(projectFile, "utf8"))["application"]?.["run/main_scene"];
37426
37437
  return typeof scene === "string" && scene !== "";
37427
37438
  }
37439
+ function runtimeAddonAnnouncesEditors(projectPath) {
37440
+ if (projectPath === null) {
37441
+ return false;
37442
+ }
37443
+ try {
37444
+ return readFileSync17(join19(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
37445
+ } catch {
37446
+ return false;
37447
+ }
37448
+ }
37428
37449
  function editorPlaysHeadless(projectFile) {
37429
37450
  const runArgs = parseProjectGodot(readFileSync17(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
37430
37451
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
@@ -38862,7 +38883,7 @@ class GodotServer {
38862
38883
  const located = await this.locator.find();
38863
38884
  const announced = runtimesAnnounced();
38864
38885
  const pinged = Promise.all(announced.running.map(async (endpoint) => {
38865
- const held = this.knownHoldOn(endpoint);
38886
+ const held = this.knownHoldOn(endpoint, announced.running);
38866
38887
  const reply = held === null ? await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS) : {
38867
38888
  ok: false,
38868
38889
  message: `The game is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is. debug_control continue lets it go.`
@@ -38871,6 +38892,7 @@ class GodotServer {
38871
38892
  pid: endpoint.pid,
38872
38893
  port: endpoint.port,
38873
38894
  project: endpoint.project,
38895
+ editorPid: endpoint.editorPid,
38874
38896
  reachable: reply.ok,
38875
38897
  problem: reply.ok ? null : reply.message
38876
38898
  };
@@ -39166,6 +39188,7 @@ class GodotServer {
39166
39188
  let editorSaysGoing = true;
39167
39189
  const endpoint = await announcedSince(projectPath, before, {
39168
39190
  budgetMs,
39191
+ accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
39169
39192
  giveUp: () => {
39170
39193
  if (this.dapClient?.isStopped() === true) {
39171
39194
  return true;
@@ -39471,18 +39494,39 @@ class GodotServer {
39471
39494
  };
39472
39495
  this.activeProcess = picked;
39473
39496
  }
39474
- announcedPidOf(run) {
39497
+ announcedPidOf(run, announced) {
39475
39498
  if (!run.throughEditor || run.announcedPid !== undefined) {
39476
39499
  return run.announcedPid;
39477
39500
  }
39478
39501
  const before = run.announcedBefore ?? new Set;
39479
- const fresh = this.allAnnouncedForOurProject(runtimesAnnounced().running).filter((one) => !before.has(one.pid));
39480
- const theOne = fresh.length === 1 ? fresh[0] : undefined;
39502
+ const fresh = this.allAnnouncedForOurProject(announced ?? runtimesAnnounced().running).filter((one) => !before.has(one.pid));
39503
+ const candidates = fresh.filter((one) => this.playedByOurEditor(one, run.projectPath));
39504
+ const theOne = candidates.length === 1 ? candidates[0] : undefined;
39481
39505
  if (theOne !== undefined) {
39482
39506
  run.announcedPid = theOne.pid;
39483
39507
  }
39484
39508
  return run.announcedPid;
39485
39509
  }
39510
+ playedByOurEditor(endpoint, projectPath) {
39511
+ const editor = this.godotBridge.getStatus().editorPid;
39512
+ if (editor === undefined) {
39513
+ return true;
39514
+ }
39515
+ if (endpoint.editorPid !== undefined) {
39516
+ return endpoint.editorPid === editor;
39517
+ }
39518
+ return !runtimeAddonAnnouncesEditors(projectPath ?? this.ownProject);
39519
+ }
39520
+ isTheGameOf(run, endpoint, projectPath) {
39521
+ if (run === null) {
39522
+ return false;
39523
+ }
39524
+ if (run.throughEditor) {
39525
+ return this.playedByOurEditor(endpoint, projectPath);
39526
+ }
39527
+ const editor = this.godotBridge.getStatus().editorPid;
39528
+ return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
39529
+ }
39486
39530
  adoptRecordedRun() {
39487
39531
  const record = readRunRecord();
39488
39532
  if (record === null || !this.couldBeOurs(record.projectPath)) {
@@ -39626,6 +39670,9 @@ class GodotServer {
39626
39670
  if (announced !== undefined && !alive(announced)) {
39627
39671
  return false;
39628
39672
  }
39673
+ if (announced !== undefined && editorSays === undefined) {
39674
+ return stillRunning(run);
39675
+ }
39629
39676
  return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
39630
39677
  }
39631
39678
  async handleGetDebugOutput(args, waitedMs) {
@@ -39870,13 +39917,13 @@ class GodotServer {
39870
39917
  const override = Number.parseInt(envValue("GDHARNESS_RUNTIME_TIMEOUT_MS") ?? "", 10);
39871
39918
  return Number.isInteger(override) && override > 0 ? override : 1e4;
39872
39919
  }
39873
- knownHoldOn(endpoint) {
39920
+ knownHoldOn(endpoint, announced) {
39874
39921
  const run = this.activeProcess;
39875
39922
  const session = this.dapClient;
39876
39923
  if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
39877
39924
  return null;
39878
39925
  }
39879
- if (this.announcedPidOf(run) !== endpoint.pid || !session.holdIsKnown()) {
39926
+ if (this.announcedPidOf(run, announced) !== endpoint.pid || !session.holdIsKnown()) {
39880
39927
  return null;
39881
39928
  }
39882
39929
  return session.whereItStopped();
@@ -39921,7 +39968,7 @@ class GodotServer {
39921
39968
  if ("problem" in choice) {
39922
39969
  return this.createErrorResponse(choice.problem);
39923
39970
  }
39924
- const held = this.knownHoldOn(choice.endpoint);
39971
+ const held = this.knownHoldOn(choice.endpoint, announced.running);
39925
39972
  if (held !== null) {
39926
39973
  return this.createErrorResponse(`The game (pid ${choice.endpoint.pid}) is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is.`, [
39927
39974
  "debug_control continue lets it go, after which this call answers",
@@ -43,6 +43,12 @@ const DAP_ASKED: String = "GDHARNESS_DAP_PORT"
43
43
  ## What a server that opened this editor says about itself, so that being opened by one is stated
44
44
  ## rather than guessed at from the ports. Kept in step with `OPENED_BY_A_SERVER` in src/launch.ts.
45
45
  const OPENED_BY_A_SERVER: String = "GDHARNESS_OPENED_BY_A_SERVER"
46
+ ## Put into this editor's own environment as it loads, so every game it plays inherits it and
47
+ ## the runtime addon can announce which editor played it. A game of the same project that some
48
+ ## other server started has no such variable, or another editor's number, and that is what
49
+ ## tells the two apart when both are announced at once. Kept in step with `EDITOR_PID_VARIABLE`
50
+ ## in the runtime autoload.
51
+ const EDITOR_PID_VARIABLE: String = "GDHARNESS_EDITOR_PID"
46
52
 
47
53
  ## How long one attempt is given before the address is called a bad one.
48
54
  ##
@@ -93,6 +99,7 @@ func _ready() -> void:
93
99
  _project_path = ProjectSettings.globalize_path("res://")
94
100
  version_at_load = _loaded_version()
95
101
  _keep_breakpoints_through_sessions()
102
+ OS.set_environment(EDITOR_PID_VARIABLE, str(OS.get_process_id()))
96
103
 
97
104
  _reconnect_timer = Timer.new()
98
105
  _reconnect_timer.one_shot = true
@@ -30,6 +30,12 @@ const SCRIPT_FLAGS: PackedStringArray = ["-s", "--script"]
30
30
  ## Serve a script run anyway, for somebody driving a `-s` script rather than a game.
31
31
  const SCRIPT_RUNS_SETTING: String = "gdharness/runtime/serve_script_runs"
32
32
 
33
+ ## The variable the editor addon puts into its own environment, which every game the editor plays
34
+ ## inherits: it says which editor played this game, so a server can tell the editor's game from
35
+ ## another of the same project that some other server started. Kept in step with
36
+ ## `EDITOR_PID_VARIABLE` in the editor addon's bridge client.
37
+ const EDITOR_PID_VARIABLE: String = "GDHARNESS_EDITOR_PID"
38
+
33
39
  var values: Values = Values.new()
34
40
 
35
41
  # The modules are members and not locals of _init, because a Callable holds its object by id
@@ -250,7 +256,7 @@ func _announce(bind_address: String) -> void:
250
256
  ## What the announcement file and the welcome both carry: enough to pick this game out of
251
257
  ## several and to know whether the server speaks its protocol.
252
258
  func _identity(bind_address: String) -> Dictionary:
253
- return {
259
+ var identity: Dictionary = {
254
260
  "protocol": PROTOCOL,
255
261
  "pid": OS.get_process_id(),
256
262
  "port": _port,
@@ -262,6 +268,10 @@ func _identity(bind_address: String) -> Dictionary:
262
268
  },
263
269
  "godot": Engine.get_version_info().get("string", ""),
264
270
  }
271
+ var played_by: String = OS.get_environment(EDITOR_PID_VARIABLE)
272
+ if played_by.is_valid_int():
273
+ identity["editor_pid"] = played_by.to_int()
274
+ return identity
265
275
 
266
276
 
267
277
  func _send_welcome(client: StreamPeerTCP) -> void:
package/build/index.js CHANGED
@@ -27713,7 +27713,7 @@ async function handleLSPTool(client, toolName, args) {
27713
27713
  import { execFile as execFile3 } from "node:child_process";
27714
27714
  import { promisify as promisify3 } from "node:util";
27715
27715
  var run3 = promisify3(execFile3);
27716
- var ASK_TIMEOUT_MS = 2000;
27716
+ var ASK_TIMEOUT_MS = 6000;
27717
27717
  async function cpuSecondsOf(pid) {
27718
27718
  try {
27719
27719
  return process.platform === "win32" ? await askWindows(pid) : await askPosix(pid);
@@ -28270,7 +28270,18 @@ function parseAnnouncement(file, pid) {
28270
28270
  if (protocol !== RUNTIME_PROTOCOL) {
28271
28271
  return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
28272
28272
  }
28273
- 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
+ };
28274
28285
  }
28275
28286
  function runtimesAnnounced(directories = runtimeDirectories()) {
28276
28287
  const running = new Map;
@@ -28349,7 +28360,7 @@ async function announcedSince(projectPath, before, waiting = {}) {
28349
28360
  const directories = waiting.directories ?? runtimeDirectories();
28350
28361
  const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
28351
28362
  for (;; ) {
28352
- 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));
28353
28364
  if (fresh !== undefined) {
28354
28365
  return fresh;
28355
28366
  }
@@ -29505,7 +29516,7 @@ var TOOL_SPECS = [
29505
29516
  },
29506
29517
  {
29507
29518
  name: "editor_status",
29508
- 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.",
29509
29520
  parameters: {},
29510
29521
  requires: []
29511
29522
  },
@@ -30174,6 +30185,16 @@ function hasMainScene(projectFile) {
30174
30185
  const scene = parseProjectGodot(readFileSync13(projectFile, "utf8"))["application"]?.["run/main_scene"];
30175
30186
  return typeof scene === "string" && scene !== "";
30176
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
+ }
30177
30198
  function editorPlaysHeadless(projectFile) {
30178
30199
  const runArgs = parseProjectGodot(readFileSync13(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
30179
30200
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
@@ -31611,7 +31632,7 @@ class GodotServer {
31611
31632
  const located = await this.locator.find();
31612
31633
  const announced = runtimesAnnounced();
31613
31634
  const pinged = Promise.all(announced.running.map(async (endpoint) => {
31614
- const held = this.knownHoldOn(endpoint);
31635
+ const held = this.knownHoldOn(endpoint, announced.running);
31615
31636
  const reply = held === null ? await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS) : {
31616
31637
  ok: false,
31617
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.`
@@ -31620,6 +31641,7 @@ class GodotServer {
31620
31641
  pid: endpoint.pid,
31621
31642
  port: endpoint.port,
31622
31643
  project: endpoint.project,
31644
+ editorPid: endpoint.editorPid,
31623
31645
  reachable: reply.ok,
31624
31646
  problem: reply.ok ? null : reply.message
31625
31647
  };
@@ -31915,6 +31937,7 @@ class GodotServer {
31915
31937
  let editorSaysGoing = true;
31916
31938
  const endpoint = await announcedSince(projectPath, before, {
31917
31939
  budgetMs,
31940
+ accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
31918
31941
  giveUp: () => {
31919
31942
  if (this.dapClient?.isStopped() === true) {
31920
31943
  return true;
@@ -32220,18 +32243,39 @@ class GodotServer {
32220
32243
  };
32221
32244
  this.activeProcess = picked;
32222
32245
  }
32223
- announcedPidOf(run) {
32246
+ announcedPidOf(run, announced) {
32224
32247
  if (!run.throughEditor || run.announcedPid !== undefined) {
32225
32248
  return run.announcedPid;
32226
32249
  }
32227
32250
  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;
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;
32230
32254
  if (theOne !== undefined) {
32231
32255
  run.announcedPid = theOne.pid;
32232
32256
  }
32233
32257
  return run.announcedPid;
32234
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;
32278
+ }
32235
32279
  adoptRecordedRun() {
32236
32280
  const record = readRunRecord();
32237
32281
  if (record === null || !this.couldBeOurs(record.projectPath)) {
@@ -32375,6 +32419,9 @@ class GodotServer {
32375
32419
  if (announced !== undefined && !alive(announced)) {
32376
32420
  return false;
32377
32421
  }
32422
+ if (announced !== undefined && editorSays === undefined) {
32423
+ return stillRunning(run);
32424
+ }
32378
32425
  return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
32379
32426
  }
32380
32427
  async handleGetDebugOutput(args, waitedMs) {
@@ -32619,13 +32666,13 @@ class GodotServer {
32619
32666
  const override = Number.parseInt(envValue("GDHARNESS_RUNTIME_TIMEOUT_MS") ?? "", 10);
32620
32667
  return Number.isInteger(override) && override > 0 ? override : 1e4;
32621
32668
  }
32622
- knownHoldOn(endpoint) {
32669
+ knownHoldOn(endpoint, announced) {
32623
32670
  const run = this.activeProcess;
32624
32671
  const session = this.dapClient;
32625
32672
  if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
32626
32673
  return null;
32627
32674
  }
32628
- if (this.announcedPidOf(run) !== endpoint.pid || !session.holdIsKnown()) {
32675
+ if (this.announcedPidOf(run, announced) !== endpoint.pid || !session.holdIsKnown()) {
32629
32676
  return null;
32630
32677
  }
32631
32678
  return session.whereItStopped();
@@ -32670,7 +32717,7 @@ class GodotServer {
32670
32717
  if ("problem" in choice) {
32671
32718
  return this.createErrorResponse(choice.problem);
32672
32719
  }
32673
- const held = this.knownHoldOn(choice.endpoint);
32720
+ const held = this.knownHoldOn(choice.endpoint, announced.running);
32674
32721
  if (held !== null) {
32675
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.`, [
32676
32723
  "debug_control continue lets it go, after which this call answers",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.13.40",
3
+ "version": "0.13.41",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",