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 +87 -13
- package/build/index.js +87 -11
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
37445
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
30225
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|