gdharness 0.10.2 → 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 +401 -84
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +56 -6
- package/build/index.js +391 -80
- 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,86 @@ 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 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
|
+
|
|
27713
27871
|
// src/setup.ts
|
|
27714
|
-
import { cpSync, existsSync as existsSync6, mkdirSync as
|
|
27715
|
-
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";
|
|
27716
27874
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
27717
27875
|
var RUNTIME_AUTOLOAD = {
|
|
27718
27876
|
name: "GdharnessRuntime",
|
|
@@ -27720,11 +27878,11 @@ var RUNTIME_AUTOLOAD = {
|
|
|
27720
27878
|
};
|
|
27721
27879
|
var VERSION_MARKER = ".gdharness-version";
|
|
27722
27880
|
function installedAddonVersion(projectPath) {
|
|
27723
|
-
const marker =
|
|
27881
|
+
const marker = join9(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
27724
27882
|
if (!existsSync6(marker)) {
|
|
27725
27883
|
return null;
|
|
27726
27884
|
}
|
|
27727
|
-
const said =
|
|
27885
|
+
const said = readFileSync9(marker, "utf8").trim();
|
|
27728
27886
|
return said === "" ? null : said;
|
|
27729
27887
|
}
|
|
27730
27888
|
|
|
@@ -28041,7 +28199,7 @@ var TOOL_SPECS = [
|
|
|
28041
28199
|
},
|
|
28042
28200
|
{
|
|
28043
28201
|
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.",
|
|
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.",
|
|
28045
28203
|
parameters: {
|
|
28046
28204
|
projectPath: PROJECT_PATH,
|
|
28047
28205
|
path: {
|
|
@@ -28453,8 +28611,8 @@ var TOOL_SPECS = [
|
|
|
28453
28611
|
},
|
|
28454
28612
|
includeHidden: {
|
|
28455
28613
|
type: "boolean",
|
|
28456
|
-
ops: ["text"],
|
|
28457
|
-
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."
|
|
28458
28616
|
},
|
|
28459
28617
|
metrics: {
|
|
28460
28618
|
type: "array",
|
|
@@ -28471,7 +28629,7 @@ var TOOL_SPECS = [
|
|
|
28471
28629
|
requires: []
|
|
28472
28630
|
},
|
|
28473
28631
|
find: {
|
|
28474
|
-
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",
|
|
28475
28633
|
requires: []
|
|
28476
28634
|
},
|
|
28477
28635
|
rect: {
|
|
@@ -28779,15 +28937,24 @@ var SLOWEST_FRAME_RATE = 20;
|
|
|
28779
28937
|
function patienceForFrames(frames, atLeast) {
|
|
28780
28938
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
28781
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
|
+
}
|
|
28782
28949
|
function alive(pid) {
|
|
28783
|
-
if (pid === undefined) {
|
|
28950
|
+
if (pid === undefined || pid === null) {
|
|
28784
28951
|
return false;
|
|
28785
28952
|
}
|
|
28786
28953
|
try {
|
|
28787
28954
|
process.kill(pid, 0);
|
|
28788
28955
|
return true;
|
|
28789
|
-
} catch {
|
|
28790
|
-
return
|
|
28956
|
+
} catch (error) {
|
|
28957
|
+
return error.code === "EPERM";
|
|
28791
28958
|
}
|
|
28792
28959
|
}
|
|
28793
28960
|
function isOfType(value, kind) {
|
|
@@ -28929,13 +29096,13 @@ function aboveTheRunner(entry) {
|
|
|
28929
29096
|
}
|
|
28930
29097
|
function elsewhereIn(projectPath, asked) {
|
|
28931
29098
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
28932
|
-
if (wanted === "" || existsSync7(
|
|
29099
|
+
if (wanted === "" || existsSync7(join10(projectPath, wanted))) {
|
|
28933
29100
|
return "";
|
|
28934
29101
|
}
|
|
28935
29102
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
28936
29103
|
let near = [];
|
|
28937
29104
|
try {
|
|
28938
|
-
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()));
|
|
28939
29106
|
} catch {
|
|
28940
29107
|
return "";
|
|
28941
29108
|
}
|
|
@@ -28943,11 +29110,11 @@ function elsewhereIn(projectPath, asked) {
|
|
|
28943
29110
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
28944
29111
|
}
|
|
28945
29112
|
function hasMainScene(projectFile) {
|
|
28946
|
-
const scene = parseProjectGodot(
|
|
29113
|
+
const scene = parseProjectGodot(readFileSync10(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
28947
29114
|
return typeof scene === "string" && scene !== "";
|
|
28948
29115
|
}
|
|
28949
29116
|
function editorPlaysHeadless(projectFile) {
|
|
28950
|
-
const runArgs = parseProjectGodot(
|
|
29117
|
+
const runArgs = parseProjectGodot(readFileSync10(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
28951
29118
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
28952
29119
|
}
|
|
28953
29120
|
function camelCased(params) {
|
|
@@ -28963,7 +29130,7 @@ function camelCased(params) {
|
|
|
28963
29130
|
class GodotServer {
|
|
28964
29131
|
mcp;
|
|
28965
29132
|
locator = new GodotLocator;
|
|
28966
|
-
operationsScript =
|
|
29133
|
+
operationsScript = join10(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
28967
29134
|
godotBridge;
|
|
28968
29135
|
tools = buildToolDefinitions();
|
|
28969
29136
|
activeProcess = null;
|
|
@@ -29120,10 +29287,7 @@ class GodotServer {
|
|
|
29120
29287
|
this.stopWatchingForASuccessor();
|
|
29121
29288
|
withdrawBridge(this.announcedAt);
|
|
29122
29289
|
this.announcedAt = null;
|
|
29123
|
-
|
|
29124
|
-
this.activeProcess.process?.kill();
|
|
29125
|
-
this.activeProcess = null;
|
|
29126
|
-
}
|
|
29290
|
+
this.activeProcess = null;
|
|
29127
29291
|
if (this.lspClient) {
|
|
29128
29292
|
try {
|
|
29129
29293
|
await this.lspClient.disconnect();
|
|
@@ -29188,14 +29352,7 @@ class GodotServer {
|
|
|
29188
29352
|
return;
|
|
29189
29353
|
}
|
|
29190
29354
|
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
|
-
}
|
|
29355
|
+
this.activeProcess = null;
|
|
29199
29356
|
}
|
|
29200
29357
|
setupToolHandlers() {
|
|
29201
29358
|
this.mcp.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: this.tools }));
|
|
@@ -29555,7 +29712,7 @@ class GodotServer {
|
|
|
29555
29712
|
if (path === undefined) {
|
|
29556
29713
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
29557
29714
|
}
|
|
29558
|
-
const file =
|
|
29715
|
+
const file = join10(path, "project.godot");
|
|
29559
29716
|
if (!existsSync7(file)) {
|
|
29560
29717
|
return {
|
|
29561
29718
|
ok: false,
|
|
@@ -29725,7 +29882,7 @@ class GodotServer {
|
|
|
29725
29882
|
if (!engine.ok) {
|
|
29726
29883
|
return engine.response;
|
|
29727
29884
|
}
|
|
29728
|
-
const application = parseProjectGodot(
|
|
29885
|
+
const application = parseProjectGodot(readFileSync10(project.value.file, "utf8"))["application"] ?? {};
|
|
29729
29886
|
const name = application["config/name"];
|
|
29730
29887
|
const mainScene = application["run/main_scene"];
|
|
29731
29888
|
const info = {
|
|
@@ -29788,7 +29945,7 @@ class GodotServer {
|
|
|
29788
29945
|
}
|
|
29789
29946
|
const folder = dirname4(output.absolutePath);
|
|
29790
29947
|
try {
|
|
29791
|
-
|
|
29948
|
+
mkdirSync5(folder, { recursive: true });
|
|
29792
29949
|
} catch (error) {
|
|
29793
29950
|
return this.createErrorResponse(`Cannot create ${dirname4(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
29794
29951
|
}
|
|
@@ -29850,7 +30007,7 @@ class GodotServer {
|
|
|
29850
30007
|
return contained.response;
|
|
29851
30008
|
}
|
|
29852
30009
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
29853
|
-
if (!existsSync7(
|
|
30010
|
+
if (!existsSync7(join10(project.value.path, runner))) {
|
|
29854
30011
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
29855
30012
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
29856
30013
|
]);
|
|
@@ -29884,7 +30041,7 @@ class GodotServer {
|
|
|
29884
30041
|
];
|
|
29885
30042
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
29886
30043
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
29887
|
-
const userData = mkdtempSync2(
|
|
30044
|
+
const userData = mkdtempSync2(join10(tmpdir4(), "gdharness-tests-"));
|
|
29888
30045
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
29889
30046
|
const hung = await new Promise((resolve) => {
|
|
29890
30047
|
const timer = setTimeout(() => {
|
|
@@ -29900,26 +30057,26 @@ class GodotServer {
|
|
|
29900
30057
|
resolve(false);
|
|
29901
30058
|
});
|
|
29902
30059
|
});
|
|
29903
|
-
const reportsDir =
|
|
30060
|
+
const reportsDir = join10(project.value.path, ".godot", "gdharness-reports");
|
|
29904
30061
|
let report = null;
|
|
29905
30062
|
let reportProblem = null;
|
|
29906
30063
|
try {
|
|
29907
|
-
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))) : [];
|
|
29908
30065
|
const newest = written.at(-1);
|
|
29909
30066
|
if (newest !== undefined) {
|
|
29910
|
-
report = parseJUnit(
|
|
30067
|
+
report = parseJUnit(readFileSync10(join10(reportsDir, newest, "results.xml"), "utf8"));
|
|
29911
30068
|
}
|
|
29912
30069
|
} catch (error) {
|
|
29913
30070
|
reportProblem = errorMessage(error);
|
|
29914
30071
|
} finally {
|
|
29915
|
-
|
|
29916
|
-
|
|
30072
|
+
rmSync5(reportsDir, { recursive: true, force: true });
|
|
30073
|
+
rmSync5(userData, { recursive: true, force: true });
|
|
29917
30074
|
}
|
|
29918
30075
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
29919
30076
|
const verdicts = {
|
|
29920
30077
|
0: "passed",
|
|
29921
30078
|
100: "failures",
|
|
29922
|
-
101: "warnings",
|
|
30079
|
+
101: "warnings: orphan nodes",
|
|
29923
30080
|
103: "headless not supported by this gdUnit4",
|
|
29924
30081
|
104: "Godot version not supported by this gdUnit4",
|
|
29925
30082
|
105: "script errors"
|
|
@@ -29951,7 +30108,17 @@ class GodotServer {
|
|
|
29951
30108
|
};
|
|
29952
30109
|
}
|
|
29953
30110
|
const failed = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path })));
|
|
29954
|
-
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);
|
|
29955
30122
|
return this.jsonTextResponse({
|
|
29956
30123
|
passed: !hung && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
29957
30124
|
verdict,
|
|
@@ -29962,6 +30129,9 @@ class GodotServer {
|
|
|
29962
30129
|
skipped: report.skipped,
|
|
29963
30130
|
time: report.time,
|
|
29964
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,
|
|
29965
30135
|
suites: unclean.map((suite) => ({
|
|
29966
30136
|
name: suite.name,
|
|
29967
30137
|
path: suite.path,
|
|
@@ -29969,6 +30139,7 @@ class GodotServer {
|
|
|
29969
30139
|
failures: suite.failures,
|
|
29970
30140
|
errors: suite.errors,
|
|
29971
30141
|
skipped: suite.skipped,
|
|
30142
|
+
orphans: orphansIn(suite.path) > 0 ? orphansIn(suite.path) : undefined,
|
|
29972
30143
|
time: suite.time
|
|
29973
30144
|
})),
|
|
29974
30145
|
suitesPassed: report.suites.length - unclean.length,
|
|
@@ -30095,7 +30266,7 @@ class GodotServer {
|
|
|
30095
30266
|
version: godotPath === null ? null : await this.godotVersion(godotPath)
|
|
30096
30267
|
},
|
|
30097
30268
|
game: {
|
|
30098
|
-
processActive: this.
|
|
30269
|
+
processActive: stillRunning(this.currentRun()),
|
|
30099
30270
|
playingInEditor: playing,
|
|
30100
30271
|
runtimeConnected: games.some((game) => game.reachable),
|
|
30101
30272
|
runtimes: games
|
|
@@ -30263,7 +30434,7 @@ class GodotServer {
|
|
|
30263
30434
|
if (op === "check") {
|
|
30264
30435
|
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
|
|
30265
30436
|
}
|
|
30266
|
-
if (this.
|
|
30437
|
+
if (stillRunning(this.currentRun())) {
|
|
30267
30438
|
this.logDebug("Ending the running game before starting another");
|
|
30268
30439
|
await this.endActiveGame();
|
|
30269
30440
|
}
|
|
@@ -30283,7 +30454,7 @@ class GodotServer {
|
|
|
30283
30454
|
userArgs: given.value
|
|
30284
30455
|
});
|
|
30285
30456
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
30286
|
-
const started = this.
|
|
30457
|
+
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
|
|
30287
30458
|
this.activeProcess = started;
|
|
30288
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." : "";
|
|
30289
30460
|
return this.jsonTextResponse({
|
|
@@ -30297,7 +30468,7 @@ class GodotServer {
|
|
|
30297
30468
|
});
|
|
30298
30469
|
}
|
|
30299
30470
|
async runtimeUp(projectPath, before) {
|
|
30300
|
-
if (!existsSync7(
|
|
30471
|
+
if (!existsSync7(join10(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
30301
30472
|
return {
|
|
30302
30473
|
listening: false,
|
|
30303
30474
|
note: "this project has no gdharness_runtime addon, so the runtime_* tools have nothing to talk to"
|
|
@@ -30332,7 +30503,11 @@ class GodotServer {
|
|
|
30332
30503
|
}
|
|
30333
30504
|
const played = {
|
|
30334
30505
|
process: null,
|
|
30506
|
+
pid: null,
|
|
30335
30507
|
log,
|
|
30508
|
+
transcript: null,
|
|
30509
|
+
readOffset: 0,
|
|
30510
|
+
projectPath,
|
|
30336
30511
|
startedAt: Date.now(),
|
|
30337
30512
|
exitCode: null,
|
|
30338
30513
|
throughEditor: true,
|
|
@@ -30371,6 +30546,7 @@ class GodotServer {
|
|
|
30371
30546
|
async endActiveGame() {
|
|
30372
30547
|
const running = this.activeProcess;
|
|
30373
30548
|
this.activeProcess = null;
|
|
30549
|
+
clearRunRecord();
|
|
30374
30550
|
if (!running) {
|
|
30375
30551
|
return;
|
|
30376
30552
|
}
|
|
@@ -30378,14 +30554,26 @@ class GodotServer {
|
|
|
30378
30554
|
await this.handleViaBridge("stop_playing", {});
|
|
30379
30555
|
return;
|
|
30380
30556
|
}
|
|
30381
|
-
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
|
+
}
|
|
30382
30566
|
}
|
|
30383
30567
|
spawnGame(godotPath, cmdArgs, env) {
|
|
30384
30568
|
const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
30385
30569
|
const log = new GameLog;
|
|
30386
30570
|
const started = {
|
|
30387
30571
|
process: child,
|
|
30572
|
+
pid: child.pid ?? null,
|
|
30388
30573
|
log,
|
|
30574
|
+
transcript: null,
|
|
30575
|
+
readOffset: 0,
|
|
30576
|
+
projectPath: null,
|
|
30389
30577
|
startedAt: Date.now(),
|
|
30390
30578
|
exitCode: null,
|
|
30391
30579
|
throughEditor: false,
|
|
@@ -30410,6 +30598,117 @@ class GodotServer {
|
|
|
30410
30598
|
});
|
|
30411
30599
|
return started;
|
|
30412
30600
|
}
|
|
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
|
+
}
|
|
30413
30712
|
async checkBoot(godotPath, projectPath, scene, args, userArgs) {
|
|
30414
30713
|
const frames = readPositiveNumber(args, "frames") ?? 3;
|
|
30415
30714
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
|
|
@@ -30443,37 +30742,47 @@ class GodotServer {
|
|
|
30443
30742
|
});
|
|
30444
30743
|
}
|
|
30445
30744
|
handleGetDebugOutput(args) {
|
|
30446
|
-
|
|
30745
|
+
const run = this.currentRun();
|
|
30746
|
+
if (!run) {
|
|
30447
30747
|
return this.createErrorResponse("No game is running. Start one with editor_run.");
|
|
30448
30748
|
}
|
|
30449
|
-
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
|
+
}
|
|
30450
30755
|
const severity = readString(args, "severity");
|
|
30451
|
-
const selected =
|
|
30756
|
+
const selected = run.log.select({
|
|
30452
30757
|
severity: severity === "error" || severity === "warning" ? severity : "info",
|
|
30453
30758
|
sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
|
|
30454
30759
|
contains: readNonEmptyString(args, "contains"),
|
|
30455
30760
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
30456
30761
|
});
|
|
30457
|
-
const halt =
|
|
30762
|
+
const halt = run.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
|
|
30458
30763
|
return this.jsonTextResponse({
|
|
30459
|
-
running:
|
|
30460
|
-
exitCode:
|
|
30461
|
-
through:
|
|
30462
|
-
pid:
|
|
30463
|
-
errors:
|
|
30464
|
-
warnings:
|
|
30465
|
-
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,
|
|
30466
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,
|
|
30467
30775
|
omitted: selected.omitted,
|
|
30468
30776
|
entries: selected.entries
|
|
30469
30777
|
});
|
|
30470
30778
|
}
|
|
30471
30779
|
async handleStopProject() {
|
|
30472
|
-
|
|
30780
|
+
const stopped = this.currentRun();
|
|
30781
|
+
if (!stopped) {
|
|
30473
30782
|
return this.createErrorResponse("No game is running. Start one with editor_run.");
|
|
30474
30783
|
}
|
|
30475
|
-
const stopped = this.activeProcess;
|
|
30476
30784
|
this.drainEditorOutput(stopped);
|
|
30785
|
+
this.drainTranscript(stopped);
|
|
30477
30786
|
this.logDebug("Stopping the running game");
|
|
30478
30787
|
await this.endActiveGame();
|
|
30479
30788
|
return this.jsonTextResponse({
|
|
@@ -30579,8 +30888,8 @@ class GodotServer {
|
|
|
30579
30888
|
return this.createErrorResponse(choice.problem);
|
|
30580
30889
|
}
|
|
30581
30890
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
30582
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
30583
|
-
const screenshotPath = screenshotDir ?
|
|
30891
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join10(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
30892
|
+
const screenshotPath = screenshotDir ? join10(screenshotDir, "capture.png") : null;
|
|
30584
30893
|
try {
|
|
30585
30894
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
30586
30895
|
if (!reply.ok) {
|
|
@@ -30601,12 +30910,12 @@ class GodotServer {
|
|
|
30601
30910
|
return {
|
|
30602
30911
|
content: [
|
|
30603
30912
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
30604
|
-
{ type: "image", data:
|
|
30913
|
+
{ type: "image", data: readFileSync10(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
30605
30914
|
]
|
|
30606
30915
|
};
|
|
30607
30916
|
} finally {
|
|
30608
30917
|
if (screenshotDir) {
|
|
30609
|
-
|
|
30918
|
+
rmSync5(screenshotDir, { recursive: true, force: true });
|
|
30610
30919
|
}
|
|
30611
30920
|
}
|
|
30612
30921
|
}
|
|
@@ -30631,12 +30940,14 @@ class GodotServer {
|
|
|
30631
30940
|
return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group, says.");
|
|
30632
30941
|
}
|
|
30633
30942
|
const property = readNonEmptyString(args, "property");
|
|
30943
|
+
const includeHidden = readBoolean(args, "includeHidden");
|
|
30634
30944
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
30635
30945
|
...filters,
|
|
30636
30946
|
projectPath: args["projectPath"],
|
|
30637
30947
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
30638
30948
|
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
30639
|
-
...property === undefined ? {} : { property }
|
|
30949
|
+
...property === undefined ? {} : { property },
|
|
30950
|
+
...includeHidden === undefined ? {} : { include_hidden: includeHidden }
|
|
30640
30951
|
});
|
|
30641
30952
|
}
|
|
30642
30953
|
async handleRuntimeWait(op, args) {
|