gdharness 0.10.0 → 0.10.2

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
@@ -155,6 +155,9 @@ function runArguments(options) {
155
155
  if (options.scene !== null) {
156
156
  args.push(`res://${options.scene}`);
157
157
  }
158
+ if (options.userArgs !== undefined && options.userArgs.length > 0) {
159
+ args.push("--", ...options.userArgs);
160
+ }
158
161
  return args;
159
162
  }
160
163
  function editorArguments(projectPath, ports) {
@@ -15680,10 +15683,15 @@ var init_tool_definitions = __esm(() => {
15680
15683
  },
15681
15684
  {
15682
15685
  name: "editor_run",
15683
- description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way.",
15686
+ description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice.",
15684
15687
  parameters: {
15685
15688
  projectPath: PROJECT_PATH,
15686
15689
  scene: { type: "string", description: "A scene to run instead of the main scene." },
15690
+ args: {
15691
+ type: "array",
15692
+ items: { type: "string" },
15693
+ description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
15694
+ },
15687
15695
  headless: { type: "boolean", description: "start: force a window or no window." },
15688
15696
  frames: { type: "number", description: "check: frames to run before quitting. Default 3." },
15689
15697
  timeoutMs: {
@@ -35780,6 +35788,7 @@ import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync a
35780
35788
  import { createConnection as createConnection3 } from "node:net";
35781
35789
  import { tmpdir as tmpdir4 } from "node:os";
35782
35790
  import { join as join13, resolve as resolve4 } from "node:path";
35791
+ import { setTimeout as delay2 } from "node:timers/promises";
35783
35792
  function runtimeDirectory(variables = process.env) {
35784
35793
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
35785
35794
  if (explicit) {
@@ -35868,6 +35877,22 @@ function announcedIn(directory) {
35868
35877
  }
35869
35878
  return found;
35870
35879
  }
35880
+ async function announcedSince(projectPath, before, waiting = {}) {
35881
+ const wanted = resolve4(projectPath);
35882
+ const directories = waiting.directories ?? runtimeDirectories();
35883
+ const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
35884
+ for (;; ) {
35885
+ const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted);
35886
+ if (fresh !== undefined) {
35887
+ return fresh;
35888
+ }
35889
+ const left = until - Date.now();
35890
+ if (left <= 0 || waiting.giveUp?.() === true) {
35891
+ return null;
35892
+ }
35893
+ await delay2(Math.min(LOOK_EVERY_MS, left));
35894
+ }
35895
+ }
35871
35896
  function describe2(endpoint) {
35872
35897
  return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
35873
35898
  }
@@ -35994,7 +36019,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
35994
36019
  });
35995
36020
  });
35996
36021
  }
35997
- var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, nextRequestId = 1;
36022
+ var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
35998
36023
  var init_runtime_client = __esm(() => {
35999
36024
  ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
36000
36025
  });
@@ -36010,13 +36035,17 @@ function installedAddonVersion(projectPath) {
36010
36035
  const said = readFileSync12(marker, "utf8").trim();
36011
36036
  return said === "" ? null : said;
36012
36037
  }
36013
- var ADDONS2, VERSION_MARKER2 = ".gdharness-version";
36038
+ var ADDONS2, RUNTIME_AUTOLOAD2, VERSION_MARKER2 = ".gdharness-version";
36014
36039
  var init_setup = __esm(() => {
36015
36040
  init_class_cache();
36016
36041
  init_headless();
36017
36042
  init_resources();
36018
36043
  init_server_version();
36019
36044
  ADDONS2 = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
36045
+ RUNTIME_AUTOLOAD2 = {
36046
+ name: "GdharnessRuntime",
36047
+ path: "addons/gdharness_runtime/runtime_autoload.gd"
36048
+ };
36020
36049
  });
36021
36050
 
36022
36051
  // src/server.ts
@@ -36024,7 +36053,7 @@ import { execFile as execFile5, spawn } from "node:child_process";
36024
36053
  import { existsSync as existsSync12, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, readFileSync as readFileSync13, realpathSync as realpathSync2, rmSync as rmSync8 } from "node:fs";
36025
36054
  import { tmpdir as tmpdir5 } from "node:os";
36026
36055
  import { basename as basename2, dirname as dirname9, join as join15, normalize as normalize3 } from "node:path";
36027
- import { setTimeout as delay2 } from "node:timers/promises";
36056
+ import { setTimeout as delay3 } from "node:timers/promises";
36028
36057
  import { fileURLToPath as fileURLToPath4 } from "node:url";
36029
36058
  import { promisify as promisify5 } from "node:util";
36030
36059
  function patienceForFrames(frames, atLeast) {
@@ -36755,6 +36784,21 @@ class GodotServer {
36755
36784
  }
36756
36785
  return { ok: true, value: { path, file } };
36757
36786
  }
36787
+ gameArguments(args) {
36788
+ if (args["args"] === undefined) {
36789
+ return { ok: true, value: [] };
36790
+ }
36791
+ const given = readStringArray(args, "args");
36792
+ if (given === undefined) {
36793
+ return {
36794
+ ok: false,
36795
+ response: this.createErrorResponse("args must be a list of strings.", [
36796
+ `Pass the game's own flags, such as ["--level=2"]`
36797
+ ])
36798
+ };
36799
+ }
36800
+ return { ok: true, value: given };
36801
+ }
36758
36802
  async debuggedGame() {
36759
36803
  const game = this.activeProcess;
36760
36804
  if (game !== null && game.exitCode !== null) {
@@ -37341,7 +37385,7 @@ class GodotServer {
37341
37385
  if (Date.now() >= deadline) {
37342
37386
  return false;
37343
37387
  }
37344
- await delay2(250);
37388
+ await delay3(250);
37345
37389
  }
37346
37390
  }
37347
37391
  async handleLaunchEditor(args) {
@@ -37415,6 +37459,10 @@ class GodotServer {
37415
37459
  if (sceneToRun && !sceneToRun.ok) {
37416
37460
  return this.createErrorResponse(sceneToRun.reason, PATH_SOLUTIONS);
37417
37461
  }
37462
+ const given = this.gameArguments(args);
37463
+ if (!given.ok) {
37464
+ return given.response;
37465
+ }
37418
37466
  if (!sceneToRun && !hasMainScene(project.value.file)) {
37419
37467
  return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
37420
37468
  "Pass scene to run one scene",
@@ -37431,37 +37479,62 @@ class GodotServer {
37431
37479
  }
37432
37480
  const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
37433
37481
  if (op === "check") {
37434
- return await this.checkBoot(engine.value, project.value.path, sceneArgument, args);
37482
+ return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
37435
37483
  }
37436
37484
  if (this.activeProcess?.exitCode === null) {
37437
37485
  this.logDebug("Ending the running game before starting another");
37438
37486
  await this.endActiveGame();
37439
37487
  }
37488
+ const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
37440
37489
  const headless = resolveHeadless(args["headless"], {
37441
37490
  platform: process.platform,
37442
37491
  variables: process.env
37443
37492
  });
37444
- if (this.godotBridge.isConnected() && (!headless || editorPlaysHeadless(project.value.file))) {
37445
- return await this.playThroughEditor(sceneArgument, refreshed.value);
37493
+ const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
37494
+ if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0) {
37495
+ return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying);
37446
37496
  }
37447
37497
  const cmdArgs = runArguments({
37448
37498
  projectPath: project.value.path,
37449
37499
  headless,
37450
- scene: sceneArgument
37500
+ scene: sceneArgument,
37501
+ userArgs: given.value
37451
37502
  });
37452
37503
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
37453
37504
  const started = this.spawnGame(engine.value, cmdArgs);
37454
37505
  this.activeProcess = started;
37506
+ 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." : "";
37455
37507
  return this.jsonTextResponse({
37456
37508
  started: true,
37457
37509
  through: "gdharness",
37458
37510
  pid: started.process.pid ?? null,
37459
37511
  arguments: cmdArgs,
37460
37512
  refreshedClasses: refreshed.value,
37461
- message: "Use editor_output for what it prints and editor_run stop to end it."
37513
+ runtime: await this.runtimeUp(project.value.path, alreadyPlaying),
37514
+ message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}`
37515
+ });
37516
+ }
37517
+ async runtimeUp(projectPath, before) {
37518
+ if (!existsSync12(join15(projectPath, RUNTIME_AUTOLOAD2.path))) {
37519
+ return {
37520
+ listening: false,
37521
+ note: "this project has no gdharness_runtime addon, so the runtime_* tools have nothing to talk to"
37522
+ };
37523
+ }
37524
+ const endpoint = await announcedSince(projectPath, before, {
37525
+ giveUp: () => this.dapClient?.isStopped() === true
37462
37526
  });
37527
+ if (endpoint === null) {
37528
+ const held = this.dapClient?.whereItStopped() ?? null;
37529
+ return {
37530
+ listening: false,
37531
+ note: held === null ? `nothing announced itself within ${ANNOUNCE_BUDGET_MS}ms: the game may be slow to boot, or its runtime is not coming up` : "the game stopped before its runtime came up, so the runtime_* tools have nothing to talk to yet: debug_control continue lets it carry on",
37532
+ heldAt: held
37533
+ };
37534
+ }
37535
+ return { listening: true, pid: endpoint.pid, port: endpoint.port };
37463
37536
  }
37464
- async playThroughEditor(scene, refreshedClasses) {
37537
+ async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying) {
37465
37538
  const log = new GameLog;
37466
37539
  try {
37467
37540
  await this.dap().connect();
@@ -37490,6 +37563,7 @@ class GodotServer {
37490
37563
  scene: scene === null ? "the main scene" : `res://${scene}`,
37491
37564
  refreshedClasses,
37492
37565
  debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
37566
+ runtime: await this.runtimeUp(projectPath, alreadyPlaying),
37493
37567
  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."
37494
37568
  });
37495
37569
  }
@@ -37554,10 +37628,10 @@ class GodotServer {
37554
37628
  });
37555
37629
  return started;
37556
37630
  }
37557
- async checkBoot(godotPath, projectPath, scene, args) {
37631
+ async checkBoot(godotPath, projectPath, scene, args, userArgs) {
37558
37632
  const frames = readPositiveNumber(args, "frames") ?? 3;
37559
37633
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
37560
- const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames });
37634
+ const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames, userArgs });
37561
37635
  const boot = this.spawnGame(godotPath, cmdArgs);
37562
37636
  const hung = await new Promise((resolve) => {
37563
37637
  const timer = setTimeout(() => {
package/build/index.js CHANGED
@@ -10129,7 +10129,7 @@ import { execFile as execFile3, spawn } from "node:child_process";
10129
10129
  import { existsSync as existsSync7, mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync9, realpathSync as realpathSync2, rmSync as rmSync4 } from "node:fs";
10130
10130
  import { tmpdir as tmpdir4 } from "node:os";
10131
10131
  import { basename, dirname as dirname4, join as join9, normalize as normalize2 } from "node:path";
10132
- import { setTimeout as delay2 } from "node:timers/promises";
10132
+ import { setTimeout as delay3 } from "node:timers/promises";
10133
10133
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10134
10134
  import { promisify as promisify3 } from "node:util";
10135
10135
 
@@ -26154,6 +26154,9 @@ function runArguments(options) {
26154
26154
  if (options.scene !== null) {
26155
26155
  args.push(`res://${options.scene}`);
26156
26156
  }
26157
+ if (options.userArgs !== undefined && options.userArgs.length > 0) {
26158
+ args.push("--", ...options.userArgs);
26159
+ }
26157
26160
  return args;
26158
26161
  }
26159
26162
  function editorArguments(projectPath, ports) {
@@ -27417,6 +27420,7 @@ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as
27417
27420
  import { createConnection as createConnection3 } from "node:net";
27418
27421
  import { tmpdir as tmpdir3 } from "node:os";
27419
27422
  import { join as join7, resolve as resolve4 } from "node:path";
27423
+ import { setTimeout as delay2 } from "node:timers/promises";
27420
27424
 
27421
27425
  // src/tool-args.ts
27422
27426
  function asParams(value) {
@@ -27560,6 +27564,24 @@ function announcedIn(directory) {
27560
27564
  }
27561
27565
  return found;
27562
27566
  }
27567
+ var LOOK_EVERY_MS = 50;
27568
+ var ANNOUNCE_BUDGET_MS = 5000;
27569
+ async function announcedSince(projectPath, before, waiting = {}) {
27570
+ const wanted = resolve4(projectPath);
27571
+ const directories = waiting.directories ?? runtimeDirectories();
27572
+ const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
27573
+ for (;; ) {
27574
+ const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted);
27575
+ if (fresh !== undefined) {
27576
+ return fresh;
27577
+ }
27578
+ const left = until - Date.now();
27579
+ if (left <= 0 || waiting.giveUp?.() === true) {
27580
+ return null;
27581
+ }
27582
+ await delay2(Math.min(LOOK_EVERY_MS, left));
27583
+ }
27584
+ }
27563
27585
  function describe2(endpoint) {
27564
27586
  return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
27565
27587
  }
@@ -27692,6 +27714,10 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
27692
27714
  import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
27693
27715
  import { dirname as dirname3, join as join8 } from "node:path";
27694
27716
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
27717
+ var RUNTIME_AUTOLOAD = {
27718
+ name: "GdharnessRuntime",
27719
+ path: "addons/gdharness_runtime/runtime_autoload.gd"
27720
+ };
27695
27721
  var VERSION_MARKER = ".gdharness-version";
27696
27722
  function installedAddonVersion(projectPath) {
27697
27723
  const marker = join8(projectPath, "addons", ADDONS[0], VERSION_MARKER);
@@ -28290,10 +28316,15 @@ var TOOL_SPECS = [
28290
28316
  },
28291
28317
  {
28292
28318
  name: "editor_run",
28293
- description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way.",
28319
+ description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice.",
28294
28320
  parameters: {
28295
28321
  projectPath: PROJECT_PATH,
28296
28322
  scene: { type: "string", description: "A scene to run instead of the main scene." },
28323
+ args: {
28324
+ type: "array",
28325
+ items: { type: "string" },
28326
+ description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
28327
+ },
28297
28328
  headless: { type: "boolean", description: "start: force a window or no window." },
28298
28329
  frames: { type: "number", description: "check: frames to run before quitting. Default 3." },
28299
28330
  timeoutMs: {
@@ -29535,6 +29566,21 @@ class GodotServer {
29535
29566
  }
29536
29567
  return { ok: true, value: { path, file } };
29537
29568
  }
29569
+ gameArguments(args) {
29570
+ if (args["args"] === undefined) {
29571
+ return { ok: true, value: [] };
29572
+ }
29573
+ const given = readStringArray(args, "args");
29574
+ if (given === undefined) {
29575
+ return {
29576
+ ok: false,
29577
+ response: this.createErrorResponse("args must be a list of strings.", [
29578
+ `Pass the game's own flags, such as ["--level=2"]`
29579
+ ])
29580
+ };
29581
+ }
29582
+ return { ok: true, value: given };
29583
+ }
29538
29584
  async debuggedGame() {
29539
29585
  const game = this.activeProcess;
29540
29586
  if (game !== null && game.exitCode !== null) {
@@ -30121,7 +30167,7 @@ class GodotServer {
30121
30167
  if (Date.now() >= deadline) {
30122
30168
  return false;
30123
30169
  }
30124
- await delay2(250);
30170
+ await delay3(250);
30125
30171
  }
30126
30172
  }
30127
30173
  async handleLaunchEditor(args) {
@@ -30195,6 +30241,10 @@ class GodotServer {
30195
30241
  if (sceneToRun && !sceneToRun.ok) {
30196
30242
  return this.createErrorResponse(sceneToRun.reason, PATH_SOLUTIONS);
30197
30243
  }
30244
+ const given = this.gameArguments(args);
30245
+ if (!given.ok) {
30246
+ return given.response;
30247
+ }
30198
30248
  if (!sceneToRun && !hasMainScene(project.value.file)) {
30199
30249
  return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
30200
30250
  "Pass scene to run one scene",
@@ -30211,37 +30261,62 @@ class GodotServer {
30211
30261
  }
30212
30262
  const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
30213
30263
  if (op === "check") {
30214
- return await this.checkBoot(engine.value, project.value.path, sceneArgument, args);
30264
+ return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
30215
30265
  }
30216
30266
  if (this.activeProcess?.exitCode === null) {
30217
30267
  this.logDebug("Ending the running game before starting another");
30218
30268
  await this.endActiveGame();
30219
30269
  }
30270
+ const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
30220
30271
  const headless = resolveHeadless(args["headless"], {
30221
30272
  platform: process.platform,
30222
30273
  variables: process.env
30223
30274
  });
30224
- if (this.godotBridge.isConnected() && (!headless || editorPlaysHeadless(project.value.file))) {
30225
- return await this.playThroughEditor(sceneArgument, refreshed.value);
30275
+ const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
30276
+ if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0) {
30277
+ return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying);
30226
30278
  }
30227
30279
  const cmdArgs = runArguments({
30228
30280
  projectPath: project.value.path,
30229
30281
  headless,
30230
- scene: sceneArgument
30282
+ scene: sceneArgument,
30283
+ userArgs: given.value
30231
30284
  });
30232
30285
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
30233
30286
  const started = this.spawnGame(engine.value, cmdArgs);
30234
30287
  this.activeProcess = started;
30288
+ 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." : "";
30235
30289
  return this.jsonTextResponse({
30236
30290
  started: true,
30237
30291
  through: "gdharness",
30238
30292
  pid: started.process.pid ?? null,
30239
30293
  arguments: cmdArgs,
30240
30294
  refreshedClasses: refreshed.value,
30241
- message: "Use editor_output for what it prints and editor_run stop to end it."
30295
+ runtime: await this.runtimeUp(project.value.path, alreadyPlaying),
30296
+ message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}`
30297
+ });
30298
+ }
30299
+ async runtimeUp(projectPath, before) {
30300
+ if (!existsSync7(join9(projectPath, RUNTIME_AUTOLOAD.path))) {
30301
+ return {
30302
+ listening: false,
30303
+ note: "this project has no gdharness_runtime addon, so the runtime_* tools have nothing to talk to"
30304
+ };
30305
+ }
30306
+ const endpoint = await announcedSince(projectPath, before, {
30307
+ giveUp: () => this.dapClient?.isStopped() === true
30242
30308
  });
30309
+ if (endpoint === null) {
30310
+ const held = this.dapClient?.whereItStopped() ?? null;
30311
+ return {
30312
+ listening: false,
30313
+ note: held === null ? `nothing announced itself within ${ANNOUNCE_BUDGET_MS}ms: the game may be slow to boot, or its runtime is not coming up` : "the game stopped before its runtime came up, so the runtime_* tools have nothing to talk to yet: debug_control continue lets it carry on",
30314
+ heldAt: held
30315
+ };
30316
+ }
30317
+ return { listening: true, pid: endpoint.pid, port: endpoint.port };
30243
30318
  }
30244
- async playThroughEditor(scene, refreshedClasses) {
30319
+ async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying) {
30245
30320
  const log = new GameLog;
30246
30321
  try {
30247
30322
  await this.dap().connect();
@@ -30270,6 +30345,7 @@ class GodotServer {
30270
30345
  scene: scene === null ? "the main scene" : `res://${scene}`,
30271
30346
  refreshedClasses,
30272
30347
  debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
30348
+ runtime: await this.runtimeUp(projectPath, alreadyPlaying),
30273
30349
  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."
30274
30350
  });
30275
30351
  }
@@ -30334,10 +30410,10 @@ class GodotServer {
30334
30410
  });
30335
30411
  return started;
30336
30412
  }
30337
- async checkBoot(godotPath, projectPath, scene, args) {
30413
+ async checkBoot(godotPath, projectPath, scene, args, userArgs) {
30338
30414
  const frames = readPositiveNumber(args, "frames") ?? 3;
30339
30415
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
30340
- const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames });
30416
+ const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames, userArgs });
30341
30417
  const boot = this.spawnGame(godotPath, cmdArgs);
30342
30418
  const hung = await new Promise((resolve) => {
30343
30419
  const timer = setTimeout(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
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",