gdharness 0.10.1 → 0.11.0
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 +438 -91
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +56 -6
- package/build/index.js +428 -87
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -10126,9 +10126,21 @@ function defectReport(where, error, godotVersion) {
|
|
|
10126
10126
|
|
|
10127
10127
|
// src/server.ts
|
|
10128
10128
|
import { execFile as execFile3, spawn } from "node:child_process";
|
|
10129
|
-
import {
|
|
10129
|
+
import {
|
|
10130
|
+
closeSync,
|
|
10131
|
+
existsSync as existsSync7,
|
|
10132
|
+
fstatSync,
|
|
10133
|
+
mkdirSync as mkdirSync5,
|
|
10134
|
+
mkdtempSync as mkdtempSync2,
|
|
10135
|
+
openSync as openSync2,
|
|
10136
|
+
readdirSync as readdirSync6,
|
|
10137
|
+
readFileSync as readFileSync10,
|
|
10138
|
+
readSync,
|
|
10139
|
+
realpathSync as realpathSync2,
|
|
10140
|
+
rmSync as rmSync5
|
|
10141
|
+
} from "node:fs";
|
|
10130
10142
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10131
|
-
import { basename, dirname as dirname4, join as
|
|
10143
|
+
import { basename, dirname as dirname4, join as join10, normalize as normalize2 } from "node:path";
|
|
10132
10144
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10133
10145
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10134
10146
|
import { promisify as promisify3 } from "node:util";
|
|
@@ -25425,11 +25437,16 @@ function announced(line) {
|
|
|
25425
25437
|
}
|
|
25426
25438
|
return { severity: match[2] === "WARNING" ? "warning" : "error", text: match[3] ?? "" };
|
|
25427
25439
|
}
|
|
25440
|
+
var STREAMS = ["stdout", "stderr", "transcript"];
|
|
25428
25441
|
|
|
25429
25442
|
class GameLog {
|
|
25430
25443
|
entries = [];
|
|
25431
|
-
decoders = {
|
|
25432
|
-
|
|
25444
|
+
decoders = {
|
|
25445
|
+
stdout: new StringDecoder("utf8"),
|
|
25446
|
+
stderr: new StringDecoder("utf8"),
|
|
25447
|
+
transcript: new StringDecoder("utf8")
|
|
25448
|
+
};
|
|
25449
|
+
partial = { stdout: "", stderr: "", transcript: "" };
|
|
25433
25450
|
lastHeadline = null;
|
|
25434
25451
|
cursor = 0;
|
|
25435
25452
|
append(source, chunk) {
|
|
@@ -25442,7 +25459,7 @@ class GameLog {
|
|
|
25442
25459
|
}
|
|
25443
25460
|
}
|
|
25444
25461
|
finish() {
|
|
25445
|
-
for (const source of
|
|
25462
|
+
for (const source of STREAMS) {
|
|
25446
25463
|
const tail = this.partial[source] + this.decoders[source].end();
|
|
25447
25464
|
this.partial[source] = "";
|
|
25448
25465
|
if (tail !== "") {
|
|
@@ -26154,6 +26171,9 @@ function runArguments(options) {
|
|
|
26154
26171
|
if (options.scene !== null) {
|
|
26155
26172
|
args.push(`res://${options.scene}`);
|
|
26156
26173
|
}
|
|
26174
|
+
if (options.userArgs !== undefined && options.userArgs.length > 0) {
|
|
26175
|
+
args.push("--", ...options.userArgs);
|
|
26176
|
+
}
|
|
26157
26177
|
return args;
|
|
26158
26178
|
}
|
|
26159
26179
|
function editorArguments(projectPath, ports) {
|
|
@@ -26601,6 +26621,39 @@ function whyNoReport(printed, asked) {
|
|
|
26601
26621
|
}
|
|
26602
26622
|
return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
|
|
26603
26623
|
}
|
|
26624
|
+
var RUNNING_SUITE = /Run Test Suite:\s*(\S+)/;
|
|
26625
|
+
var ORPHANS = /(\d+)\s+orphans/;
|
|
26626
|
+
var OVERALL = "Overall Summary";
|
|
26627
|
+
function orphansPrinted(printed) {
|
|
26628
|
+
const suites = [];
|
|
26629
|
+
let total = null;
|
|
26630
|
+
let running = null;
|
|
26631
|
+
for (const line of printed) {
|
|
26632
|
+
const started = RUNNING_SUITE.exec(line);
|
|
26633
|
+
if (started?.[1] !== undefined) {
|
|
26634
|
+
running = started[1];
|
|
26635
|
+
continue;
|
|
26636
|
+
}
|
|
26637
|
+
const counted = ORPHANS.exec(line);
|
|
26638
|
+
if (counted?.[1] === undefined) {
|
|
26639
|
+
continue;
|
|
26640
|
+
}
|
|
26641
|
+
const orphans = Number(counted[1]);
|
|
26642
|
+
if (line.includes(OVERALL)) {
|
|
26643
|
+
total = orphans;
|
|
26644
|
+
running = null;
|
|
26645
|
+
continue;
|
|
26646
|
+
}
|
|
26647
|
+
if (running !== null && orphans > 0) {
|
|
26648
|
+
suites.push({ path: running, orphans });
|
|
26649
|
+
}
|
|
26650
|
+
running = null;
|
|
26651
|
+
}
|
|
26652
|
+
return {
|
|
26653
|
+
total: total ?? suites.reduce((sum, suite) => sum + suite.orphans, 0),
|
|
26654
|
+
suites
|
|
26655
|
+
};
|
|
26656
|
+
}
|
|
26604
26657
|
|
|
26605
26658
|
// src/lsp_client.ts
|
|
26606
26659
|
import { realpathSync } from "node:fs";
|
|
@@ -26905,6 +26958,14 @@ class GodotLSPClient {
|
|
|
26905
26958
|
}
|
|
26906
26959
|
return uri;
|
|
26907
26960
|
}
|
|
26961
|
+
closeDocument(uri) {
|
|
26962
|
+
if (!this.documentVersions.delete(uri)) {
|
|
26963
|
+
return;
|
|
26964
|
+
}
|
|
26965
|
+
try {
|
|
26966
|
+
this.sendNotification("textDocument/didClose", { textDocument: { uri } });
|
|
26967
|
+
} catch {}
|
|
26968
|
+
}
|
|
26908
26969
|
async initialize(rootPath) {
|
|
26909
26970
|
await this.ensureConnected();
|
|
26910
26971
|
const resolvedRootPath = resolve2(rootPath);
|
|
@@ -26958,8 +27019,9 @@ class GodotLSPClient {
|
|
|
26958
27019
|
timer
|
|
26959
27020
|
});
|
|
26960
27021
|
});
|
|
27022
|
+
let opened = null;
|
|
26961
27023
|
try {
|
|
26962
|
-
this.syncDocument(filePath, content);
|
|
27024
|
+
opened = this.syncDocument(filePath, content);
|
|
26963
27025
|
return await diagnosticsPromise;
|
|
26964
27026
|
} catch (error) {
|
|
26965
27027
|
const waiter = this.diagnosticsWaiters.get(key);
|
|
@@ -26968,16 +27030,25 @@ class GodotLSPClient {
|
|
|
26968
27030
|
this.diagnosticsWaiters.delete(key);
|
|
26969
27031
|
}
|
|
26970
27032
|
throw error;
|
|
27033
|
+
} finally {
|
|
27034
|
+
if (opened !== null) {
|
|
27035
|
+
this.closeDocument(opened);
|
|
27036
|
+
}
|
|
26971
27037
|
}
|
|
26972
27038
|
}
|
|
26973
27039
|
async getCompletions(filePath, content, line, character) {
|
|
26974
27040
|
await this.ensureConnected();
|
|
26975
27041
|
await this.ensureInitializedForFile(filePath);
|
|
26976
27042
|
const uri = this.syncDocument(filePath, content);
|
|
26977
|
-
|
|
26978
|
-
|
|
26979
|
-
|
|
26980
|
-
|
|
27043
|
+
let result;
|
|
27044
|
+
try {
|
|
27045
|
+
result = await this.sendRequest("textDocument/completion", {
|
|
27046
|
+
textDocument: { uri },
|
|
27047
|
+
position: { line, character }
|
|
27048
|
+
});
|
|
27049
|
+
} finally {
|
|
27050
|
+
this.closeDocument(uri);
|
|
27051
|
+
}
|
|
26981
27052
|
if (Array.isArray(result)) {
|
|
26982
27053
|
return result;
|
|
26983
27054
|
}
|
|
@@ -26993,18 +27064,27 @@ class GodotLSPClient {
|
|
|
26993
27064
|
await this.ensureConnected();
|
|
26994
27065
|
await this.ensureInitializedForFile(filePath);
|
|
26995
27066
|
const uri = this.syncDocument(filePath, content);
|
|
26996
|
-
|
|
26997
|
-
textDocument
|
|
26998
|
-
|
|
26999
|
-
|
|
27067
|
+
try {
|
|
27068
|
+
return await this.sendRequest("textDocument/hover", {
|
|
27069
|
+
textDocument: { uri },
|
|
27070
|
+
position: { line, character }
|
|
27071
|
+
});
|
|
27072
|
+
} finally {
|
|
27073
|
+
this.closeDocument(uri);
|
|
27074
|
+
}
|
|
27000
27075
|
}
|
|
27001
27076
|
async getDocumentSymbols(filePath, content) {
|
|
27002
27077
|
await this.ensureConnected();
|
|
27003
27078
|
await this.ensureInitializedForFile(filePath);
|
|
27004
27079
|
const uri = this.syncDocument(filePath, content);
|
|
27005
|
-
|
|
27006
|
-
|
|
27007
|
-
|
|
27080
|
+
let result;
|
|
27081
|
+
try {
|
|
27082
|
+
result = await this.sendRequest("textDocument/documentSymbol", {
|
|
27083
|
+
textDocument: { uri }
|
|
27084
|
+
});
|
|
27085
|
+
} finally {
|
|
27086
|
+
this.closeDocument(uri);
|
|
27087
|
+
}
|
|
27008
27088
|
return Array.isArray(result) ? result : [];
|
|
27009
27089
|
}
|
|
27010
27090
|
isConnected() {
|
|
@@ -27412,6 +27492,10 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
27412
27492
|
});
|
|
27413
27493
|
}
|
|
27414
27494
|
|
|
27495
|
+
// src/run-record.ts
|
|
27496
|
+
import { mkdirSync as mkdirSync3, openSync, readdirSync as readdirSync5, readFileSync as readFileSync8, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
27497
|
+
import { join as join8 } from "node:path";
|
|
27498
|
+
|
|
27415
27499
|
// src/runtime-client.ts
|
|
27416
27500
|
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync7, unlinkSync } from "node:fs";
|
|
27417
27501
|
import { createConnection as createConnection3 } from "node:net";
|
|
@@ -27707,9 +27791,86 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
27707
27791
|
});
|
|
27708
27792
|
}
|
|
27709
27793
|
|
|
27794
|
+
// src/run-record.ts
|
|
27795
|
+
function runsDirectory() {
|
|
27796
|
+
return join8(runtimeDirectory(), "runs");
|
|
27797
|
+
}
|
|
27798
|
+
function recordPath() {
|
|
27799
|
+
return join8(runsDirectory(), "run.json");
|
|
27800
|
+
}
|
|
27801
|
+
function openTranscript(startedAt) {
|
|
27802
|
+
mkdirSync3(runsDirectory(), { recursive: true });
|
|
27803
|
+
const path = join8(runsDirectory(), `run-${startedAt}.log`);
|
|
27804
|
+
return { path, fd: openSync(path, "a") };
|
|
27805
|
+
}
|
|
27806
|
+
function writeRunRecord(record) {
|
|
27807
|
+
mkdirSync3(runsDirectory(), { recursive: true });
|
|
27808
|
+
writeFileSync4(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
27809
|
+
}
|
|
27810
|
+
function readRunRecord() {
|
|
27811
|
+
let parsed;
|
|
27812
|
+
try {
|
|
27813
|
+
parsed = JSON.parse(readFileSync8(recordPath(), "utf8"));
|
|
27814
|
+
} catch {
|
|
27815
|
+
return null;
|
|
27816
|
+
}
|
|
27817
|
+
if (!parsed || typeof parsed !== "object") {
|
|
27818
|
+
return null;
|
|
27819
|
+
}
|
|
27820
|
+
const fields = parsed;
|
|
27821
|
+
const pid = fields["pid"];
|
|
27822
|
+
const transcript = fields["transcript"];
|
|
27823
|
+
const startedAt = fields["startedAt"];
|
|
27824
|
+
if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
27825
|
+
return null;
|
|
27826
|
+
}
|
|
27827
|
+
const args = fields["arguments"];
|
|
27828
|
+
return {
|
|
27829
|
+
pid,
|
|
27830
|
+
transcript,
|
|
27831
|
+
startedAt,
|
|
27832
|
+
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
27833
|
+
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : []
|
|
27834
|
+
};
|
|
27835
|
+
}
|
|
27836
|
+
function clearRunRecord() {
|
|
27837
|
+
try {
|
|
27838
|
+
rmSync3(recordPath());
|
|
27839
|
+
} catch {}
|
|
27840
|
+
}
|
|
27841
|
+
function ageMs(path, now) {
|
|
27842
|
+
try {
|
|
27843
|
+
return now - statSync2(path).mtimeMs;
|
|
27844
|
+
} catch {
|
|
27845
|
+
return 0;
|
|
27846
|
+
}
|
|
27847
|
+
}
|
|
27848
|
+
function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
27849
|
+
const directory = runsDirectory();
|
|
27850
|
+
const keep = readRunRecord()?.transcript;
|
|
27851
|
+
let entries;
|
|
27852
|
+
try {
|
|
27853
|
+
entries = readdirSync5(directory);
|
|
27854
|
+
} catch {
|
|
27855
|
+
return;
|
|
27856
|
+
}
|
|
27857
|
+
for (const entry of entries) {
|
|
27858
|
+
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
27859
|
+
continue;
|
|
27860
|
+
}
|
|
27861
|
+
const path = join8(directory, entry);
|
|
27862
|
+
if (path === keep || ageMs(path, now) < keepMs) {
|
|
27863
|
+
continue;
|
|
27864
|
+
}
|
|
27865
|
+
try {
|
|
27866
|
+
rmSync3(path);
|
|
27867
|
+
} catch {}
|
|
27868
|
+
}
|
|
27869
|
+
}
|
|
27870
|
+
|
|
27710
27871
|
// src/setup.ts
|
|
27711
|
-
import { cpSync, existsSync as existsSync6, mkdirSync as
|
|
27712
|
-
import { dirname as dirname3, join as
|
|
27872
|
+
import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync9, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
27873
|
+
import { dirname as dirname3, join as join9 } from "node:path";
|
|
27713
27874
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
27714
27875
|
var RUNTIME_AUTOLOAD = {
|
|
27715
27876
|
name: "GdharnessRuntime",
|
|
@@ -27717,11 +27878,11 @@ var RUNTIME_AUTOLOAD = {
|
|
|
27717
27878
|
};
|
|
27718
27879
|
var VERSION_MARKER = ".gdharness-version";
|
|
27719
27880
|
function installedAddonVersion(projectPath) {
|
|
27720
|
-
const marker =
|
|
27881
|
+
const marker = join9(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
27721
27882
|
if (!existsSync6(marker)) {
|
|
27722
27883
|
return null;
|
|
27723
27884
|
}
|
|
27724
|
-
const said =
|
|
27885
|
+
const said = readFileSync9(marker, "utf8").trim();
|
|
27725
27886
|
return said === "" ? null : said;
|
|
27726
27887
|
}
|
|
27727
27888
|
|
|
@@ -28038,7 +28199,7 @@ var TOOL_SPECS = [
|
|
|
28038
28199
|
},
|
|
28039
28200
|
{
|
|
28040
28201
|
name: "project_test",
|
|
28041
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. Needs gdUnit4 under addons/gdUnit4.",
|
|
28202
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
|
|
28042
28203
|
parameters: {
|
|
28043
28204
|
projectPath: PROJECT_PATH,
|
|
28044
28205
|
path: {
|
|
@@ -28313,10 +28474,15 @@ var TOOL_SPECS = [
|
|
|
28313
28474
|
},
|
|
28314
28475
|
{
|
|
28315
28476
|
name: "editor_run",
|
|
28316
|
-
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. 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.",
|
|
28477
|
+
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.",
|
|
28317
28478
|
parameters: {
|
|
28318
28479
|
projectPath: PROJECT_PATH,
|
|
28319
28480
|
scene: { type: "string", description: "A scene to run instead of the main scene." },
|
|
28481
|
+
args: {
|
|
28482
|
+
type: "array",
|
|
28483
|
+
items: { type: "string" },
|
|
28484
|
+
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.`
|
|
28485
|
+
},
|
|
28320
28486
|
headless: { type: "boolean", description: "start: force a window or no window." },
|
|
28321
28487
|
frames: { type: "number", description: "check: frames to run before quitting. Default 3." },
|
|
28322
28488
|
timeoutMs: {
|
|
@@ -28445,8 +28611,8 @@ var TOOL_SPECS = [
|
|
|
28445
28611
|
},
|
|
28446
28612
|
includeHidden: {
|
|
28447
28613
|
type: "boolean",
|
|
28448
|
-
ops: ["text"],
|
|
28449
|
-
description: "text: read hidden nodes as well, for checking that something is not showing. Default false."
|
|
28614
|
+
ops: ["text", "find"],
|
|
28615
|
+
description: "text: read hidden nodes as well, for checking that something is not showing. Default false. find: default true, since a find means the node whether or not it is drawn; false answers with what the player can actually see, which is how a panel that keeps a label for every line and hides the ones that do not is read. A node counts as hidden when anything above it is, and how many matches were left out comes back under hidden."
|
|
28450
28616
|
},
|
|
28451
28617
|
metrics: {
|
|
28452
28618
|
type: "array",
|
|
@@ -28463,7 +28629,7 @@ var TOOL_SPECS = [
|
|
|
28463
28629
|
requires: []
|
|
28464
28630
|
},
|
|
28465
28631
|
find: {
|
|
28466
|
-
summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each",
|
|
28632
|
+
summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each, hidden ones included unless includeHidden says otherwise",
|
|
28467
28633
|
requires: []
|
|
28468
28634
|
},
|
|
28469
28635
|
rect: {
|
|
@@ -28771,15 +28937,24 @@ var SLOWEST_FRAME_RATE = 20;
|
|
|
28771
28937
|
function patienceForFrames(frames, atLeast) {
|
|
28772
28938
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
28773
28939
|
}
|
|
28940
|
+
function stillRunning(run) {
|
|
28941
|
+
if (run?.exitCode !== null || run.endedUnwatched === true) {
|
|
28942
|
+
return false;
|
|
28943
|
+
}
|
|
28944
|
+
if (!run.throughEditor && run.process === null && run.pid !== null) {
|
|
28945
|
+
return alive(run.pid);
|
|
28946
|
+
}
|
|
28947
|
+
return true;
|
|
28948
|
+
}
|
|
28774
28949
|
function alive(pid) {
|
|
28775
|
-
if (pid === undefined) {
|
|
28950
|
+
if (pid === undefined || pid === null) {
|
|
28776
28951
|
return false;
|
|
28777
28952
|
}
|
|
28778
28953
|
try {
|
|
28779
28954
|
process.kill(pid, 0);
|
|
28780
28955
|
return true;
|
|
28781
|
-
} catch {
|
|
28782
|
-
return
|
|
28956
|
+
} catch (error) {
|
|
28957
|
+
return error.code === "EPERM";
|
|
28783
28958
|
}
|
|
28784
28959
|
}
|
|
28785
28960
|
function isOfType(value, kind) {
|
|
@@ -28921,13 +29096,13 @@ function aboveTheRunner(entry) {
|
|
|
28921
29096
|
}
|
|
28922
29097
|
function elsewhereIn(projectPath, asked) {
|
|
28923
29098
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
28924
|
-
if (wanted === "" || existsSync7(
|
|
29099
|
+
if (wanted === "" || existsSync7(join10(projectPath, wanted))) {
|
|
28925
29100
|
return "";
|
|
28926
29101
|
}
|
|
28927
29102
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
28928
29103
|
let near = [];
|
|
28929
29104
|
try {
|
|
28930
|
-
near =
|
|
29105
|
+
near = readdirSync6(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
|
|
28931
29106
|
} catch {
|
|
28932
29107
|
return "";
|
|
28933
29108
|
}
|
|
@@ -28935,11 +29110,11 @@ function elsewhereIn(projectPath, asked) {
|
|
|
28935
29110
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
28936
29111
|
}
|
|
28937
29112
|
function hasMainScene(projectFile) {
|
|
28938
|
-
const scene = parseProjectGodot(
|
|
29113
|
+
const scene = parseProjectGodot(readFileSync10(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
28939
29114
|
return typeof scene === "string" && scene !== "";
|
|
28940
29115
|
}
|
|
28941
29116
|
function editorPlaysHeadless(projectFile) {
|
|
28942
|
-
const runArgs = parseProjectGodot(
|
|
29117
|
+
const runArgs = parseProjectGodot(readFileSync10(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
28943
29118
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
28944
29119
|
}
|
|
28945
29120
|
function camelCased(params) {
|
|
@@ -28955,7 +29130,7 @@ function camelCased(params) {
|
|
|
28955
29130
|
class GodotServer {
|
|
28956
29131
|
mcp;
|
|
28957
29132
|
locator = new GodotLocator;
|
|
28958
|
-
operationsScript =
|
|
29133
|
+
operationsScript = join10(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
28959
29134
|
godotBridge;
|
|
28960
29135
|
tools = buildToolDefinitions();
|
|
28961
29136
|
activeProcess = null;
|
|
@@ -29112,10 +29287,7 @@ class GodotServer {
|
|
|
29112
29287
|
this.stopWatchingForASuccessor();
|
|
29113
29288
|
withdrawBridge(this.announcedAt);
|
|
29114
29289
|
this.announcedAt = null;
|
|
29115
|
-
|
|
29116
|
-
this.activeProcess.process?.kill();
|
|
29117
|
-
this.activeProcess = null;
|
|
29118
|
-
}
|
|
29290
|
+
this.activeProcess = null;
|
|
29119
29291
|
if (this.lspClient) {
|
|
29120
29292
|
try {
|
|
29121
29293
|
await this.lspClient.disconnect();
|
|
@@ -29180,14 +29352,7 @@ class GodotServer {
|
|
|
29180
29352
|
return;
|
|
29181
29353
|
}
|
|
29182
29354
|
this.shutdownInitiated = true;
|
|
29183
|
-
|
|
29184
|
-
try {
|
|
29185
|
-
this.activeProcess.process?.kill();
|
|
29186
|
-
} catch (error) {
|
|
29187
|
-
console.error("[SERVER] Failed to kill the Godot process on exit:", errorMessage(error));
|
|
29188
|
-
}
|
|
29189
|
-
this.activeProcess = null;
|
|
29190
|
-
}
|
|
29355
|
+
this.activeProcess = null;
|
|
29191
29356
|
}
|
|
29192
29357
|
setupToolHandlers() {
|
|
29193
29358
|
this.mcp.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: this.tools }));
|
|
@@ -29547,7 +29712,7 @@ class GodotServer {
|
|
|
29547
29712
|
if (path === undefined) {
|
|
29548
29713
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
29549
29714
|
}
|
|
29550
|
-
const file =
|
|
29715
|
+
const file = join10(path, "project.godot");
|
|
29551
29716
|
if (!existsSync7(file)) {
|
|
29552
29717
|
return {
|
|
29553
29718
|
ok: false,
|
|
@@ -29558,6 +29723,21 @@ class GodotServer {
|
|
|
29558
29723
|
}
|
|
29559
29724
|
return { ok: true, value: { path, file } };
|
|
29560
29725
|
}
|
|
29726
|
+
gameArguments(args) {
|
|
29727
|
+
if (args["args"] === undefined) {
|
|
29728
|
+
return { ok: true, value: [] };
|
|
29729
|
+
}
|
|
29730
|
+
const given = readStringArray(args, "args");
|
|
29731
|
+
if (given === undefined) {
|
|
29732
|
+
return {
|
|
29733
|
+
ok: false,
|
|
29734
|
+
response: this.createErrorResponse("args must be a list of strings.", [
|
|
29735
|
+
`Pass the game's own flags, such as ["--level=2"]`
|
|
29736
|
+
])
|
|
29737
|
+
};
|
|
29738
|
+
}
|
|
29739
|
+
return { ok: true, value: given };
|
|
29740
|
+
}
|
|
29561
29741
|
async debuggedGame() {
|
|
29562
29742
|
const game = this.activeProcess;
|
|
29563
29743
|
if (game !== null && game.exitCode !== null) {
|
|
@@ -29702,7 +29882,7 @@ class GodotServer {
|
|
|
29702
29882
|
if (!engine.ok) {
|
|
29703
29883
|
return engine.response;
|
|
29704
29884
|
}
|
|
29705
|
-
const application = parseProjectGodot(
|
|
29885
|
+
const application = parseProjectGodot(readFileSync10(project.value.file, "utf8"))["application"] ?? {};
|
|
29706
29886
|
const name = application["config/name"];
|
|
29707
29887
|
const mainScene = application["run/main_scene"];
|
|
29708
29888
|
const info = {
|
|
@@ -29765,7 +29945,7 @@ class GodotServer {
|
|
|
29765
29945
|
}
|
|
29766
29946
|
const folder = dirname4(output.absolutePath);
|
|
29767
29947
|
try {
|
|
29768
|
-
|
|
29948
|
+
mkdirSync5(folder, { recursive: true });
|
|
29769
29949
|
} catch (error) {
|
|
29770
29950
|
return this.createErrorResponse(`Cannot create ${dirname4(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
29771
29951
|
}
|
|
@@ -29827,7 +30007,7 @@ class GodotServer {
|
|
|
29827
30007
|
return contained.response;
|
|
29828
30008
|
}
|
|
29829
30009
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
29830
|
-
if (!existsSync7(
|
|
30010
|
+
if (!existsSync7(join10(project.value.path, runner))) {
|
|
29831
30011
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
29832
30012
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
29833
30013
|
]);
|
|
@@ -29861,7 +30041,7 @@ class GodotServer {
|
|
|
29861
30041
|
];
|
|
29862
30042
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
29863
30043
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
29864
|
-
const userData = mkdtempSync2(
|
|
30044
|
+
const userData = mkdtempSync2(join10(tmpdir4(), "gdharness-tests-"));
|
|
29865
30045
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
29866
30046
|
const hung = await new Promise((resolve) => {
|
|
29867
30047
|
const timer = setTimeout(() => {
|
|
@@ -29877,26 +30057,26 @@ class GodotServer {
|
|
|
29877
30057
|
resolve(false);
|
|
29878
30058
|
});
|
|
29879
30059
|
});
|
|
29880
|
-
const reportsDir =
|
|
30060
|
+
const reportsDir = join10(project.value.path, ".godot", "gdharness-reports");
|
|
29881
30061
|
let report = null;
|
|
29882
30062
|
let reportProblem = null;
|
|
29883
30063
|
try {
|
|
29884
|
-
const written = existsSync7(reportsDir) ?
|
|
30064
|
+
const written = existsSync7(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
29885
30065
|
const newest = written.at(-1);
|
|
29886
30066
|
if (newest !== undefined) {
|
|
29887
|
-
report = parseJUnit(
|
|
30067
|
+
report = parseJUnit(readFileSync10(join10(reportsDir, newest, "results.xml"), "utf8"));
|
|
29888
30068
|
}
|
|
29889
30069
|
} catch (error) {
|
|
29890
30070
|
reportProblem = errorMessage(error);
|
|
29891
30071
|
} finally {
|
|
29892
|
-
|
|
29893
|
-
|
|
30072
|
+
rmSync5(reportsDir, { recursive: true, force: true });
|
|
30073
|
+
rmSync5(userData, { recursive: true, force: true });
|
|
29894
30074
|
}
|
|
29895
30075
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
29896
30076
|
const verdicts = {
|
|
29897
30077
|
0: "passed",
|
|
29898
30078
|
100: "failures",
|
|
29899
|
-
101: "warnings",
|
|
30079
|
+
101: "warnings: orphan nodes",
|
|
29900
30080
|
103: "headless not supported by this gdUnit4",
|
|
29901
30081
|
104: "Godot version not supported by this gdUnit4",
|
|
29902
30082
|
105: "script errors"
|
|
@@ -29928,7 +30108,17 @@ class GodotServer {
|
|
|
29928
30108
|
};
|
|
29929
30109
|
}
|
|
29930
30110
|
const failed = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path })));
|
|
29931
|
-
const
|
|
30111
|
+
const orphans = orphansPrinted(said);
|
|
30112
|
+
const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
|
|
30113
|
+
const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
|
|
30114
|
+
const warnings = orphans.suites.map((suite) => ({
|
|
30115
|
+
kind: "orphans",
|
|
30116
|
+
path: suite.path,
|
|
30117
|
+
name: report.suites.find((entry) => entry.path === suite.path)?.name ?? suite.path,
|
|
30118
|
+
orphans: suite.orphans,
|
|
30119
|
+
message: `${suite.orphans} node${suite.orphans === 1 ? "" : "s"} were still in the tree when this suite finished. A node built in a test and neither freed nor added to the tree is the usual cause; free it, or add it with auto_free().`
|
|
30120
|
+
}));
|
|
30121
|
+
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
|
|
29932
30122
|
return this.jsonTextResponse({
|
|
29933
30123
|
passed: !hung && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
29934
30124
|
verdict,
|
|
@@ -29939,6 +30129,9 @@ class GodotServer {
|
|
|
29939
30129
|
skipped: report.skipped,
|
|
29940
30130
|
time: report.time,
|
|
29941
30131
|
failed,
|
|
30132
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
30133
|
+
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
30134
|
+
note: verdict.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : undefined,
|
|
29942
30135
|
suites: unclean.map((suite) => ({
|
|
29943
30136
|
name: suite.name,
|
|
29944
30137
|
path: suite.path,
|
|
@@ -29946,6 +30139,7 @@ class GodotServer {
|
|
|
29946
30139
|
failures: suite.failures,
|
|
29947
30140
|
errors: suite.errors,
|
|
29948
30141
|
skipped: suite.skipped,
|
|
30142
|
+
orphans: orphansIn(suite.path) > 0 ? orphansIn(suite.path) : undefined,
|
|
29949
30143
|
time: suite.time
|
|
29950
30144
|
})),
|
|
29951
30145
|
suitesPassed: report.suites.length - unclean.length,
|
|
@@ -30072,7 +30266,7 @@ class GodotServer {
|
|
|
30072
30266
|
version: godotPath === null ? null : await this.godotVersion(godotPath)
|
|
30073
30267
|
},
|
|
30074
30268
|
game: {
|
|
30075
|
-
processActive: this.
|
|
30269
|
+
processActive: stillRunning(this.currentRun()),
|
|
30076
30270
|
playingInEditor: playing,
|
|
30077
30271
|
runtimeConnected: games.some((game) => game.reachable),
|
|
30078
30272
|
runtimes: games
|
|
@@ -30218,6 +30412,10 @@ class GodotServer {
|
|
|
30218
30412
|
if (sceneToRun && !sceneToRun.ok) {
|
|
30219
30413
|
return this.createErrorResponse(sceneToRun.reason, PATH_SOLUTIONS);
|
|
30220
30414
|
}
|
|
30415
|
+
const given = this.gameArguments(args);
|
|
30416
|
+
if (!given.ok) {
|
|
30417
|
+
return given.response;
|
|
30418
|
+
}
|
|
30221
30419
|
if (!sceneToRun && !hasMainScene(project.value.file)) {
|
|
30222
30420
|
return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
|
|
30223
30421
|
"Pass scene to run one scene",
|
|
@@ -30234,9 +30432,9 @@ class GodotServer {
|
|
|
30234
30432
|
}
|
|
30235
30433
|
const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
|
|
30236
30434
|
if (op === "check") {
|
|
30237
|
-
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args);
|
|
30435
|
+
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
|
|
30238
30436
|
}
|
|
30239
|
-
if (this.
|
|
30437
|
+
if (stillRunning(this.currentRun())) {
|
|
30240
30438
|
this.logDebug("Ending the running game before starting another");
|
|
30241
30439
|
await this.endActiveGame();
|
|
30242
30440
|
}
|
|
@@ -30245,17 +30443,20 @@ class GodotServer {
|
|
|
30245
30443
|
platform: process.platform,
|
|
30246
30444
|
variables: process.env
|
|
30247
30445
|
});
|
|
30248
|
-
|
|
30446
|
+
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
30447
|
+
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0) {
|
|
30249
30448
|
return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying);
|
|
30250
30449
|
}
|
|
30251
30450
|
const cmdArgs = runArguments({
|
|
30252
30451
|
projectPath: project.value.path,
|
|
30253
30452
|
headless,
|
|
30254
|
-
scene: sceneArgument
|
|
30453
|
+
scene: sceneArgument,
|
|
30454
|
+
userArgs: given.value
|
|
30255
30455
|
});
|
|
30256
30456
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
30257
|
-
const started = this.
|
|
30457
|
+
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
|
|
30258
30458
|
this.activeProcess = started;
|
|
30459
|
+
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." : "";
|
|
30259
30460
|
return this.jsonTextResponse({
|
|
30260
30461
|
started: true,
|
|
30261
30462
|
through: "gdharness",
|
|
@@ -30263,11 +30464,11 @@ class GodotServer {
|
|
|
30263
30464
|
arguments: cmdArgs,
|
|
30264
30465
|
refreshedClasses: refreshed.value,
|
|
30265
30466
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying),
|
|
30266
|
-
message:
|
|
30467
|
+
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}`
|
|
30267
30468
|
});
|
|
30268
30469
|
}
|
|
30269
30470
|
async runtimeUp(projectPath, before) {
|
|
30270
|
-
if (!existsSync7(
|
|
30471
|
+
if (!existsSync7(join10(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
30271
30472
|
return {
|
|
30272
30473
|
listening: false,
|
|
30273
30474
|
note: "this project has no gdharness_runtime addon, so the runtime_* tools have nothing to talk to"
|
|
@@ -30302,7 +30503,11 @@ class GodotServer {
|
|
|
30302
30503
|
}
|
|
30303
30504
|
const played = {
|
|
30304
30505
|
process: null,
|
|
30506
|
+
pid: null,
|
|
30305
30507
|
log,
|
|
30508
|
+
transcript: null,
|
|
30509
|
+
readOffset: 0,
|
|
30510
|
+
projectPath,
|
|
30306
30511
|
startedAt: Date.now(),
|
|
30307
30512
|
exitCode: null,
|
|
30308
30513
|
throughEditor: true,
|
|
@@ -30341,6 +30546,7 @@ class GodotServer {
|
|
|
30341
30546
|
async endActiveGame() {
|
|
30342
30547
|
const running = this.activeProcess;
|
|
30343
30548
|
this.activeProcess = null;
|
|
30549
|
+
clearRunRecord();
|
|
30344
30550
|
if (!running) {
|
|
30345
30551
|
return;
|
|
30346
30552
|
}
|
|
@@ -30348,14 +30554,26 @@ class GodotServer {
|
|
|
30348
30554
|
await this.handleViaBridge("stop_playing", {});
|
|
30349
30555
|
return;
|
|
30350
30556
|
}
|
|
30351
|
-
running.process
|
|
30557
|
+
if (running.process !== null) {
|
|
30558
|
+
running.process.kill();
|
|
30559
|
+
return;
|
|
30560
|
+
}
|
|
30561
|
+
if (running.pid !== null) {
|
|
30562
|
+
try {
|
|
30563
|
+
process.kill(running.pid);
|
|
30564
|
+
} catch {}
|
|
30565
|
+
}
|
|
30352
30566
|
}
|
|
30353
30567
|
spawnGame(godotPath, cmdArgs, env) {
|
|
30354
30568
|
const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
30355
30569
|
const log = new GameLog;
|
|
30356
30570
|
const started = {
|
|
30357
30571
|
process: child,
|
|
30572
|
+
pid: child.pid ?? null,
|
|
30358
30573
|
log,
|
|
30574
|
+
transcript: null,
|
|
30575
|
+
readOffset: 0,
|
|
30576
|
+
projectPath: null,
|
|
30359
30577
|
startedAt: Date.now(),
|
|
30360
30578
|
exitCode: null,
|
|
30361
30579
|
throughEditor: false,
|
|
@@ -30380,10 +30598,121 @@ class GodotServer {
|
|
|
30380
30598
|
});
|
|
30381
30599
|
return started;
|
|
30382
30600
|
}
|
|
30383
|
-
|
|
30601
|
+
spawnKeptGame(godotPath, cmdArgs, projectPath, env) {
|
|
30602
|
+
const startedAt = Date.now();
|
|
30603
|
+
const transcript = openTranscript(startedAt);
|
|
30604
|
+
let child;
|
|
30605
|
+
try {
|
|
30606
|
+
child = spawn(godotPath, cmdArgs, {
|
|
30607
|
+
stdio: ["ignore", transcript.fd, transcript.fd],
|
|
30608
|
+
detached: true,
|
|
30609
|
+
...env ? { env } : {}
|
|
30610
|
+
});
|
|
30611
|
+
} finally {
|
|
30612
|
+
closeSync(transcript.fd);
|
|
30613
|
+
}
|
|
30614
|
+
child.unref();
|
|
30615
|
+
const log = new GameLog;
|
|
30616
|
+
const started = {
|
|
30617
|
+
process: child,
|
|
30618
|
+
pid: child.pid ?? null,
|
|
30619
|
+
log,
|
|
30620
|
+
transcript: transcript.path,
|
|
30621
|
+
readOffset: 0,
|
|
30622
|
+
projectPath,
|
|
30623
|
+
startedAt,
|
|
30624
|
+
exitCode: null,
|
|
30625
|
+
throughEditor: false,
|
|
30626
|
+
brokeOn: null
|
|
30627
|
+
};
|
|
30628
|
+
child.on("exit", (code) => {
|
|
30629
|
+
this.logDebug(`Godot process exited with code ${code ?? "none"}`);
|
|
30630
|
+
this.drainTranscript(started);
|
|
30631
|
+
log.finish();
|
|
30632
|
+
started.exitCode = code ?? -1;
|
|
30633
|
+
});
|
|
30634
|
+
child.on("error", (err) => {
|
|
30635
|
+
console.error("Failed to start Godot process:", err);
|
|
30636
|
+
log.append("stderr", `${err.message}
|
|
30637
|
+
`);
|
|
30638
|
+
started.exitCode = -1;
|
|
30639
|
+
});
|
|
30640
|
+
if (started.pid !== null) {
|
|
30641
|
+
writeRunRecord({
|
|
30642
|
+
pid: started.pid,
|
|
30643
|
+
transcript: transcript.path,
|
|
30644
|
+
startedAt,
|
|
30645
|
+
projectPath,
|
|
30646
|
+
arguments: cmdArgs
|
|
30647
|
+
});
|
|
30648
|
+
}
|
|
30649
|
+
sweepTranscripts();
|
|
30650
|
+
return started;
|
|
30651
|
+
}
|
|
30652
|
+
drainTranscript(game) {
|
|
30653
|
+
if (game.transcript === null) {
|
|
30654
|
+
return;
|
|
30655
|
+
}
|
|
30656
|
+
let handle;
|
|
30657
|
+
try {
|
|
30658
|
+
handle = openSync2(game.transcript, "r");
|
|
30659
|
+
} catch {
|
|
30660
|
+
return;
|
|
30661
|
+
}
|
|
30662
|
+
try {
|
|
30663
|
+
const size = fstatSync(handle).size;
|
|
30664
|
+
if (size <= game.readOffset) {
|
|
30665
|
+
return;
|
|
30666
|
+
}
|
|
30667
|
+
const buffer = Buffer.alloc(size - game.readOffset);
|
|
30668
|
+
const read = readSync(handle, buffer, 0, buffer.length, game.readOffset);
|
|
30669
|
+
game.readOffset += read;
|
|
30670
|
+
game.log.append("transcript", buffer.subarray(0, read));
|
|
30671
|
+
} finally {
|
|
30672
|
+
closeSync(handle);
|
|
30673
|
+
}
|
|
30674
|
+
}
|
|
30675
|
+
adoptRecordedRun() {
|
|
30676
|
+
const record = readRunRecord();
|
|
30677
|
+
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
30678
|
+
return null;
|
|
30679
|
+
}
|
|
30680
|
+
const adopted = {
|
|
30681
|
+
process: null,
|
|
30682
|
+
pid: record.pid,
|
|
30683
|
+
log: new GameLog,
|
|
30684
|
+
transcript: record.transcript,
|
|
30685
|
+
readOffset: 0,
|
|
30686
|
+
projectPath: record.projectPath === "" ? null : record.projectPath,
|
|
30687
|
+
startedAt: record.startedAt,
|
|
30688
|
+
exitCode: null,
|
|
30689
|
+
throughEditor: false,
|
|
30690
|
+
brokeOn: null
|
|
30691
|
+
};
|
|
30692
|
+
this.drainTranscript(adopted);
|
|
30693
|
+
if (!alive(record.pid)) {
|
|
30694
|
+
adopted.log.finish();
|
|
30695
|
+
adopted.exitCode = null;
|
|
30696
|
+
adopted.endedUnwatched = true;
|
|
30697
|
+
}
|
|
30698
|
+
this.activeProcess = adopted;
|
|
30699
|
+
return adopted;
|
|
30700
|
+
}
|
|
30701
|
+
couldBeOurs(project) {
|
|
30702
|
+
if (project === "") {
|
|
30703
|
+
return true;
|
|
30704
|
+
}
|
|
30705
|
+
const status = this.godotBridge.getStatus();
|
|
30706
|
+
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
30707
|
+
return mine === null || samePath(mine, project);
|
|
30708
|
+
}
|
|
30709
|
+
currentRun() {
|
|
30710
|
+
return this.activeProcess ?? this.adoptRecordedRun();
|
|
30711
|
+
}
|
|
30712
|
+
async checkBoot(godotPath, projectPath, scene, args, userArgs) {
|
|
30384
30713
|
const frames = readPositiveNumber(args, "frames") ?? 3;
|
|
30385
30714
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
|
|
30386
|
-
const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames });
|
|
30715
|
+
const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames, userArgs });
|
|
30387
30716
|
const boot = this.spawnGame(godotPath, cmdArgs);
|
|
30388
30717
|
const hung = await new Promise((resolve) => {
|
|
30389
30718
|
const timer = setTimeout(() => {
|
|
@@ -30413,37 +30742,47 @@ class GodotServer {
|
|
|
30413
30742
|
});
|
|
30414
30743
|
}
|
|
30415
30744
|
handleGetDebugOutput(args) {
|
|
30416
|
-
|
|
30745
|
+
const run = this.currentRun();
|
|
30746
|
+
if (!run) {
|
|
30417
30747
|
return this.createErrorResponse("No game is running. Start one with editor_run.");
|
|
30418
30748
|
}
|
|
30419
|
-
this.drainEditorOutput(
|
|
30749
|
+
this.drainEditorOutput(run);
|
|
30750
|
+
this.drainTranscript(run);
|
|
30751
|
+
if (run.endedUnwatched !== true && !stillRunning(run)) {
|
|
30752
|
+
run.log.finish();
|
|
30753
|
+
run.endedUnwatched = run.exitCode === null;
|
|
30754
|
+
}
|
|
30420
30755
|
const severity = readString(args, "severity");
|
|
30421
|
-
const selected =
|
|
30756
|
+
const selected = run.log.select({
|
|
30422
30757
|
severity: severity === "error" || severity === "warning" ? severity : "info",
|
|
30423
30758
|
sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
|
|
30424
30759
|
contains: readNonEmptyString(args, "contains"),
|
|
30425
30760
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
30426
30761
|
});
|
|
30427
|
-
const halt =
|
|
30762
|
+
const halt = run.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
|
|
30428
30763
|
return this.jsonTextResponse({
|
|
30429
|
-
running:
|
|
30430
|
-
exitCode:
|
|
30431
|
-
through:
|
|
30432
|
-
pid:
|
|
30433
|
-
errors:
|
|
30434
|
-
warnings:
|
|
30435
|
-
clean:
|
|
30764
|
+
running: stillRunning(run),
|
|
30765
|
+
exitCode: run.exitCode,
|
|
30766
|
+
through: run.throughEditor ? "editor" : "gdharness",
|
|
30767
|
+
pid: run.pid,
|
|
30768
|
+
errors: run.log.count("error"),
|
|
30769
|
+
warnings: run.log.count("warning"),
|
|
30770
|
+
clean: run.log.count("error") === 0,
|
|
30436
30771
|
heldAt: halt,
|
|
30772
|
+
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
30773
|
+
project: !run.throughEditor && run.process === null ? run.projectPath ?? undefined : undefined,
|
|
30774
|
+
note: run.endedUnwatched === true ? "This run outlived the server that started it and is over now, so its exit code was never collected. Everything it printed is below, read back from its transcript." : undefined,
|
|
30437
30775
|
omitted: selected.omitted,
|
|
30438
30776
|
entries: selected.entries
|
|
30439
30777
|
});
|
|
30440
30778
|
}
|
|
30441
30779
|
async handleStopProject() {
|
|
30442
|
-
|
|
30780
|
+
const stopped = this.currentRun();
|
|
30781
|
+
if (!stopped) {
|
|
30443
30782
|
return this.createErrorResponse("No game is running. Start one with editor_run.");
|
|
30444
30783
|
}
|
|
30445
|
-
const stopped = this.activeProcess;
|
|
30446
30784
|
this.drainEditorOutput(stopped);
|
|
30785
|
+
this.drainTranscript(stopped);
|
|
30447
30786
|
this.logDebug("Stopping the running game");
|
|
30448
30787
|
await this.endActiveGame();
|
|
30449
30788
|
return this.jsonTextResponse({
|
|
@@ -30549,8 +30888,8 @@ class GodotServer {
|
|
|
30549
30888
|
return this.createErrorResponse(choice.problem);
|
|
30550
30889
|
}
|
|
30551
30890
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
30552
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
30553
|
-
const screenshotPath = screenshotDir ?
|
|
30891
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join10(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
30892
|
+
const screenshotPath = screenshotDir ? join10(screenshotDir, "capture.png") : null;
|
|
30554
30893
|
try {
|
|
30555
30894
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
30556
30895
|
if (!reply.ok) {
|
|
@@ -30571,12 +30910,12 @@ class GodotServer {
|
|
|
30571
30910
|
return {
|
|
30572
30911
|
content: [
|
|
30573
30912
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
30574
|
-
{ type: "image", data:
|
|
30913
|
+
{ type: "image", data: readFileSync10(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
30575
30914
|
]
|
|
30576
30915
|
};
|
|
30577
30916
|
} finally {
|
|
30578
30917
|
if (screenshotDir) {
|
|
30579
|
-
|
|
30918
|
+
rmSync5(screenshotDir, { recursive: true, force: true });
|
|
30580
30919
|
}
|
|
30581
30920
|
}
|
|
30582
30921
|
}
|
|
@@ -30601,12 +30940,14 @@ class GodotServer {
|
|
|
30601
30940
|
return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group, says.");
|
|
30602
30941
|
}
|
|
30603
30942
|
const property = readNonEmptyString(args, "property");
|
|
30943
|
+
const includeHidden = readBoolean(args, "includeHidden");
|
|
30604
30944
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
30605
30945
|
...filters,
|
|
30606
30946
|
projectPath: args["projectPath"],
|
|
30607
30947
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
30608
30948
|
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
30609
|
-
...property === undefined ? {} : { property }
|
|
30949
|
+
...property === undefined ? {} : { property },
|
|
30950
|
+
...includeHidden === undefined ? {} : { include_hidden: includeHidden }
|
|
30610
30951
|
});
|
|
30611
30952
|
}
|
|
30612
30953
|
async handleRuntimeWait(op, args) {
|