gdharness 0.10.2 → 0.11.1
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/README.md +23 -0
- package/build/cli.js +427 -86
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +56 -6
- package/build/index.js +417 -82
- 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 !== "") {
|
|
@@ -26604,6 +26621,39 @@ function whyNoReport(printed, asked) {
|
|
|
26604
26621
|
}
|
|
26605
26622
|
return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
|
|
26606
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
|
+
}
|
|
26607
26657
|
|
|
26608
26658
|
// src/lsp_client.ts
|
|
26609
26659
|
import { realpathSync } from "node:fs";
|
|
@@ -26908,6 +26958,14 @@ class GodotLSPClient {
|
|
|
26908
26958
|
}
|
|
26909
26959
|
return uri;
|
|
26910
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
|
+
}
|
|
26911
26969
|
async initialize(rootPath) {
|
|
26912
26970
|
await this.ensureConnected();
|
|
26913
26971
|
const resolvedRootPath = resolve2(rootPath);
|
|
@@ -26961,8 +27019,9 @@ class GodotLSPClient {
|
|
|
26961
27019
|
timer
|
|
26962
27020
|
});
|
|
26963
27021
|
});
|
|
27022
|
+
let opened = null;
|
|
26964
27023
|
try {
|
|
26965
|
-
this.syncDocument(filePath, content);
|
|
27024
|
+
opened = this.syncDocument(filePath, content);
|
|
26966
27025
|
return await diagnosticsPromise;
|
|
26967
27026
|
} catch (error) {
|
|
26968
27027
|
const waiter = this.diagnosticsWaiters.get(key);
|
|
@@ -26971,16 +27030,25 @@ class GodotLSPClient {
|
|
|
26971
27030
|
this.diagnosticsWaiters.delete(key);
|
|
26972
27031
|
}
|
|
26973
27032
|
throw error;
|
|
27033
|
+
} finally {
|
|
27034
|
+
if (opened !== null) {
|
|
27035
|
+
this.closeDocument(opened);
|
|
27036
|
+
}
|
|
26974
27037
|
}
|
|
26975
27038
|
}
|
|
26976
27039
|
async getCompletions(filePath, content, line, character) {
|
|
26977
27040
|
await this.ensureConnected();
|
|
26978
27041
|
await this.ensureInitializedForFile(filePath);
|
|
26979
27042
|
const uri = this.syncDocument(filePath, content);
|
|
26980
|
-
|
|
26981
|
-
|
|
26982
|
-
|
|
26983
|
-
|
|
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
|
+
}
|
|
26984
27052
|
if (Array.isArray(result)) {
|
|
26985
27053
|
return result;
|
|
26986
27054
|
}
|
|
@@ -26996,18 +27064,27 @@ class GodotLSPClient {
|
|
|
26996
27064
|
await this.ensureConnected();
|
|
26997
27065
|
await this.ensureInitializedForFile(filePath);
|
|
26998
27066
|
const uri = this.syncDocument(filePath, content);
|
|
26999
|
-
|
|
27000
|
-
textDocument
|
|
27001
|
-
|
|
27002
|
-
|
|
27067
|
+
try {
|
|
27068
|
+
return await this.sendRequest("textDocument/hover", {
|
|
27069
|
+
textDocument: { uri },
|
|
27070
|
+
position: { line, character }
|
|
27071
|
+
});
|
|
27072
|
+
} finally {
|
|
27073
|
+
this.closeDocument(uri);
|
|
27074
|
+
}
|
|
27003
27075
|
}
|
|
27004
27076
|
async getDocumentSymbols(filePath, content) {
|
|
27005
27077
|
await this.ensureConnected();
|
|
27006
27078
|
await this.ensureInitializedForFile(filePath);
|
|
27007
27079
|
const uri = this.syncDocument(filePath, content);
|
|
27008
|
-
|
|
27009
|
-
|
|
27010
|
-
|
|
27080
|
+
let result;
|
|
27081
|
+
try {
|
|
27082
|
+
result = await this.sendRequest("textDocument/documentSymbol", {
|
|
27083
|
+
textDocument: { uri }
|
|
27084
|
+
});
|
|
27085
|
+
} finally {
|
|
27086
|
+
this.closeDocument(uri);
|
|
27087
|
+
}
|
|
27011
27088
|
return Array.isArray(result) ? result : [];
|
|
27012
27089
|
}
|
|
27013
27090
|
isConnected() {
|
|
@@ -27415,6 +27492,10 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
27415
27492
|
});
|
|
27416
27493
|
}
|
|
27417
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
|
+
|
|
27418
27499
|
// src/runtime-client.ts
|
|
27419
27500
|
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync7, unlinkSync } from "node:fs";
|
|
27420
27501
|
import { createConnection as createConnection3 } from "node:net";
|
|
@@ -27710,9 +27791,104 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
27710
27791
|
});
|
|
27711
27792
|
}
|
|
27712
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 recordPaths() {
|
|
27802
|
+
return runtimeDirectories().map((directory) => join8(directory, "runs", "run.json"));
|
|
27803
|
+
}
|
|
27804
|
+
function openTranscript(startedAt) {
|
|
27805
|
+
mkdirSync3(runsDirectory(), { recursive: true });
|
|
27806
|
+
const path = join8(runsDirectory(), `run-${startedAt}.log`);
|
|
27807
|
+
return { path, fd: openSync(path, "a") };
|
|
27808
|
+
}
|
|
27809
|
+
function writeRunRecord(record) {
|
|
27810
|
+
mkdirSync3(runsDirectory(), { recursive: true });
|
|
27811
|
+
writeFileSync4(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
27812
|
+
}
|
|
27813
|
+
function readRunRecord() {
|
|
27814
|
+
for (const path of recordPaths()) {
|
|
27815
|
+
const record = recordAt(path);
|
|
27816
|
+
if (record !== null) {
|
|
27817
|
+
return record;
|
|
27818
|
+
}
|
|
27819
|
+
}
|
|
27820
|
+
return null;
|
|
27821
|
+
}
|
|
27822
|
+
function recordAt(path) {
|
|
27823
|
+
let parsed;
|
|
27824
|
+
try {
|
|
27825
|
+
parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
27826
|
+
} catch {
|
|
27827
|
+
return null;
|
|
27828
|
+
}
|
|
27829
|
+
if (!parsed || typeof parsed !== "object") {
|
|
27830
|
+
return null;
|
|
27831
|
+
}
|
|
27832
|
+
const fields = parsed;
|
|
27833
|
+
const pid = fields["pid"];
|
|
27834
|
+
const transcript = fields["transcript"];
|
|
27835
|
+
const startedAt = fields["startedAt"];
|
|
27836
|
+
if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
27837
|
+
return null;
|
|
27838
|
+
}
|
|
27839
|
+
const args = fields["arguments"];
|
|
27840
|
+
return {
|
|
27841
|
+
pid,
|
|
27842
|
+
transcript,
|
|
27843
|
+
startedAt,
|
|
27844
|
+
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
27845
|
+
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : []
|
|
27846
|
+
};
|
|
27847
|
+
}
|
|
27848
|
+
function clearRunRecord(ours = () => true) {
|
|
27849
|
+
for (const path of recordPaths()) {
|
|
27850
|
+
const record = recordAt(path);
|
|
27851
|
+
if (record === null || !ours(record)) {
|
|
27852
|
+
continue;
|
|
27853
|
+
}
|
|
27854
|
+
try {
|
|
27855
|
+
rmSync3(path);
|
|
27856
|
+
} catch {}
|
|
27857
|
+
}
|
|
27858
|
+
}
|
|
27859
|
+
function ageMs(path, now) {
|
|
27860
|
+
try {
|
|
27861
|
+
return now - statSync2(path).mtimeMs;
|
|
27862
|
+
} catch {
|
|
27863
|
+
return 0;
|
|
27864
|
+
}
|
|
27865
|
+
}
|
|
27866
|
+
function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
27867
|
+
const directory = runsDirectory();
|
|
27868
|
+
const keep = readRunRecord()?.transcript;
|
|
27869
|
+
let entries;
|
|
27870
|
+
try {
|
|
27871
|
+
entries = readdirSync5(directory);
|
|
27872
|
+
} catch {
|
|
27873
|
+
return;
|
|
27874
|
+
}
|
|
27875
|
+
for (const entry of entries) {
|
|
27876
|
+
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
27877
|
+
continue;
|
|
27878
|
+
}
|
|
27879
|
+
const path = join8(directory, entry);
|
|
27880
|
+
if (path === keep || ageMs(path, now) < keepMs) {
|
|
27881
|
+
continue;
|
|
27882
|
+
}
|
|
27883
|
+
try {
|
|
27884
|
+
rmSync3(path);
|
|
27885
|
+
} catch {}
|
|
27886
|
+
}
|
|
27887
|
+
}
|
|
27888
|
+
|
|
27713
27889
|
// src/setup.ts
|
|
27714
|
-
import { cpSync, existsSync as existsSync6, mkdirSync as
|
|
27715
|
-
import { dirname as dirname3, join as
|
|
27890
|
+
import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync9, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
27891
|
+
import { dirname as dirname3, join as join9 } from "node:path";
|
|
27716
27892
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
27717
27893
|
var RUNTIME_AUTOLOAD = {
|
|
27718
27894
|
name: "GdharnessRuntime",
|
|
@@ -27720,11 +27896,11 @@ var RUNTIME_AUTOLOAD = {
|
|
|
27720
27896
|
};
|
|
27721
27897
|
var VERSION_MARKER = ".gdharness-version";
|
|
27722
27898
|
function installedAddonVersion(projectPath) {
|
|
27723
|
-
const marker =
|
|
27899
|
+
const marker = join9(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
27724
27900
|
if (!existsSync6(marker)) {
|
|
27725
27901
|
return null;
|
|
27726
27902
|
}
|
|
27727
|
-
const said =
|
|
27903
|
+
const said = readFileSync9(marker, "utf8").trim();
|
|
27728
27904
|
return said === "" ? null : said;
|
|
27729
27905
|
}
|
|
27730
27906
|
|
|
@@ -28041,7 +28217,7 @@ var TOOL_SPECS = [
|
|
|
28041
28217
|
},
|
|
28042
28218
|
{
|
|
28043
28219
|
name: "project_test",
|
|
28044
|
-
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.",
|
|
28220
|
+
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.",
|
|
28045
28221
|
parameters: {
|
|
28046
28222
|
projectPath: PROJECT_PATH,
|
|
28047
28223
|
path: {
|
|
@@ -28353,8 +28529,14 @@ var TOOL_SPECS = [
|
|
|
28353
28529
|
type: "boolean",
|
|
28354
28530
|
description: "Only entries printed since the previous editor_output. Default false."
|
|
28355
28531
|
},
|
|
28356
|
-
contains: {
|
|
28357
|
-
|
|
28532
|
+
contains: {
|
|
28533
|
+
type: "string",
|
|
28534
|
+
description: "Only entries mentioning this text, matched against everything the run has printed rather than against the entries this answer would otherwise carry: a line is found however much was printed after it."
|
|
28535
|
+
},
|
|
28536
|
+
limit: {
|
|
28537
|
+
type: "number",
|
|
28538
|
+
description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
28539
|
+
}
|
|
28358
28540
|
},
|
|
28359
28541
|
requires: []
|
|
28360
28542
|
},
|
|
@@ -28453,8 +28635,8 @@ var TOOL_SPECS = [
|
|
|
28453
28635
|
},
|
|
28454
28636
|
includeHidden: {
|
|
28455
28637
|
type: "boolean",
|
|
28456
|
-
ops: ["text"],
|
|
28457
|
-
description: "text: read hidden nodes as well, for checking that something is not showing. Default false."
|
|
28638
|
+
ops: ["text", "find"],
|
|
28639
|
+
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."
|
|
28458
28640
|
},
|
|
28459
28641
|
metrics: {
|
|
28460
28642
|
type: "array",
|
|
@@ -28471,7 +28653,7 @@ var TOOL_SPECS = [
|
|
|
28471
28653
|
requires: []
|
|
28472
28654
|
},
|
|
28473
28655
|
find: {
|
|
28474
|
-
summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each",
|
|
28656
|
+
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",
|
|
28475
28657
|
requires: []
|
|
28476
28658
|
},
|
|
28477
28659
|
rect: {
|
|
@@ -28779,15 +28961,24 @@ var SLOWEST_FRAME_RATE = 20;
|
|
|
28779
28961
|
function patienceForFrames(frames, atLeast) {
|
|
28780
28962
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
28781
28963
|
}
|
|
28964
|
+
function stillRunning(run) {
|
|
28965
|
+
if (run?.exitCode !== null || run.endedUnwatched === true) {
|
|
28966
|
+
return false;
|
|
28967
|
+
}
|
|
28968
|
+
if (!run.throughEditor && run.process === null && run.pid !== null) {
|
|
28969
|
+
return alive(run.pid);
|
|
28970
|
+
}
|
|
28971
|
+
return true;
|
|
28972
|
+
}
|
|
28782
28973
|
function alive(pid) {
|
|
28783
|
-
if (pid === undefined) {
|
|
28974
|
+
if (pid === undefined || pid === null) {
|
|
28784
28975
|
return false;
|
|
28785
28976
|
}
|
|
28786
28977
|
try {
|
|
28787
28978
|
process.kill(pid, 0);
|
|
28788
28979
|
return true;
|
|
28789
|
-
} catch {
|
|
28790
|
-
return
|
|
28980
|
+
} catch (error) {
|
|
28981
|
+
return error.code === "EPERM";
|
|
28791
28982
|
}
|
|
28792
28983
|
}
|
|
28793
28984
|
function isOfType(value, kind) {
|
|
@@ -28929,13 +29120,13 @@ function aboveTheRunner(entry) {
|
|
|
28929
29120
|
}
|
|
28930
29121
|
function elsewhereIn(projectPath, asked) {
|
|
28931
29122
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
28932
|
-
if (wanted === "" || existsSync7(
|
|
29123
|
+
if (wanted === "" || existsSync7(join10(projectPath, wanted))) {
|
|
28933
29124
|
return "";
|
|
28934
29125
|
}
|
|
28935
29126
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
28936
29127
|
let near = [];
|
|
28937
29128
|
try {
|
|
28938
|
-
near =
|
|
29129
|
+
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()));
|
|
28939
29130
|
} catch {
|
|
28940
29131
|
return "";
|
|
28941
29132
|
}
|
|
@@ -28943,11 +29134,11 @@ function elsewhereIn(projectPath, asked) {
|
|
|
28943
29134
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
28944
29135
|
}
|
|
28945
29136
|
function hasMainScene(projectFile) {
|
|
28946
|
-
const scene = parseProjectGodot(
|
|
29137
|
+
const scene = parseProjectGodot(readFileSync10(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
28947
29138
|
return typeof scene === "string" && scene !== "";
|
|
28948
29139
|
}
|
|
28949
29140
|
function editorPlaysHeadless(projectFile) {
|
|
28950
|
-
const runArgs = parseProjectGodot(
|
|
29141
|
+
const runArgs = parseProjectGodot(readFileSync10(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
28951
29142
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
28952
29143
|
}
|
|
28953
29144
|
function camelCased(params) {
|
|
@@ -28963,7 +29154,7 @@ function camelCased(params) {
|
|
|
28963
29154
|
class GodotServer {
|
|
28964
29155
|
mcp;
|
|
28965
29156
|
locator = new GodotLocator;
|
|
28966
|
-
operationsScript =
|
|
29157
|
+
operationsScript = join10(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
28967
29158
|
godotBridge;
|
|
28968
29159
|
tools = buildToolDefinitions();
|
|
28969
29160
|
activeProcess = null;
|
|
@@ -29120,10 +29311,7 @@ class GodotServer {
|
|
|
29120
29311
|
this.stopWatchingForASuccessor();
|
|
29121
29312
|
withdrawBridge(this.announcedAt);
|
|
29122
29313
|
this.announcedAt = null;
|
|
29123
|
-
|
|
29124
|
-
this.activeProcess.process?.kill();
|
|
29125
|
-
this.activeProcess = null;
|
|
29126
|
-
}
|
|
29314
|
+
this.activeProcess = null;
|
|
29127
29315
|
if (this.lspClient) {
|
|
29128
29316
|
try {
|
|
29129
29317
|
await this.lspClient.disconnect();
|
|
@@ -29188,14 +29376,7 @@ class GodotServer {
|
|
|
29188
29376
|
return;
|
|
29189
29377
|
}
|
|
29190
29378
|
this.shutdownInitiated = true;
|
|
29191
|
-
|
|
29192
|
-
try {
|
|
29193
|
-
this.activeProcess.process?.kill();
|
|
29194
|
-
} catch (error) {
|
|
29195
|
-
console.error("[SERVER] Failed to kill the Godot process on exit:", errorMessage(error));
|
|
29196
|
-
}
|
|
29197
|
-
this.activeProcess = null;
|
|
29198
|
-
}
|
|
29379
|
+
this.activeProcess = null;
|
|
29199
29380
|
}
|
|
29200
29381
|
setupToolHandlers() {
|
|
29201
29382
|
this.mcp.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: this.tools }));
|
|
@@ -29555,7 +29736,7 @@ class GodotServer {
|
|
|
29555
29736
|
if (path === undefined) {
|
|
29556
29737
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
29557
29738
|
}
|
|
29558
|
-
const file =
|
|
29739
|
+
const file = join10(path, "project.godot");
|
|
29559
29740
|
if (!existsSync7(file)) {
|
|
29560
29741
|
return {
|
|
29561
29742
|
ok: false,
|
|
@@ -29725,7 +29906,7 @@ class GodotServer {
|
|
|
29725
29906
|
if (!engine.ok) {
|
|
29726
29907
|
return engine.response;
|
|
29727
29908
|
}
|
|
29728
|
-
const application = parseProjectGodot(
|
|
29909
|
+
const application = parseProjectGodot(readFileSync10(project.value.file, "utf8"))["application"] ?? {};
|
|
29729
29910
|
const name = application["config/name"];
|
|
29730
29911
|
const mainScene = application["run/main_scene"];
|
|
29731
29912
|
const info = {
|
|
@@ -29788,7 +29969,7 @@ class GodotServer {
|
|
|
29788
29969
|
}
|
|
29789
29970
|
const folder = dirname4(output.absolutePath);
|
|
29790
29971
|
try {
|
|
29791
|
-
|
|
29972
|
+
mkdirSync5(folder, { recursive: true });
|
|
29792
29973
|
} catch (error) {
|
|
29793
29974
|
return this.createErrorResponse(`Cannot create ${dirname4(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
29794
29975
|
}
|
|
@@ -29850,7 +30031,7 @@ class GodotServer {
|
|
|
29850
30031
|
return contained.response;
|
|
29851
30032
|
}
|
|
29852
30033
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
29853
|
-
if (!existsSync7(
|
|
30034
|
+
if (!existsSync7(join10(project.value.path, runner))) {
|
|
29854
30035
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
29855
30036
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
29856
30037
|
]);
|
|
@@ -29884,7 +30065,7 @@ class GodotServer {
|
|
|
29884
30065
|
];
|
|
29885
30066
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
29886
30067
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
29887
|
-
const userData = mkdtempSync2(
|
|
30068
|
+
const userData = mkdtempSync2(join10(tmpdir4(), "gdharness-tests-"));
|
|
29888
30069
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
29889
30070
|
const hung = await new Promise((resolve) => {
|
|
29890
30071
|
const timer = setTimeout(() => {
|
|
@@ -29900,26 +30081,26 @@ class GodotServer {
|
|
|
29900
30081
|
resolve(false);
|
|
29901
30082
|
});
|
|
29902
30083
|
});
|
|
29903
|
-
const reportsDir =
|
|
30084
|
+
const reportsDir = join10(project.value.path, ".godot", "gdharness-reports");
|
|
29904
30085
|
let report = null;
|
|
29905
30086
|
let reportProblem = null;
|
|
29906
30087
|
try {
|
|
29907
|
-
const written = existsSync7(reportsDir) ?
|
|
30088
|
+
const written = existsSync7(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
29908
30089
|
const newest = written.at(-1);
|
|
29909
30090
|
if (newest !== undefined) {
|
|
29910
|
-
report = parseJUnit(
|
|
30091
|
+
report = parseJUnit(readFileSync10(join10(reportsDir, newest, "results.xml"), "utf8"));
|
|
29911
30092
|
}
|
|
29912
30093
|
} catch (error) {
|
|
29913
30094
|
reportProblem = errorMessage(error);
|
|
29914
30095
|
} finally {
|
|
29915
|
-
|
|
29916
|
-
|
|
30096
|
+
rmSync5(reportsDir, { recursive: true, force: true });
|
|
30097
|
+
rmSync5(userData, { recursive: true, force: true });
|
|
29917
30098
|
}
|
|
29918
30099
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
29919
30100
|
const verdicts = {
|
|
29920
30101
|
0: "passed",
|
|
29921
30102
|
100: "failures",
|
|
29922
|
-
101: "warnings",
|
|
30103
|
+
101: "warnings: orphan nodes",
|
|
29923
30104
|
103: "headless not supported by this gdUnit4",
|
|
29924
30105
|
104: "Godot version not supported by this gdUnit4",
|
|
29925
30106
|
105: "script errors"
|
|
@@ -29951,7 +30132,17 @@ class GodotServer {
|
|
|
29951
30132
|
};
|
|
29952
30133
|
}
|
|
29953
30134
|
const failed = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path })));
|
|
29954
|
-
const
|
|
30135
|
+
const orphans = orphansPrinted(said);
|
|
30136
|
+
const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
|
|
30137
|
+
const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
|
|
30138
|
+
const warnings = orphans.suites.map((suite) => ({
|
|
30139
|
+
kind: "orphans",
|
|
30140
|
+
path: suite.path,
|
|
30141
|
+
name: report.suites.find((entry) => entry.path === suite.path)?.name ?? suite.path,
|
|
30142
|
+
orphans: suite.orphans,
|
|
30143
|
+
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().`
|
|
30144
|
+
}));
|
|
30145
|
+
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
|
|
29955
30146
|
return this.jsonTextResponse({
|
|
29956
30147
|
passed: !hung && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
29957
30148
|
verdict,
|
|
@@ -29962,6 +30153,9 @@ class GodotServer {
|
|
|
29962
30153
|
skipped: report.skipped,
|
|
29963
30154
|
time: report.time,
|
|
29964
30155
|
failed,
|
|
30156
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
30157
|
+
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
30158
|
+
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,
|
|
29965
30159
|
suites: unclean.map((suite) => ({
|
|
29966
30160
|
name: suite.name,
|
|
29967
30161
|
path: suite.path,
|
|
@@ -29969,6 +30163,7 @@ class GodotServer {
|
|
|
29969
30163
|
failures: suite.failures,
|
|
29970
30164
|
errors: suite.errors,
|
|
29971
30165
|
skipped: suite.skipped,
|
|
30166
|
+
orphans: orphansIn(suite.path) > 0 ? orphansIn(suite.path) : undefined,
|
|
29972
30167
|
time: suite.time
|
|
29973
30168
|
})),
|
|
29974
30169
|
suitesPassed: report.suites.length - unclean.length,
|
|
@@ -30095,7 +30290,7 @@ class GodotServer {
|
|
|
30095
30290
|
version: godotPath === null ? null : await this.godotVersion(godotPath)
|
|
30096
30291
|
},
|
|
30097
30292
|
game: {
|
|
30098
|
-
processActive: this.
|
|
30293
|
+
processActive: stillRunning(this.currentRun()),
|
|
30099
30294
|
playingInEditor: playing,
|
|
30100
30295
|
runtimeConnected: games.some((game) => game.reachable),
|
|
30101
30296
|
runtimes: games
|
|
@@ -30263,7 +30458,7 @@ class GodotServer {
|
|
|
30263
30458
|
if (op === "check") {
|
|
30264
30459
|
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
|
|
30265
30460
|
}
|
|
30266
|
-
if (this.
|
|
30461
|
+
if (stillRunning(this.currentRun())) {
|
|
30267
30462
|
this.logDebug("Ending the running game before starting another");
|
|
30268
30463
|
await this.endActiveGame();
|
|
30269
30464
|
}
|
|
@@ -30283,7 +30478,7 @@ class GodotServer {
|
|
|
30283
30478
|
userArgs: given.value
|
|
30284
30479
|
});
|
|
30285
30480
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
30286
|
-
const started = this.
|
|
30481
|
+
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
|
|
30287
30482
|
this.activeProcess = started;
|
|
30288
30483
|
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." : "";
|
|
30289
30484
|
return this.jsonTextResponse({
|
|
@@ -30297,7 +30492,7 @@ class GodotServer {
|
|
|
30297
30492
|
});
|
|
30298
30493
|
}
|
|
30299
30494
|
async runtimeUp(projectPath, before) {
|
|
30300
|
-
if (!existsSync7(
|
|
30495
|
+
if (!existsSync7(join10(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
30301
30496
|
return {
|
|
30302
30497
|
listening: false,
|
|
30303
30498
|
note: "this project has no gdharness_runtime addon, so the runtime_* tools have nothing to talk to"
|
|
@@ -30332,7 +30527,11 @@ class GodotServer {
|
|
|
30332
30527
|
}
|
|
30333
30528
|
const played = {
|
|
30334
30529
|
process: null,
|
|
30530
|
+
pid: null,
|
|
30335
30531
|
log,
|
|
30532
|
+
transcript: null,
|
|
30533
|
+
readOffset: 0,
|
|
30534
|
+
projectPath,
|
|
30336
30535
|
startedAt: Date.now(),
|
|
30337
30536
|
exitCode: null,
|
|
30338
30537
|
throughEditor: true,
|
|
@@ -30371,6 +30570,7 @@ class GodotServer {
|
|
|
30371
30570
|
async endActiveGame() {
|
|
30372
30571
|
const running = this.activeProcess;
|
|
30373
30572
|
this.activeProcess = null;
|
|
30573
|
+
clearRunRecord((record) => this.couldBeOurs(record.projectPath));
|
|
30374
30574
|
if (!running) {
|
|
30375
30575
|
return;
|
|
30376
30576
|
}
|
|
@@ -30378,14 +30578,26 @@ class GodotServer {
|
|
|
30378
30578
|
await this.handleViaBridge("stop_playing", {});
|
|
30379
30579
|
return;
|
|
30380
30580
|
}
|
|
30381
|
-
running.process
|
|
30581
|
+
if (running.process !== null) {
|
|
30582
|
+
running.process.kill();
|
|
30583
|
+
return;
|
|
30584
|
+
}
|
|
30585
|
+
if (running.pid !== null) {
|
|
30586
|
+
try {
|
|
30587
|
+
process.kill(running.pid);
|
|
30588
|
+
} catch {}
|
|
30589
|
+
}
|
|
30382
30590
|
}
|
|
30383
30591
|
spawnGame(godotPath, cmdArgs, env) {
|
|
30384
30592
|
const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
30385
30593
|
const log = new GameLog;
|
|
30386
30594
|
const started = {
|
|
30387
30595
|
process: child,
|
|
30596
|
+
pid: child.pid ?? null,
|
|
30388
30597
|
log,
|
|
30598
|
+
transcript: null,
|
|
30599
|
+
readOffset: 0,
|
|
30600
|
+
projectPath: null,
|
|
30389
30601
|
startedAt: Date.now(),
|
|
30390
30602
|
exitCode: null,
|
|
30391
30603
|
throughEditor: false,
|
|
@@ -30410,6 +30622,117 @@ class GodotServer {
|
|
|
30410
30622
|
});
|
|
30411
30623
|
return started;
|
|
30412
30624
|
}
|
|
30625
|
+
spawnKeptGame(godotPath, cmdArgs, projectPath, env) {
|
|
30626
|
+
const startedAt = Date.now();
|
|
30627
|
+
const transcript = openTranscript(startedAt);
|
|
30628
|
+
let child;
|
|
30629
|
+
try {
|
|
30630
|
+
child = spawn(godotPath, cmdArgs, {
|
|
30631
|
+
stdio: ["ignore", transcript.fd, transcript.fd],
|
|
30632
|
+
detached: true,
|
|
30633
|
+
...env ? { env } : {}
|
|
30634
|
+
});
|
|
30635
|
+
} finally {
|
|
30636
|
+
closeSync(transcript.fd);
|
|
30637
|
+
}
|
|
30638
|
+
child.unref();
|
|
30639
|
+
const log = new GameLog;
|
|
30640
|
+
const started = {
|
|
30641
|
+
process: child,
|
|
30642
|
+
pid: child.pid ?? null,
|
|
30643
|
+
log,
|
|
30644
|
+
transcript: transcript.path,
|
|
30645
|
+
readOffset: 0,
|
|
30646
|
+
projectPath,
|
|
30647
|
+
startedAt,
|
|
30648
|
+
exitCode: null,
|
|
30649
|
+
throughEditor: false,
|
|
30650
|
+
brokeOn: null
|
|
30651
|
+
};
|
|
30652
|
+
child.on("exit", (code) => {
|
|
30653
|
+
this.logDebug(`Godot process exited with code ${code ?? "none"}`);
|
|
30654
|
+
this.drainTranscript(started);
|
|
30655
|
+
log.finish();
|
|
30656
|
+
started.exitCode = code ?? -1;
|
|
30657
|
+
});
|
|
30658
|
+
child.on("error", (err) => {
|
|
30659
|
+
console.error("Failed to start Godot process:", err);
|
|
30660
|
+
log.append("stderr", `${err.message}
|
|
30661
|
+
`);
|
|
30662
|
+
started.exitCode = -1;
|
|
30663
|
+
});
|
|
30664
|
+
if (started.pid !== null) {
|
|
30665
|
+
writeRunRecord({
|
|
30666
|
+
pid: started.pid,
|
|
30667
|
+
transcript: transcript.path,
|
|
30668
|
+
startedAt,
|
|
30669
|
+
projectPath,
|
|
30670
|
+
arguments: cmdArgs
|
|
30671
|
+
});
|
|
30672
|
+
}
|
|
30673
|
+
sweepTranscripts();
|
|
30674
|
+
return started;
|
|
30675
|
+
}
|
|
30676
|
+
drainTranscript(game) {
|
|
30677
|
+
if (game.transcript === null) {
|
|
30678
|
+
return;
|
|
30679
|
+
}
|
|
30680
|
+
let handle;
|
|
30681
|
+
try {
|
|
30682
|
+
handle = openSync2(game.transcript, "r");
|
|
30683
|
+
} catch {
|
|
30684
|
+
return;
|
|
30685
|
+
}
|
|
30686
|
+
try {
|
|
30687
|
+
const size = fstatSync(handle).size;
|
|
30688
|
+
if (size <= game.readOffset) {
|
|
30689
|
+
return;
|
|
30690
|
+
}
|
|
30691
|
+
const buffer = Buffer.alloc(size - game.readOffset);
|
|
30692
|
+
const read = readSync(handle, buffer, 0, buffer.length, game.readOffset);
|
|
30693
|
+
game.readOffset += read;
|
|
30694
|
+
game.log.append("transcript", buffer.subarray(0, read));
|
|
30695
|
+
} finally {
|
|
30696
|
+
closeSync(handle);
|
|
30697
|
+
}
|
|
30698
|
+
}
|
|
30699
|
+
adoptRecordedRun() {
|
|
30700
|
+
const record = readRunRecord();
|
|
30701
|
+
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
30702
|
+
return null;
|
|
30703
|
+
}
|
|
30704
|
+
const adopted = {
|
|
30705
|
+
process: null,
|
|
30706
|
+
pid: record.pid,
|
|
30707
|
+
log: new GameLog,
|
|
30708
|
+
transcript: record.transcript,
|
|
30709
|
+
readOffset: 0,
|
|
30710
|
+
projectPath: record.projectPath === "" ? null : record.projectPath,
|
|
30711
|
+
startedAt: record.startedAt,
|
|
30712
|
+
exitCode: null,
|
|
30713
|
+
throughEditor: false,
|
|
30714
|
+
brokeOn: null
|
|
30715
|
+
};
|
|
30716
|
+
this.drainTranscript(adopted);
|
|
30717
|
+
if (!alive(record.pid)) {
|
|
30718
|
+
adopted.log.finish();
|
|
30719
|
+
adopted.exitCode = null;
|
|
30720
|
+
adopted.endedUnwatched = true;
|
|
30721
|
+
}
|
|
30722
|
+
this.activeProcess = adopted;
|
|
30723
|
+
return adopted;
|
|
30724
|
+
}
|
|
30725
|
+
couldBeOurs(project) {
|
|
30726
|
+
if (project === "") {
|
|
30727
|
+
return true;
|
|
30728
|
+
}
|
|
30729
|
+
const status = this.godotBridge.getStatus();
|
|
30730
|
+
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
30731
|
+
return mine === null || samePath(mine, project);
|
|
30732
|
+
}
|
|
30733
|
+
currentRun() {
|
|
30734
|
+
return this.activeProcess ?? this.adoptRecordedRun();
|
|
30735
|
+
}
|
|
30413
30736
|
async checkBoot(godotPath, projectPath, scene, args, userArgs) {
|
|
30414
30737
|
const frames = readPositiveNumber(args, "frames") ?? 3;
|
|
30415
30738
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
|
|
@@ -30443,37 +30766,47 @@ class GodotServer {
|
|
|
30443
30766
|
});
|
|
30444
30767
|
}
|
|
30445
30768
|
handleGetDebugOutput(args) {
|
|
30446
|
-
|
|
30769
|
+
const run = this.currentRun();
|
|
30770
|
+
if (!run) {
|
|
30447
30771
|
return this.createErrorResponse("No game is running. Start one with editor_run.");
|
|
30448
30772
|
}
|
|
30449
|
-
this.drainEditorOutput(
|
|
30773
|
+
this.drainEditorOutput(run);
|
|
30774
|
+
this.drainTranscript(run);
|
|
30775
|
+
if (run.endedUnwatched !== true && !stillRunning(run)) {
|
|
30776
|
+
run.log.finish();
|
|
30777
|
+
run.endedUnwatched = run.exitCode === null;
|
|
30778
|
+
}
|
|
30450
30779
|
const severity = readString(args, "severity");
|
|
30451
|
-
const selected =
|
|
30780
|
+
const selected = run.log.select({
|
|
30452
30781
|
severity: severity === "error" || severity === "warning" ? severity : "info",
|
|
30453
30782
|
sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
|
|
30454
30783
|
contains: readNonEmptyString(args, "contains"),
|
|
30455
30784
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
30456
30785
|
});
|
|
30457
|
-
const halt =
|
|
30786
|
+
const halt = run.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
|
|
30458
30787
|
return this.jsonTextResponse({
|
|
30459
|
-
running:
|
|
30460
|
-
exitCode:
|
|
30461
|
-
through:
|
|
30462
|
-
pid:
|
|
30463
|
-
errors:
|
|
30464
|
-
warnings:
|
|
30465
|
-
clean:
|
|
30788
|
+
running: stillRunning(run),
|
|
30789
|
+
exitCode: run.exitCode,
|
|
30790
|
+
through: run.throughEditor ? "editor" : "gdharness",
|
|
30791
|
+
pid: run.pid,
|
|
30792
|
+
errors: run.log.count("error"),
|
|
30793
|
+
warnings: run.log.count("warning"),
|
|
30794
|
+
clean: run.log.count("error") === 0,
|
|
30466
30795
|
heldAt: halt,
|
|
30796
|
+
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
30797
|
+
project: !run.throughEditor && run.process === null ? run.projectPath ?? undefined : undefined,
|
|
30798
|
+
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,
|
|
30467
30799
|
omitted: selected.omitted,
|
|
30468
30800
|
entries: selected.entries
|
|
30469
30801
|
});
|
|
30470
30802
|
}
|
|
30471
30803
|
async handleStopProject() {
|
|
30472
|
-
|
|
30804
|
+
const stopped = this.currentRun();
|
|
30805
|
+
if (!stopped) {
|
|
30473
30806
|
return this.createErrorResponse("No game is running. Start one with editor_run.");
|
|
30474
30807
|
}
|
|
30475
|
-
const stopped = this.activeProcess;
|
|
30476
30808
|
this.drainEditorOutput(stopped);
|
|
30809
|
+
this.drainTranscript(stopped);
|
|
30477
30810
|
this.logDebug("Stopping the running game");
|
|
30478
30811
|
await this.endActiveGame();
|
|
30479
30812
|
return this.jsonTextResponse({
|
|
@@ -30579,8 +30912,8 @@ class GodotServer {
|
|
|
30579
30912
|
return this.createErrorResponse(choice.problem);
|
|
30580
30913
|
}
|
|
30581
30914
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
30582
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
30583
|
-
const screenshotPath = screenshotDir ?
|
|
30915
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join10(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
30916
|
+
const screenshotPath = screenshotDir ? join10(screenshotDir, "capture.png") : null;
|
|
30584
30917
|
try {
|
|
30585
30918
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
30586
30919
|
if (!reply.ok) {
|
|
@@ -30601,12 +30934,12 @@ class GodotServer {
|
|
|
30601
30934
|
return {
|
|
30602
30935
|
content: [
|
|
30603
30936
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
30604
|
-
{ type: "image", data:
|
|
30937
|
+
{ type: "image", data: readFileSync10(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
30605
30938
|
]
|
|
30606
30939
|
};
|
|
30607
30940
|
} finally {
|
|
30608
30941
|
if (screenshotDir) {
|
|
30609
|
-
|
|
30942
|
+
rmSync5(screenshotDir, { recursive: true, force: true });
|
|
30610
30943
|
}
|
|
30611
30944
|
}
|
|
30612
30945
|
}
|
|
@@ -30631,12 +30964,14 @@ class GodotServer {
|
|
|
30631
30964
|
return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group, says.");
|
|
30632
30965
|
}
|
|
30633
30966
|
const property = readNonEmptyString(args, "property");
|
|
30967
|
+
const includeHidden = readBoolean(args, "includeHidden");
|
|
30634
30968
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
30635
30969
|
...filters,
|
|
30636
30970
|
projectPath: args["projectPath"],
|
|
30637
30971
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
30638
30972
|
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
30639
|
-
...property === undefined ? {} : { property }
|
|
30973
|
+
...property === undefined ? {} : { property },
|
|
30974
|
+
...includeHidden === undefined ? {} : { include_hidden: includeHidden }
|
|
30640
30975
|
});
|
|
30641
30976
|
}
|
|
30642
30977
|
async handleRuntimeWait(op, args) {
|