gdharness 1.2.24 → 1.2.26
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 +508 -174
- package/build/godot/addons/gdharness_editor/serialisation.gd +29 -0
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +290 -0
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +7 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +24 -4
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +150 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +5 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +29 -0
- package/build/godot/operations/godot_operations.gd +7 -0
- package/build/godot/operations/import_pipeline.gd +92 -0
- package/build/godot/operations/input_actions.gd +3 -2
- package/build/godot/operations/plugins.gd +3 -2
- package/build/godot/operations/project_config.gd +17 -9
- package/build/godot/operations/project_file.gd +202 -0
- package/build/godot/operations/serialisation.gd +29 -0
- package/build/index.js +520 -190
- package/build/keeper.js +16 -9
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -3359,21 +3359,21 @@ function defectReport(where, error, context = {}) {
|
|
|
3359
3359
|
import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
|
|
3360
3360
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3361
3361
|
import {
|
|
3362
|
-
appendFileSync,
|
|
3362
|
+
appendFileSync as appendFileSync2,
|
|
3363
3363
|
closeSync as closeSync2,
|
|
3364
3364
|
constants,
|
|
3365
3365
|
copyFileSync,
|
|
3366
3366
|
existsSync as existsSync17,
|
|
3367
3367
|
fstatSync,
|
|
3368
|
-
mkdirSync as
|
|
3368
|
+
mkdirSync as mkdirSync14,
|
|
3369
3369
|
openSync as openSync3,
|
|
3370
3370
|
readdirSync as readdirSync9,
|
|
3371
3371
|
readFileSync as readFileSync21,
|
|
3372
3372
|
readSync as readSync2,
|
|
3373
3373
|
rmdirSync as rmdirSync2,
|
|
3374
|
-
statSync as
|
|
3374
|
+
statSync as statSync9
|
|
3375
3375
|
} from "node:fs";
|
|
3376
|
-
import { basename as basename5, dirname as
|
|
3376
|
+
import { basename as basename5, dirname as dirname16, extname as extname3, isAbsolute as isAbsolute3, join as join23, normalize as normalize2, relative as relative4 } from "node:path";
|
|
3377
3377
|
import { setTimeout as delay4 } from "node:timers/promises";
|
|
3378
3378
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3379
3379
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -24888,7 +24888,7 @@ import { dirname as dirname2, join as join3 } from "node:path";
|
|
|
24888
24888
|
// src/runtime-client.ts
|
|
24889
24889
|
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync, unlinkSync } from "node:fs";
|
|
24890
24890
|
import { createConnection } from "node:net";
|
|
24891
|
-
import { tmpdir as
|
|
24891
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
24892
24892
|
import { dirname, join as join2, resolve as resolve2 } from "node:path";
|
|
24893
24893
|
import { setTimeout as delay } from "node:timers/promises";
|
|
24894
24894
|
|
|
@@ -24933,12 +24933,27 @@ var ENGINE_FLAGS = Object.freeze({
|
|
|
24933
24933
|
"--fixed-fps": "number",
|
|
24934
24934
|
"--max-fps": "number",
|
|
24935
24935
|
"--resolution": "size",
|
|
24936
|
-
"--write-movie": "file"
|
|
24936
|
+
"--write-movie": "file",
|
|
24937
|
+
"--rendering-driver": "driver",
|
|
24938
|
+
"--rendering-method": "method",
|
|
24939
|
+
"--accessibility": "accessibility"
|
|
24937
24940
|
});
|
|
24938
24941
|
var VALUE_KIND = {
|
|
24939
24942
|
number: { pattern: /^\d+(?:\.\d+)?$/, named: "a number" },
|
|
24940
24943
|
size: { pattern: /^\d+x\d+$/, named: "a size such as 1280x720" },
|
|
24941
|
-
file: { pattern: /^[^-]/, named: "a file path" }
|
|
24944
|
+
file: { pattern: /^[^-]/, named: "a file path" },
|
|
24945
|
+
driver: {
|
|
24946
|
+
pattern: /^(?:vulkan|d3d12|metal|opengl3|opengl3_es|opengl3_angle|dummy)$/,
|
|
24947
|
+
named: "a rendering driver: vulkan, d3d12, metal, opengl3, opengl3_es, opengl3_angle or dummy"
|
|
24948
|
+
},
|
|
24949
|
+
method: {
|
|
24950
|
+
pattern: /^(?:forward_plus|mobile|gl_compatibility)$/,
|
|
24951
|
+
named: "a rendering method: forward_plus, mobile or gl_compatibility"
|
|
24952
|
+
},
|
|
24953
|
+
accessibility: {
|
|
24954
|
+
pattern: /^(?:auto|always|disabled)$/,
|
|
24955
|
+
named: "an accessibility mode: auto, always or disabled"
|
|
24956
|
+
}
|
|
24942
24957
|
};
|
|
24943
24958
|
var SET_BY_ARGUMENT = Object.freeze({
|
|
24944
24959
|
"--headless": "headless",
|
|
@@ -24954,7 +24969,7 @@ function engineFlags(given) {
|
|
|
24954
24969
|
const setBy = Object.hasOwn(SET_BY_ARGUMENT, flag) ? SET_BY_ARGUMENT[flag] : undefined;
|
|
24955
24970
|
return {
|
|
24956
24971
|
ok: false,
|
|
24957
|
-
reason: setBy !== undefined ? `${flag} is set by this tool's own argument, ${setBy}, rather than through engineArgs.` : `${flag} is not one of the engine flags engineArgs takes, which are those that change what the engine logs, draws for debugging, checks on the GPU
|
|
24972
|
+
reason: setBy !== undefined ? `${flag} is set by this tool's own argument, ${setBy}, rather than through engineArgs.` : `${flag} is not one of the engine flags engineArgs takes, which are those that change what the engine logs, draws for debugging, checks on the GPU, which renderer draws, how it times frames or whether it builds the tree a screen reader reads, and Movie Maker's --write-movie: ${offered}. A value goes as the next entry, as in ["--fixed-fps", "60"].`
|
|
24958
24973
|
};
|
|
24959
24974
|
}
|
|
24960
24975
|
if (takes === "none") {
|
|
@@ -25105,6 +25120,7 @@ import { promisify } from "node:util";
|
|
|
25105
25120
|
|
|
25106
25121
|
// src/windows-ask.ts
|
|
25107
25122
|
import { spawn } from "node:child_process";
|
|
25123
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
25108
25124
|
import process6 from "node:process";
|
|
25109
25125
|
var SCRIPT = [
|
|
25110
25126
|
"[Console]::OutputEncoding = [Text.Encoding]::UTF8",
|
|
@@ -25159,7 +25175,7 @@ function running() {
|
|
|
25159
25175
|
"Bypass",
|
|
25160
25176
|
"-EncodedCommand",
|
|
25161
25177
|
Buffer.from(SCRIPT, "utf16le").toString("base64")
|
|
25162
|
-
], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
25178
|
+
], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, cwd: tmpdir2() });
|
|
25163
25179
|
buffered = "";
|
|
25164
25180
|
child.stdout.setEncoding("utf8");
|
|
25165
25181
|
child.stdout.on("data", (chunk) => {
|
|
@@ -25556,7 +25572,7 @@ function runtimeDirectory(variables = process.env) {
|
|
|
25556
25572
|
return explicit;
|
|
25557
25573
|
}
|
|
25558
25574
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
25559
|
-
return join2(perUser ??
|
|
25575
|
+
return join2(perUser ?? tmpdir3(), "gdharness");
|
|
25560
25576
|
}
|
|
25561
25577
|
function runtimeDirectories(variables = process.env) {
|
|
25562
25578
|
const candidates = [runtimeDirectory(variables)];
|
|
@@ -29498,7 +29514,7 @@ function importRecord(projectPath, resourcePath) {
|
|
|
29498
29514
|
|
|
29499
29515
|
// src/scratch.ts
|
|
29500
29516
|
import { existsSync as existsSync10, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync6 } from "node:fs";
|
|
29501
|
-
import { tmpdir as
|
|
29517
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
29502
29518
|
import { join as join13 } from "node:path";
|
|
29503
29519
|
import process7 from "node:process";
|
|
29504
29520
|
var SWEPT = { recursive: true, force: true };
|
|
@@ -29514,9 +29530,9 @@ var KINDS = [
|
|
|
29514
29530
|
"tests"
|
|
29515
29531
|
];
|
|
29516
29532
|
function scratchDirectory(kind) {
|
|
29517
|
-
return mkdtempSync(join13(
|
|
29533
|
+
return mkdtempSync(join13(tmpdir4(), `gdharness-${kind}-${process7.pid}-`));
|
|
29518
29534
|
}
|
|
29519
|
-
function sweepAbandonedScratch(directory =
|
|
29535
|
+
function sweepAbandonedScratch(directory = tmpdir4()) {
|
|
29520
29536
|
let entries;
|
|
29521
29537
|
try {
|
|
29522
29538
|
entries = readdirSync4(directory);
|
|
@@ -29609,17 +29625,25 @@ function snakeCased(params) {
|
|
|
29609
29625
|
return result;
|
|
29610
29626
|
}
|
|
29611
29627
|
function engineExtras(outcome) {
|
|
29628
|
+
const after = outcome.afterAnswerMessages ?? [];
|
|
29612
29629
|
return {
|
|
29613
29630
|
...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
|
|
29614
29631
|
...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
|
|
29632
|
+
...after.length > 0 ? { engine_messages_after_answer: after, engine_messages_after_answer_note: AFTER_ANSWER_NOTE } : {},
|
|
29633
|
+
...outcome.afterAnswerOmitted === undefined ? {} : { engine_messages_after_answer_omitted: outcome.afterAnswerOmitted },
|
|
29615
29634
|
...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
|
|
29616
29635
|
};
|
|
29617
29636
|
}
|
|
29637
|
+
var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, while the engine ran the project's autoloads and shut down: Godot starts a project's autoloads in every script run and cannot be told not to. An error raised in a project script here is that autoload's own, and none of these changes the answer.";
|
|
29618
29638
|
var OWN_ERROR = "[ERROR] ";
|
|
29639
|
+
var ANSWERED = "[gdharness] answered";
|
|
29619
29640
|
var MESSAGES_KEPT = 200;
|
|
29620
29641
|
var ERRORS_NAMED = 3;
|
|
29621
29642
|
function problemsIn(log) {
|
|
29622
|
-
return log.everything()
|
|
29643
|
+
return problemsAmong(log.everything());
|
|
29644
|
+
}
|
|
29645
|
+
function problemsAmong(entries) {
|
|
29646
|
+
return entries.flatMap((entry) => {
|
|
29623
29647
|
if (entry.severity !== "info") {
|
|
29624
29648
|
return [entry];
|
|
29625
29649
|
}
|
|
@@ -29634,7 +29658,7 @@ function reported2(problems) {
|
|
|
29634
29658
|
return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
|
|
29635
29659
|
}
|
|
29636
29660
|
function reason(log, problems) {
|
|
29637
|
-
const own = log
|
|
29661
|
+
const own = refusalsIn(log);
|
|
29638
29662
|
if (own.length > 0) {
|
|
29639
29663
|
return own.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; ");
|
|
29640
29664
|
}
|
|
@@ -29646,6 +29670,9 @@ function reason(log, problems) {
|
|
|
29646
29670
|
const printed = log.everything().filter((entry) => entry.source === "stdout");
|
|
29647
29671
|
return printed.at(-1)?.text ?? "no output at all";
|
|
29648
29672
|
}
|
|
29673
|
+
function refusalsIn(log) {
|
|
29674
|
+
return log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
|
|
29675
|
+
}
|
|
29649
29676
|
function scriptFault(problems, script) {
|
|
29650
29677
|
const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
|
|
29651
29678
|
const directories = [...new Set([spelled(dirname8(script)), spelled(resolvedOr(dirname8(script)))])];
|
|
@@ -29748,15 +29775,32 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
29748
29775
|
} finally {
|
|
29749
29776
|
discard(paramsDir);
|
|
29750
29777
|
}
|
|
29751
|
-
const
|
|
29778
|
+
const everything = ran.log.everything();
|
|
29779
|
+
const answered = everything.findIndex((entry) => entry.source === "stderr" && entry.text === ANSWERED);
|
|
29780
|
+
const problems = problemsAmong(answered === -1 ? everything : everything.slice(0, answered));
|
|
29781
|
+
const after = answered === -1 ? [] : problemsAmong(everything.slice(answered + 1));
|
|
29782
|
+
const afterAnswered = {
|
|
29783
|
+
...after.length === 0 ? {} : { afterAnswerMessages: after.slice(-MESSAGES_KEPT) },
|
|
29784
|
+
...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {}
|
|
29785
|
+
};
|
|
29752
29786
|
if (answer.kind === "unreadable") {
|
|
29753
29787
|
return {
|
|
29754
29788
|
ok: false,
|
|
29755
29789
|
message: `${operation} wrote an answer that could not be read (${answer.why})`,
|
|
29756
|
-
...reported2(problems)
|
|
29790
|
+
...reported2(problems),
|
|
29791
|
+
...afterAnswered
|
|
29757
29792
|
};
|
|
29758
29793
|
}
|
|
29759
29794
|
if (answer.kind === "none") {
|
|
29795
|
+
const refusals = refusalsIn(ran.log);
|
|
29796
|
+
if (refusals.length > 0) {
|
|
29797
|
+
const said = new Set(refusals.map((entry) => entry.index));
|
|
29798
|
+
return {
|
|
29799
|
+
ok: false,
|
|
29800
|
+
message: refusals.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; "),
|
|
29801
|
+
...reported2(problems.filter((entry) => !said.has(entry.index)))
|
|
29802
|
+
};
|
|
29803
|
+
}
|
|
29760
29804
|
const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
|
|
29761
29805
|
return {
|
|
29762
29806
|
ok: false,
|
|
@@ -29769,14 +29813,16 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
29769
29813
|
return {
|
|
29770
29814
|
ok: false,
|
|
29771
29815
|
message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
|
|
29772
|
-
...reported2(problems)
|
|
29816
|
+
...reported2(problems),
|
|
29817
|
+
...afterAnswered
|
|
29773
29818
|
};
|
|
29774
29819
|
}
|
|
29775
|
-
const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under engine_messages.`;
|
|
29820
|
+
const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under ${answered === -1 ? "engine_messages" : "engine_messages_after_answer"}.`;
|
|
29776
29821
|
return {
|
|
29777
29822
|
ok: true,
|
|
29778
29823
|
payload: answer.payload,
|
|
29779
29824
|
...reported2(problems),
|
|
29825
|
+
...afterAnswered,
|
|
29780
29826
|
...afterAnswer === undefined ? {} : { afterAnswer }
|
|
29781
29827
|
};
|
|
29782
29828
|
}
|
|
@@ -30333,11 +30379,54 @@ function askTheKeeperToStop(address, withinMs) {
|
|
|
30333
30379
|
});
|
|
30334
30380
|
}
|
|
30335
30381
|
|
|
30382
|
+
// src/lifecycle-log.ts
|
|
30383
|
+
import { appendFileSync, mkdirSync as mkdirSync8, renameSync, statSync as statSync6 } from "node:fs";
|
|
30384
|
+
import { dirname as dirname9, join as join15 } from "node:path";
|
|
30385
|
+
import process8 from "node:process";
|
|
30386
|
+
function lifecycleLogPath() {
|
|
30387
|
+
return join15(runtimeDirectory(), "servers.log");
|
|
30388
|
+
}
|
|
30389
|
+
var LIFECYCLE_LOG_LIMIT = 1e6;
|
|
30390
|
+
function writeLifecycle(event, fields = {}) {
|
|
30391
|
+
const path = lifecycleLogPath();
|
|
30392
|
+
try {
|
|
30393
|
+
mkdirSync8(dirname9(path), { recursive: true });
|
|
30394
|
+
if ((statSync6(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
|
|
30395
|
+
try {
|
|
30396
|
+
renameSync(path, `${path}.1`);
|
|
30397
|
+
} catch {}
|
|
30398
|
+
}
|
|
30399
|
+
const line = {
|
|
30400
|
+
at: new Date().toISOString(),
|
|
30401
|
+
pid: process8.pid,
|
|
30402
|
+
ppid: process8.ppid,
|
|
30403
|
+
version: SERVER_VERSION,
|
|
30404
|
+
event
|
|
30405
|
+
};
|
|
30406
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
30407
|
+
if (!Object.hasOwn(line, name)) {
|
|
30408
|
+
line[name] = value;
|
|
30409
|
+
}
|
|
30410
|
+
}
|
|
30411
|
+
appendFileSync(path, `${JSON.stringify(line)}
|
|
30412
|
+
`, "utf8");
|
|
30413
|
+
} catch {
|
|
30414
|
+
console.error(`[SERVER] Could not write the lifecycle log at ${path}.`);
|
|
30415
|
+
}
|
|
30416
|
+
}
|
|
30417
|
+
function callsInFlight(calls, now = Date.now()) {
|
|
30418
|
+
return [...calls].map((call) => ({
|
|
30419
|
+
tool: call.tool,
|
|
30420
|
+
...call.op === "" ? {} : { op: call.op },
|
|
30421
|
+
runningMs: now - call.startedAt
|
|
30422
|
+
}));
|
|
30423
|
+
}
|
|
30424
|
+
|
|
30336
30425
|
// src/lsp_client.ts
|
|
30337
30426
|
import { existsSync as existsSync12, realpathSync as realpathSync3 } from "node:fs";
|
|
30338
30427
|
import { readFile, realpath } from "node:fs/promises";
|
|
30339
30428
|
import { createConnection as createConnection3 } from "node:net";
|
|
30340
|
-
import { dirname as
|
|
30429
|
+
import { dirname as dirname10, join as join16, resolve as resolve3 } from "node:path";
|
|
30341
30430
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
30342
30431
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
30343
30432
|
var DEFAULT_LSP_PORT = 6005;
|
|
@@ -30535,7 +30624,7 @@ class GodotLSPClient {
|
|
|
30535
30624
|
if (this.initialized) {
|
|
30536
30625
|
return;
|
|
30537
30626
|
}
|
|
30538
|
-
const rootPath = this.rootPath ??
|
|
30627
|
+
const rootPath = this.rootPath ?? dirname10(resolve3(filePath));
|
|
30539
30628
|
await this.initialize(rootPath);
|
|
30540
30629
|
}
|
|
30541
30630
|
async sendRequest(method, params, timeoutMs = this.timeouts.requestMs) {
|
|
@@ -31093,7 +31182,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
31093
31182
|
} catch {
|
|
31094
31183
|
throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
|
|
31095
31184
|
}
|
|
31096
|
-
if (!existsSync12(
|
|
31185
|
+
if (!existsSync12(join16(projectPath, "project.godot"))) {
|
|
31097
31186
|
throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
|
|
31098
31187
|
}
|
|
31099
31188
|
const contained = resolveWithinProject(projectPath, scriptPathValue);
|
|
@@ -31301,7 +31390,7 @@ function secondsFromClock(printed) {
|
|
|
31301
31390
|
|
|
31302
31391
|
// src/project-scan.ts
|
|
31303
31392
|
import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync14 } from "node:fs";
|
|
31304
|
-
import { extname, join as
|
|
31393
|
+
import { extname, join as join17 } from "node:path";
|
|
31305
31394
|
var SKIPPED = new Set(["node_modules"]);
|
|
31306
31395
|
function skipped(name) {
|
|
31307
31396
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
@@ -31311,10 +31400,10 @@ function steppedOver(directory) {
|
|
|
31311
31400
|
return gdignored(directory) || installedByUs(directory);
|
|
31312
31401
|
}
|
|
31313
31402
|
function gdignored(directory) {
|
|
31314
|
-
return existsSync13(
|
|
31403
|
+
return existsSync13(join17(directory, ".gdignore"));
|
|
31315
31404
|
}
|
|
31316
31405
|
function installedByUs(directory) {
|
|
31317
|
-
return existsSync13(
|
|
31406
|
+
return existsSync13(join17(directory, VERSION_MARKER));
|
|
31318
31407
|
}
|
|
31319
31408
|
var SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
|
|
31320
31409
|
var ASSET_EXTENSIONS = new Set([
|
|
@@ -31358,7 +31447,7 @@ function projectStructure(projectPath) {
|
|
|
31358
31447
|
continue;
|
|
31359
31448
|
}
|
|
31360
31449
|
if (entry.isDirectory()) {
|
|
31361
|
-
visit(
|
|
31450
|
+
visit(join17(directory, entry.name));
|
|
31362
31451
|
} else if (entry.isFile()) {
|
|
31363
31452
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
31364
31453
|
if (SCENE_EXTENSIONS.has(extension)) {
|
|
@@ -31388,7 +31477,7 @@ function projectFiles(projectPath, extensions) {
|
|
|
31388
31477
|
}
|
|
31389
31478
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
31390
31479
|
if (entry.isDirectory()) {
|
|
31391
|
-
visit(
|
|
31480
|
+
visit(join17(directory, entry.name), spelled);
|
|
31392
31481
|
} else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
|
|
31393
31482
|
found.push(spelled);
|
|
31394
31483
|
}
|
|
@@ -31410,14 +31499,14 @@ function scriptsWithoutUid(projectPath) {
|
|
|
31410
31499
|
}
|
|
31411
31500
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
31412
31501
|
if (entry.isDirectory()) {
|
|
31413
|
-
visit(
|
|
31502
|
+
visit(join17(directory, entry.name), spelled);
|
|
31414
31503
|
continue;
|
|
31415
31504
|
}
|
|
31416
31505
|
if (!entry.isFile()) {
|
|
31417
31506
|
continue;
|
|
31418
31507
|
}
|
|
31419
31508
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
31420
|
-
if (CARRIES_A_UID.has(extension) && !existsSync13(
|
|
31509
|
+
if (CARRIES_A_UID.has(extension) && !existsSync13(join17(directory, `${entry.name}.uid`))) {
|
|
31421
31510
|
found.push(spelled);
|
|
31422
31511
|
}
|
|
31423
31512
|
}
|
|
@@ -31449,7 +31538,7 @@ function projectTexts(projectPath) {
|
|
|
31449
31538
|
if (skipped(entry.name)) {
|
|
31450
31539
|
continue;
|
|
31451
31540
|
}
|
|
31452
|
-
const absolute =
|
|
31541
|
+
const absolute = join17(directory, entry.name);
|
|
31453
31542
|
const spelled = `${prefix}${entry.name}`;
|
|
31454
31543
|
if (entry.isDirectory()) {
|
|
31455
31544
|
visit(absolute, `${spelled}/`);
|
|
@@ -31479,7 +31568,7 @@ function ignoredTexts(projectPath) {
|
|
|
31479
31568
|
if (skipped(entry.name)) {
|
|
31480
31569
|
continue;
|
|
31481
31570
|
}
|
|
31482
|
-
const absolute =
|
|
31571
|
+
const absolute = join17(directory, entry.name);
|
|
31483
31572
|
const spelled = `${prefix}${entry.name}`;
|
|
31484
31573
|
if (entry.isDirectory()) {
|
|
31485
31574
|
visit(absolute, `${spelled}/`, under);
|
|
@@ -31519,7 +31608,7 @@ function searchProject(projectPath, options) {
|
|
|
31519
31608
|
if (skipped(entry.name)) {
|
|
31520
31609
|
continue;
|
|
31521
31610
|
}
|
|
31522
|
-
const entryPath =
|
|
31611
|
+
const entryPath = join17(directory, entry.name);
|
|
31523
31612
|
if (entry.isDirectory()) {
|
|
31524
31613
|
visit(entryPath);
|
|
31525
31614
|
continue;
|
|
@@ -31690,8 +31779,8 @@ function setupPromptHandlers(server) {
|
|
|
31690
31779
|
}
|
|
31691
31780
|
|
|
31692
31781
|
// src/rename.ts
|
|
31693
|
-
import { existsSync as existsSync14, mkdirSync as
|
|
31694
|
-
import { dirname as
|
|
31782
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync15, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
|
|
31783
|
+
import { dirname as dirname11, posix as posix2 } from "node:path";
|
|
31695
31784
|
class Script {
|
|
31696
31785
|
path;
|
|
31697
31786
|
text;
|
|
@@ -32424,15 +32513,15 @@ function applyRewrites(rewrites) {
|
|
|
32424
32513
|
const done = [];
|
|
32425
32514
|
try {
|
|
32426
32515
|
for (const rewrite of rewrites) {
|
|
32427
|
-
|
|
32516
|
+
mkdirSync9(dirname11(rewrite.writeTo), { recursive: true });
|
|
32428
32517
|
const staging = `${rewrite.writeTo}.gdharness-rename`;
|
|
32429
32518
|
writeFileSync10(staging, rewrite.after, "utf8");
|
|
32430
|
-
|
|
32519
|
+
renameSync2(staging, rewrite.writeTo);
|
|
32431
32520
|
let movedUid = false;
|
|
32432
32521
|
if (rewrite.writeTo !== rewrite.absolute) {
|
|
32433
32522
|
unlinkSync2(rewrite.absolute);
|
|
32434
32523
|
if (existsSync14(`${rewrite.absolute}.uid`)) {
|
|
32435
|
-
|
|
32524
|
+
renameSync2(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
|
|
32436
32525
|
movedUid = true;
|
|
32437
32526
|
}
|
|
32438
32527
|
}
|
|
@@ -32445,7 +32534,7 @@ function applyRewrites(rewrites) {
|
|
|
32445
32534
|
if (rewrite.writeTo !== rewrite.absolute) {
|
|
32446
32535
|
unlinkSync2(rewrite.writeTo);
|
|
32447
32536
|
if (movedUid) {
|
|
32448
|
-
|
|
32537
|
+
renameSync2(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
|
|
32449
32538
|
}
|
|
32450
32539
|
}
|
|
32451
32540
|
} catch {}
|
|
@@ -32455,8 +32544,8 @@ function applyRewrites(rewrites) {
|
|
|
32455
32544
|
}
|
|
32456
32545
|
|
|
32457
32546
|
// src/rename-symbol.ts
|
|
32458
|
-
import { existsSync as existsSync15, mkdirSync as
|
|
32459
|
-
import { basename as basename3, dirname as
|
|
32547
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, readdirSync as readdirSync6, realpathSync as realpathSync5, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
|
|
32548
|
+
import { basename as basename3, dirname as dirname12, join as join18, relative as relative3, resolve as resolve5 } from "node:path";
|
|
32460
32549
|
|
|
32461
32550
|
// src/resources.ts
|
|
32462
32551
|
import { readFileSync as readFileSync16, realpathSync as realpathSync4 } from "node:fs";
|
|
@@ -32764,9 +32853,9 @@ function onDiskSpelling(path) {
|
|
|
32764
32853
|
let at = resolve5(path);
|
|
32765
32854
|
for (;; ) {
|
|
32766
32855
|
try {
|
|
32767
|
-
return
|
|
32856
|
+
return join18(realpathSync5.native(at), ...missing.reverse());
|
|
32768
32857
|
} catch {
|
|
32769
|
-
const parent =
|
|
32858
|
+
const parent = dirname12(at);
|
|
32770
32859
|
if (parent === at) {
|
|
32771
32860
|
return resolve5(path);
|
|
32772
32861
|
}
|
|
@@ -33194,15 +33283,15 @@ ${mention.kind}`;
|
|
|
33194
33283
|
var LEFT_ALONE_ANSWER_LINES = 300;
|
|
33195
33284
|
var detailWrites = 0;
|
|
33196
33285
|
function writeLeftAloneDetail(projectPath, symbol, detail) {
|
|
33197
|
-
const directory =
|
|
33198
|
-
|
|
33286
|
+
const directory = join18(projectPath, ".godot", "gdharness-renames");
|
|
33287
|
+
mkdirSync10(directory, { recursive: true });
|
|
33199
33288
|
detailWrites += 1;
|
|
33200
|
-
const path =
|
|
33289
|
+
const path = join18(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
|
|
33201
33290
|
writeFileSync11(path, `${JSON.stringify(detail, null, 2)}
|
|
33202
33291
|
`, "utf8");
|
|
33203
33292
|
const kept = readdirSync6(directory).filter((name) => name.endsWith(".json")).sort().reverse();
|
|
33204
33293
|
for (const old of kept.slice(10)) {
|
|
33205
|
-
rmSync7(
|
|
33294
|
+
rmSync7(join18(directory, old), { force: true });
|
|
33206
33295
|
}
|
|
33207
33296
|
return path;
|
|
33208
33297
|
}
|
|
@@ -33225,15 +33314,15 @@ function summaryOf(edits, mentions, rewrites, left) {
|
|
|
33225
33314
|
}
|
|
33226
33315
|
|
|
33227
33316
|
// src/restart-note.ts
|
|
33228
|
-
import { mkdirSync as
|
|
33229
|
-
import { dirname as
|
|
33317
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync17, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "node:fs";
|
|
33318
|
+
import { dirname as dirname13, join as join19 } from "node:path";
|
|
33230
33319
|
function restartNotePath(projectPath) {
|
|
33231
|
-
return
|
|
33320
|
+
return join19(projectPath, ".godot", "gdharness-restart.json");
|
|
33232
33321
|
}
|
|
33233
33322
|
function noteRestartBegun(note) {
|
|
33234
33323
|
const path = restartNotePath(note.projectPath);
|
|
33235
33324
|
try {
|
|
33236
|
-
|
|
33325
|
+
mkdirSync11(dirname13(path), { recursive: true });
|
|
33237
33326
|
writeFileSync12(path, `${JSON.stringify(note, null, 2)}
|
|
33238
33327
|
`, "utf8");
|
|
33239
33328
|
} catch (error) {
|
|
@@ -33266,12 +33355,12 @@ function restartSettled(projectPath) {
|
|
|
33266
33355
|
// src/run-record.ts
|
|
33267
33356
|
import { execFile as execFile4 } from "node:child_process";
|
|
33268
33357
|
import { createHash as createHash3 } from "node:crypto";
|
|
33269
|
-
import { mkdirSync as
|
|
33270
|
-
import { basename as basename4, dirname as
|
|
33358
|
+
import { mkdirSync as mkdirSync12, openSync as openSync2, readdirSync as readdirSync7, readFileSync as readFileSync18, realpathSync as realpathSync6, rmSync as rmSync10, statSync as statSync7 } from "node:fs";
|
|
33359
|
+
import { basename as basename4, dirname as dirname14, join as join20, resolve as resolve6 } from "node:path";
|
|
33271
33360
|
import { promisify as promisify4 } from "node:util";
|
|
33272
33361
|
|
|
33273
33362
|
// src/whole-file.ts
|
|
33274
|
-
import { renameSync as
|
|
33363
|
+
import { renameSync as renameSync3, rmSync as rmSync9, writeFileSync as writeFileSync13 } from "node:fs";
|
|
33275
33364
|
var HELD_FILE_MS = 1000;
|
|
33276
33365
|
var HELD_FILE_RETRY_MS = 10;
|
|
33277
33366
|
var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
@@ -33285,7 +33374,7 @@ function writeWhole(path, text) {
|
|
|
33285
33374
|
const until = Date.now() + HELD_FILE_MS;
|
|
33286
33375
|
for (;; ) {
|
|
33287
33376
|
try {
|
|
33288
|
-
|
|
33377
|
+
renameSync3(partial, path);
|
|
33289
33378
|
return true;
|
|
33290
33379
|
} catch (error) {
|
|
33291
33380
|
const code = error.code ?? "";
|
|
@@ -33303,16 +33392,16 @@ function writeWhole(path, text) {
|
|
|
33303
33392
|
// src/run-record.ts
|
|
33304
33393
|
var execFileAsync = promisify4(execFile4);
|
|
33305
33394
|
function runsDirectory() {
|
|
33306
|
-
return
|
|
33395
|
+
return join20(runtimeDirectory(), "runs");
|
|
33307
33396
|
}
|
|
33308
33397
|
function realAsFarAsItExists(path) {
|
|
33309
33398
|
const missing = [];
|
|
33310
33399
|
let at = path;
|
|
33311
33400
|
for (;; ) {
|
|
33312
33401
|
try {
|
|
33313
|
-
return
|
|
33402
|
+
return join20(realpathSync6.native(at), ...missing);
|
|
33314
33403
|
} catch {
|
|
33315
|
-
const parent =
|
|
33404
|
+
const parent = dirname14(at);
|
|
33316
33405
|
if (parent === at) {
|
|
33317
33406
|
return path;
|
|
33318
33407
|
}
|
|
@@ -33327,14 +33416,14 @@ function projectKey(projectPath) {
|
|
|
33327
33416
|
return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
33328
33417
|
}
|
|
33329
33418
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
33330
|
-
return
|
|
33419
|
+
return join20(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
33331
33420
|
}
|
|
33332
33421
|
function latestRunPath(servedBy, directory = runtimeDirectory()) {
|
|
33333
|
-
return
|
|
33422
|
+
return join20(directory, "runs", `latest-${projectKey(servedBy)}.json`);
|
|
33334
33423
|
}
|
|
33335
33424
|
function writeLatestRun(servedBy, latest) {
|
|
33336
33425
|
try {
|
|
33337
|
-
|
|
33426
|
+
mkdirSync12(runsDirectory(), { recursive: true });
|
|
33338
33427
|
writeWhole(latestRunPath(servedBy), JSON.stringify(latest, null, 2));
|
|
33339
33428
|
} catch {}
|
|
33340
33429
|
}
|
|
@@ -33355,21 +33444,21 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
|
|
|
33355
33444
|
}
|
|
33356
33445
|
var SHARED_NOTE = "run.json";
|
|
33357
33446
|
function openTranscript(startedAt) {
|
|
33358
|
-
|
|
33359
|
-
const path =
|
|
33447
|
+
mkdirSync12(runsDirectory(), { recursive: true });
|
|
33448
|
+
const path = join20(runsDirectory(), `run-${startedAt}.log`);
|
|
33360
33449
|
return { path, fd: openSync2(path, "a") };
|
|
33361
33450
|
}
|
|
33362
33451
|
function profilePath(startedAt) {
|
|
33363
|
-
return
|
|
33452
|
+
return join20(runsDirectory(), `run-${startedAt}.profile.json`);
|
|
33364
33453
|
}
|
|
33365
33454
|
function noteIsAbout(note, playing) {
|
|
33366
33455
|
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
33367
33456
|
}
|
|
33368
33457
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
33369
|
-
return
|
|
33458
|
+
return join20(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
33370
33459
|
}
|
|
33371
33460
|
function writeEditorRunNote(note) {
|
|
33372
|
-
|
|
33461
|
+
mkdirSync12(runsDirectory(), { recursive: true });
|
|
33373
33462
|
const path = editorNotePath(note.projectPath);
|
|
33374
33463
|
if (!writeWhole(path, JSON.stringify(note, null, 2))) {
|
|
33375
33464
|
throw new Error(`the editor run note could not be written at ${path}`);
|
|
@@ -33377,7 +33466,7 @@ function writeEditorRunNote(note) {
|
|
|
33377
33466
|
}
|
|
33378
33467
|
function readEditorRunNote(projectPath) {
|
|
33379
33468
|
for (const directory of runtimeDirectories()) {
|
|
33380
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
33469
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join20(directory, "runs", "editor-run.json"));
|
|
33381
33470
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
33382
33471
|
return note;
|
|
33383
33472
|
}
|
|
@@ -33411,7 +33500,7 @@ function readRunRecord(projectPath) {
|
|
|
33411
33500
|
if (own !== null) {
|
|
33412
33501
|
return own;
|
|
33413
33502
|
}
|
|
33414
|
-
const shared = recordAt(
|
|
33503
|
+
const shared = recordAt(join20(directory, "runs", SHARED_NOTE));
|
|
33415
33504
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
33416
33505
|
return shared;
|
|
33417
33506
|
}
|
|
@@ -33423,13 +33512,13 @@ function everyRunRecord() {
|
|
|
33423
33512
|
for (const directory of runtimeDirectories()) {
|
|
33424
33513
|
let entries;
|
|
33425
33514
|
try {
|
|
33426
|
-
entries = readdirSync7(
|
|
33515
|
+
entries = readdirSync7(join20(directory, "runs"));
|
|
33427
33516
|
} catch {
|
|
33428
33517
|
continue;
|
|
33429
33518
|
}
|
|
33430
33519
|
for (const entry of entries) {
|
|
33431
33520
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
33432
|
-
const record = recordAt(
|
|
33521
|
+
const record = recordAt(join20(directory, "runs", entry));
|
|
33433
33522
|
if (record !== null) {
|
|
33434
33523
|
found.push(record);
|
|
33435
33524
|
}
|
|
@@ -33640,7 +33729,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
33640
33729
|
}
|
|
33641
33730
|
function clearRunRecord(projectPath) {
|
|
33642
33731
|
for (const directory of runtimeDirectories()) {
|
|
33643
|
-
const shared =
|
|
33732
|
+
const shared = join20(directory, "runs", SHARED_NOTE);
|
|
33644
33733
|
const sharedRecord = recordAt(shared);
|
|
33645
33734
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
33646
33735
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
@@ -33652,7 +33741,7 @@ function clearRunRecord(projectPath) {
|
|
|
33652
33741
|
}
|
|
33653
33742
|
function ageMs(path, now) {
|
|
33654
33743
|
try {
|
|
33655
|
-
return now -
|
|
33744
|
+
return now - statSync7(path).mtimeMs;
|
|
33656
33745
|
} catch {
|
|
33657
33746
|
return 0;
|
|
33658
33747
|
}
|
|
@@ -33674,7 +33763,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
33674
33763
|
if (!transcript && !entry.endsWith(".partial")) {
|
|
33675
33764
|
continue;
|
|
33676
33765
|
}
|
|
33677
|
-
const path =
|
|
33766
|
+
const path = join20(directory, entry);
|
|
33678
33767
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
33679
33768
|
continue;
|
|
33680
33769
|
}
|
|
@@ -33685,7 +33774,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
33685
33774
|
}
|
|
33686
33775
|
|
|
33687
33776
|
// src/script-origins.ts
|
|
33688
|
-
import { join as
|
|
33777
|
+
import { join as join21 } from "node:path";
|
|
33689
33778
|
var UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
|
|
33690
33779
|
function unresolvedClassIn(message) {
|
|
33691
33780
|
return UNRESOLVED.exec(message)?.[1] ?? null;
|
|
@@ -33741,7 +33830,7 @@ function checkWithEngine(godotPath, projectPath) {
|
|
|
33741
33830
|
const ran = await runEngine(godotPath, [
|
|
33742
33831
|
"--headless",
|
|
33743
33832
|
"--log-file",
|
|
33744
|
-
|
|
33833
|
+
join21(logDir, "engine.log"),
|
|
33745
33834
|
"--path",
|
|
33746
33835
|
projectPath,
|
|
33747
33836
|
"--check-only",
|
|
@@ -34159,7 +34248,7 @@ var TOOL_SPECS = [
|
|
|
34159
34248
|
{
|
|
34160
34249
|
name: "project_settings",
|
|
34161
34250
|
title: "Project settings",
|
|
34162
|
-
description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
|
|
34251
|
+
description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change touches only its own lines. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
|
|
34163
34252
|
parameters: {
|
|
34164
34253
|
projectPath: PROJECT_PATH2,
|
|
34165
34254
|
setting: {
|
|
@@ -34344,7 +34433,7 @@ var TOOL_SPECS = [
|
|
|
34344
34433
|
},
|
|
34345
34434
|
options: {
|
|
34346
34435
|
type: "object",
|
|
34347
|
-
description:
|
|
34436
|
+
description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads.`,
|
|
34348
34437
|
additionalProperties: true
|
|
34349
34438
|
},
|
|
34350
34439
|
reimport: {
|
|
@@ -34432,7 +34521,7 @@ var TOOL_SPECS = [
|
|
|
34432
34521
|
engineArgs: {
|
|
34433
34522
|
type: "array",
|
|
34434
34523
|
items: { type: "string" },
|
|
34435
|
-
description: `The engine's own flags for the run, such as ["--verbose"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU
|
|
34524
|
+
description: `The engine's own flags for the run, such as ["--verbose"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU, which renderer draws, how it times frames or whether it builds the tree a screen reader reads: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused. With --verbose, engineEntries holds the ordinary lines the run printed as well, which is where the engine names the instances leaked and the resources still in use at exit.`
|
|
34436
34525
|
},
|
|
34437
34526
|
failFast: {
|
|
34438
34527
|
type: "boolean",
|
|
@@ -34859,7 +34948,7 @@ var TOOL_SPECS = [
|
|
|
34859
34948
|
{
|
|
34860
34949
|
name: "editor_launch",
|
|
34861
34950
|
title: "Open, restart or close the editor",
|
|
34862
|
-
description: "Opens the Godot editor on a project, or restarts or closes the one already connected. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; settingsDropped in its answer says what that save took out of project.godot. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
34951
|
+
description: "Opens the Godot editor on a project, or restarts or closes the one already connected. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; settingsDropped in its answer says what that save took out of project.godot. An editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead and answers beforeConnecting. open answers once the process has started, and the editor dials in after its import; a call needing the editor made before then waits for the one this server opened, for as long as a restart waits, and is refused naming it if it has not arrived. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
34863
34952
|
parameters: {
|
|
34864
34953
|
projectPath: { ...PROJECT_PATH2, ops: ["open"] },
|
|
34865
34954
|
hidden: {
|
|
@@ -34909,7 +34998,7 @@ var TOOL_SPECS = [
|
|
|
34909
34998
|
type: "array",
|
|
34910
34999
|
items: { type: "string" },
|
|
34911
35000
|
ops: ["start", "check"],
|
|
34912
|
-
description: `The engine's own flags, before the separator, such as ["--verbose"] or ["--fixed-fps", "60"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU
|
|
35001
|
+
description: `The engine's own flags, before the separator, such as ["--verbose"] or ["--fixed-fps", "60"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU, which renderer draws, how it times frames or whether it builds the tree a screen reader reads, and Movie Maker's --write-movie: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused, since the engine's command line can also run scripts, open other projects and write files. --rendering-driver takes vulkan, d3d12, metal, opengl3, opengl3_es, opengl3_angle or dummy, and --rendering-method forward_plus, mobile or gl_compatibility, so the same scene can be measured on another driver and a project's OpenGL fallback exercised on a machine that has Vulkan; they choose what draws a run with a window. --accessibility always has the game build the tree a screen reader reads, as the project's accessibility/general/accessibility_support at Always Active does, so runtime_inspect accessibility can read it; otherwise the engine builds it only when a screen reader is running. --write-movie takes an absolute path outside the project, in a directory that exists, ending in .avi (one file, video and sound) or .png (one numbered PNG per frame and a .wav beside them), where nothing it would write is there already; it needs a start with headless: false, since the engine crashes writing a movie headless, and it records at the project's editor/movie_writer/fps unless --fixed-fps says otherwise. A run with any is started by this server, as one with args is. A check given --verbose answers with everything printed rather than only errors and warnings, since what verbose adds, the instances leaked at exit among it, is printed as ordinary lines.`
|
|
34913
35002
|
},
|
|
34914
35003
|
headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
|
|
34915
35004
|
visible: {
|
|
@@ -35014,6 +35103,10 @@ var TOOL_SPECS = [
|
|
|
35014
35103
|
title: "Game and editor output",
|
|
35015
35104
|
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries: clean is true when no error was reported, and warnings, counted beside it, do not make it false. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
|
|
35016
35105
|
parameters: {
|
|
35106
|
+
projectPath: {
|
|
35107
|
+
type: "string",
|
|
35108
|
+
description: "The project whose output you mean to read. It picks nothing, since this reads the one run or editor this server holds, and a call naming another project than that one's is refused rather than answered with output from a different game."
|
|
35109
|
+
},
|
|
35017
35110
|
severity: {
|
|
35018
35111
|
type: "string",
|
|
35019
35112
|
enum: ["error", "warning", "info"],
|
|
@@ -35065,7 +35158,7 @@ var TOOL_SPECS = [
|
|
|
35065
35158
|
title: "Editor status",
|
|
35066
35159
|
effect: "read",
|
|
35067
35160
|
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and which games with the runtime addon are reachable, of this project and of every other on the machine. Read connected with mayYetConnect: the editor dials this server and backs off between tries, so for the first half-minute after a server starts false means not reached yet as often as no editor; mayYetConnect true is worth waiting out, false is final, and restartInterrupted, restartUnderway, closedHere and editorsNotYetConnected say why. An editor that has connected and not yet greeted reports nothing about itself, and every other call made in that window waits up to ten seconds for it to settle. playingInEditor null means unanswered rather than nothing playing: processActive and runtimes do not depend on the editor answering. game.run names the run editor_output and editor_run stop answer for, and stays named after it ends. addonIsStale, on the editor and on each game, says it runs addon code other than what this server ships, and staleNote says which half moves: gdharness upgrade in the project, a restart, or a reconnect of this server. A false editor.lspListening or editor.dapListening comes with serversNote, and editor_launch restart binds the port again. editor.scan says whether the editor is scanning, which editor_run waits out before starting a game. A game announced in a protocol this server does not speak is listed under unreadable, during an upgrade that moves the protocol, until the halves match.",
|
|
35068
|
-
answer: "runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
35161
|
+
answer: "server names this server: its pid, its version, and lifecycleLog, the file every gdharness server of the user appends a line to when it starts, when its client first names itself, when it replaces another server of its project, and when it ends, with why it ended (the client closing its end as stdin:end, a signal by name, superseded with the successor's pid, an uncaught exception or rejection with its stack, or the exit code) and the calls it was answering then. The parent's command line follows on its own line a few seconds after the start. A server killed without warning writes no ended line, and on Windows every kill is that, so a started line with no ended line after it is a server something killed. " + "runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
35069
35162
|
parameters: {},
|
|
35070
35163
|
requires: []
|
|
35071
35164
|
},
|
|
@@ -35145,7 +35238,7 @@ var TOOL_SPECS = [
|
|
|
35145
35238
|
{
|
|
35146
35239
|
name: "runtime_inspect",
|
|
35147
35240
|
title: "Inspect the running game",
|
|
35148
|
-
description: "Questions about the running game: what is written on the screen, the scene tree, the nodes matching a query, where one node is on screen, what one property reads,
|
|
35241
|
+
description: "Questions about the running game: what is written on the screen, the scene tree, the nodes matching a query, where one node is on screen, what one property reads, the performance metrics, or what it tells a screen reader. Needs the game running with the runtime addon.",
|
|
35149
35242
|
parameters: {
|
|
35150
35243
|
projectPath: RUNNING_PROJECT_PATH,
|
|
35151
35244
|
pid: RUNNING_PID,
|
|
@@ -35214,7 +35307,7 @@ var TOOL_SPECS = [
|
|
|
35214
35307
|
timeoutMs: {
|
|
35215
35308
|
type: "integer",
|
|
35216
35309
|
minimum: 1,
|
|
35217
|
-
description: "How long to wait for the answer before answering pending with a requestId, which runtime_invoke op result collects the reply by. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS."
|
|
35310
|
+
description: "How long to wait for the answer before answering pending with a requestId, which runtime_invoke op result collects the reply by. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS; accessibility defaults to 30000 or that, whichever is longer."
|
|
35218
35311
|
}
|
|
35219
35312
|
},
|
|
35220
35313
|
requires: [],
|
|
@@ -35242,7 +35335,12 @@ var TOOL_SPECS = [
|
|
|
35242
35335
|
},
|
|
35243
35336
|
metrics: {
|
|
35244
35337
|
effect: "read",
|
|
35245
|
-
summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked",
|
|
35338
|
+
summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked. memory_static is what the engine allocated on the CPU side, in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are what the renderer has on the GPU, in bytes, which memory_static does not count",
|
|
35339
|
+
requires: []
|
|
35340
|
+
},
|
|
35341
|
+
accessibility: {
|
|
35342
|
+
effect: "read",
|
|
35343
|
+
summary: `what the game tells a screen reader, read through UI Automation as a Windows screen reader reads it, under windows: one tree per window, each element with its role as UI Automation names it (text for static text, button, check_box, group for a control holding others), its name, its value where it has one, its bounds in the window's pixels, the actions a screen reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the one holding the focus. The name is what is read aloud, so an empty one is read as nothing; static text takes it from DisplayServer.accessibility_update_set_value, not from a name set on it, and the answer says under _note how many text elements on screen have none. With no control focused, the engine reports the first node under the root as focused, which in a run with the runtime is GdharnessRuntime. Needs a windowed run on Windows started with engineArgs ["--accessibility", "always"] or with the project's accessibility/general/accessibility_support at Always Active, since the engine builds the tree only when asked; reading it takes a second or two, starting PowerShell inside the game so it reaches the game's own desktop`,
|
|
35246
35344
|
requires: []
|
|
35247
35345
|
}
|
|
35248
35346
|
},
|
|
@@ -35268,7 +35366,7 @@ var TOOL_SPECS = [
|
|
|
35268
35366
|
value: {
|
|
35269
35367
|
ops: ["set"],
|
|
35270
35368
|
blank: true,
|
|
35271
|
-
description: `set: the value, fitted to the property's type. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
|
|
35369
|
+
description: `set: the value, fitted to the property's type. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
|
|
35272
35370
|
},
|
|
35273
35371
|
method: {
|
|
35274
35372
|
type: "string",
|
|
@@ -35278,7 +35376,7 @@ var TOOL_SPECS = [
|
|
|
35278
35376
|
args: {
|
|
35279
35377
|
type: "array",
|
|
35280
35378
|
ops: ["call"],
|
|
35281
|
-
description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
|
|
35379
|
+
description: `call: the arguments, fitted to the method's parameter types. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
|
|
35282
35380
|
},
|
|
35283
35381
|
properties: {
|
|
35284
35382
|
type: ["boolean", "array"],
|
|
@@ -35372,7 +35470,7 @@ var TOOL_SPECS = [
|
|
|
35372
35470
|
name: "runtime_input",
|
|
35373
35471
|
title: "Send input to the game",
|
|
35374
35472
|
description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position.",
|
|
35375
|
-
answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens.',
|
|
35473
|
+
answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again, for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
|
|
35376
35474
|
parameters: {
|
|
35377
35475
|
projectPath: RUNNING_PROJECT_PATH,
|
|
35378
35476
|
pid: RUNNING_PID,
|
|
@@ -35393,8 +35491,8 @@ var TOOL_SPECS = [
|
|
|
35393
35491
|
},
|
|
35394
35492
|
pressed: {
|
|
35395
35493
|
type: "boolean",
|
|
35396
|
-
ops: ["action", "key", "mouse_click"],
|
|
35397
|
-
description: "action, key, mouse_click: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. A wheel button held down is still a whole step, since a wheel is never held."
|
|
35494
|
+
ops: ["action", "key", "mouse_click", "joypad_button"],
|
|
35495
|
+
description: "action, key, mouse_click, joypad_button: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. A wheel button held down is still a whole step, since a wheel is never held."
|
|
35398
35496
|
},
|
|
35399
35497
|
strength: {
|
|
35400
35498
|
type: "number",
|
|
@@ -35439,8 +35537,26 @@ var TOOL_SPECS = [
|
|
|
35439
35537
|
},
|
|
35440
35538
|
button: {
|
|
35441
35539
|
type: ["string", "number"],
|
|
35442
|
-
ops: ["click", "mouse_click"],
|
|
35443
|
-
description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left."
|
|
35540
|
+
ops: ["click", "mouse_click", "joypad_button"],
|
|
35541
|
+
description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left. joypad_button: the controller button, by name (a, b, x, y, back, guide, start, left_stick, right_stick, left_shoulder, right_shoulder, dpad_up, dpad_down, dpad_left, dpad_right, misc1, paddle1 to paddle4, touchpad) or by its JoyButton number; a, b, x and y are the bottom, right, left and top face buttons, as Godot names them, whatever the controller prints on them."
|
|
35542
|
+
},
|
|
35543
|
+
axis: {
|
|
35544
|
+
type: ["string", "number"],
|
|
35545
|
+
ops: ["joypad_motion"],
|
|
35546
|
+
description: "joypad_motion: the stick or trigger axis, by name (left_x, left_y, right_x, right_y, trigger_left, trigger_right) or by its JoyAxis number."
|
|
35547
|
+
},
|
|
35548
|
+
value: {
|
|
35549
|
+
type: "number",
|
|
35550
|
+
minimum: -1,
|
|
35551
|
+
maximum: 1,
|
|
35552
|
+
ops: ["joypad_motion"],
|
|
35553
|
+
description: "joypad_motion: where the axis is, -1 to 1: down and right are positive on a stick, and a trigger runs 0 to 1. The axis stays there until another motion moves it, as a stick held over does; send 0 to let it go back."
|
|
35554
|
+
},
|
|
35555
|
+
device: {
|
|
35556
|
+
type: "number",
|
|
35557
|
+
minimum: 0,
|
|
35558
|
+
ops: ["joypad_button", "joypad_motion"],
|
|
35559
|
+
description: "joypad_button, joypad_motion: which controller, counting from 0. Default 0."
|
|
35444
35560
|
},
|
|
35445
35561
|
doubleClick: {
|
|
35446
35562
|
type: "boolean",
|
|
@@ -35490,6 +35606,16 @@ var TOOL_SPECS = [
|
|
|
35490
35606
|
effect: "additive",
|
|
35491
35607
|
summary: "move the mouse to a position, carrying the distance from where the last injected event put it unless relativeX and relativeY say otherwise, and the buttons held, so a control that drags moves under a run of these between a held mouse_click and its release",
|
|
35492
35608
|
requires: ["x", "y"]
|
|
35609
|
+
},
|
|
35610
|
+
joypad_button: {
|
|
35611
|
+
effect: "additive",
|
|
35612
|
+
summary: "press a controller button, or hold it, as an InputEventJoypadButton, so a game reading the pad in _input or _unhandled_input gets what a controller sends; an action bound to the button fires with it, as it would for a real one. Pressed and released a frame apart unless pressed says otherwise",
|
|
35613
|
+
requires: ["button"]
|
|
35614
|
+
},
|
|
35615
|
+
joypad_motion: {
|
|
35616
|
+
effect: "additive",
|
|
35617
|
+
summary: "move a stick or trigger axis to a value, as an InputEventJoypadMotion, which is what a game reading the sticks gets; the axis stays there until another motion moves it",
|
|
35618
|
+
requires: ["axis", "value"]
|
|
35493
35619
|
}
|
|
35494
35620
|
}
|
|
35495
35621
|
},
|
|
@@ -35696,15 +35822,15 @@ import { createHash as createHash4 } from "node:crypto";
|
|
|
35696
35822
|
import {
|
|
35697
35823
|
cpSync,
|
|
35698
35824
|
existsSync as existsSync16,
|
|
35699
|
-
mkdirSync as
|
|
35825
|
+
mkdirSync as mkdirSync13,
|
|
35700
35826
|
readdirSync as readdirSync8,
|
|
35701
35827
|
readFileSync as readFileSync20,
|
|
35702
35828
|
rmdirSync,
|
|
35703
35829
|
rmSync as rmSync11,
|
|
35704
|
-
statSync as
|
|
35830
|
+
statSync as statSync8,
|
|
35705
35831
|
writeFileSync as writeFileSync14
|
|
35706
35832
|
} from "node:fs";
|
|
35707
|
-
import { dirname as
|
|
35833
|
+
import { dirname as dirname15, join as join22 } from "node:path";
|
|
35708
35834
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
35709
35835
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
35710
35836
|
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
@@ -35727,9 +35853,9 @@ function addonsDigest(names, from) {
|
|
|
35727
35853
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
35728
35854
|
continue;
|
|
35729
35855
|
}
|
|
35730
|
-
const path =
|
|
35856
|
+
const path = join22(directory, name);
|
|
35731
35857
|
const inside = `${relative}/${name}`;
|
|
35732
|
-
if (
|
|
35858
|
+
if (statSync8(path).isDirectory()) {
|
|
35733
35859
|
walk(path, inside);
|
|
35734
35860
|
} else {
|
|
35735
35861
|
hash.update(`${inside}
|
|
@@ -35741,7 +35867,7 @@ function addonsDigest(names, from) {
|
|
|
35741
35867
|
}
|
|
35742
35868
|
};
|
|
35743
35869
|
for (const name of names) {
|
|
35744
|
-
walk(
|
|
35870
|
+
walk(join22(from, name), name);
|
|
35745
35871
|
}
|
|
35746
35872
|
return hash.digest("hex");
|
|
35747
35873
|
}
|
|
@@ -35774,7 +35900,7 @@ function installedRuntimeDigest(projectPath) {
|
|
|
35774
35900
|
return digestMarker(projectPath, RUNTIME_ADDON);
|
|
35775
35901
|
}
|
|
35776
35902
|
function digestMarker(projectPath, addon) {
|
|
35777
|
-
const marker =
|
|
35903
|
+
const marker = join22(projectPath, "addons", addon, DIGEST_MARKER);
|
|
35778
35904
|
if (!existsSync16(marker)) {
|
|
35779
35905
|
return null;
|
|
35780
35906
|
}
|
|
@@ -35782,10 +35908,10 @@ function digestMarker(projectPath, addon) {
|
|
|
35782
35908
|
return said === "" ? null : said;
|
|
35783
35909
|
}
|
|
35784
35910
|
function shippedAddonsDirectory() {
|
|
35785
|
-
return
|
|
35911
|
+
return join22(dirname15(fileURLToPath3(import.meta.url)), "godot", "addons");
|
|
35786
35912
|
}
|
|
35787
35913
|
function installedAddonVersion(projectPath) {
|
|
35788
|
-
const marker =
|
|
35914
|
+
const marker = join22(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
35789
35915
|
if (!existsSync16(marker)) {
|
|
35790
35916
|
return null;
|
|
35791
35917
|
}
|
|
@@ -35800,7 +35926,7 @@ function loaderAmong(projectPath, named) {
|
|
|
35800
35926
|
}
|
|
35801
35927
|
let source = null;
|
|
35802
35928
|
try {
|
|
35803
|
-
source = readFileSync20(
|
|
35929
|
+
source = readFileSync20(join22(projectPath, path), "utf8");
|
|
35804
35930
|
} catch {}
|
|
35805
35931
|
if (source?.includes(RUNTIME_AUTOLOAD.path)) {
|
|
35806
35932
|
return { name: entry, path, exists: true };
|
|
@@ -35842,8 +35968,8 @@ function enabledPlugins(settings) {
|
|
|
35842
35968
|
function inspectProject(projectPath) {
|
|
35843
35969
|
const problems = [];
|
|
35844
35970
|
const addons = ADDONS.map((name) => {
|
|
35845
|
-
const directory =
|
|
35846
|
-
const marker =
|
|
35971
|
+
const directory = join22(projectPath, "addons", name);
|
|
35972
|
+
const marker = join22(directory, VERSION_MARKER);
|
|
35847
35973
|
const installed = existsSync16(directory);
|
|
35848
35974
|
const version = existsSync16(marker) ? readFileSync20(marker, "utf8").trim() : null;
|
|
35849
35975
|
const current = version === SERVER_VERSION;
|
|
@@ -35854,7 +35980,7 @@ function inspectProject(projectPath) {
|
|
|
35854
35980
|
}
|
|
35855
35981
|
return { name, installed, version, current };
|
|
35856
35982
|
});
|
|
35857
|
-
const settings = parseProjectGodot(readFileSync20(
|
|
35983
|
+
const settings = parseProjectGodot(readFileSync20(join22(projectPath, "project.godot"), "utf8"));
|
|
35858
35984
|
const pluginsEnabled = enabledPlugins(settings);
|
|
35859
35985
|
for (const name of EDITOR_PLUGINS) {
|
|
35860
35986
|
if (!pluginsEnabled.includes(name)) {
|
|
@@ -35870,7 +35996,7 @@ function inspectProject(projectPath) {
|
|
|
35870
35996
|
const tracked = trackedByGit(projectPath, [...named.values()]);
|
|
35871
35997
|
const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
|
|
35872
35998
|
for (const [name, path] of named) {
|
|
35873
|
-
if (!existsSync16(
|
|
35999
|
+
if (!existsSync16(join22(projectPath, path))) {
|
|
35874
36000
|
if (!addonMissing(path)) {
|
|
35875
36001
|
problems.push(`the ${name} autoload names res://${path}, which is not in this project, so it boots with a missing script; restore that file, or point the autoload at one that is here`);
|
|
35876
36002
|
}
|
|
@@ -35921,7 +36047,8 @@ function argumentShape(schema) {
|
|
|
35921
36047
|
var UPDATE_NOTICE_EVERY = 500;
|
|
35922
36048
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
35923
36049
|
var run4 = promisify5(execFile5);
|
|
35924
|
-
var __dirname2 =
|
|
36050
|
+
var __dirname2 = dirname16(fileURLToPath4(import.meta.url));
|
|
36051
|
+
var PARENT_ASKED_AFTER_MS = 5000;
|
|
35925
36052
|
function editorRestartTimeoutMs() {
|
|
35926
36053
|
return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
|
|
35927
36054
|
}
|
|
@@ -35958,11 +36085,11 @@ function writtenFileRefusal(path, rule) {
|
|
|
35958
36085
|
if (!rule.extensions.includes(extname3(path).toLowerCase())) {
|
|
35959
36086
|
return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
|
|
35960
36087
|
}
|
|
35961
|
-
const directory = realPathOr(
|
|
36088
|
+
const directory = realPathOr(dirname16(path));
|
|
35962
36089
|
if (!existsSync17(directory)) {
|
|
35963
|
-
return `${named}'s directory ${
|
|
36090
|
+
return `${named}'s directory ${dirname16(path)} does not exist; ${what} creates no directories.`;
|
|
35964
36091
|
}
|
|
35965
|
-
const where =
|
|
36092
|
+
const where = join23(directory, basename5(path));
|
|
35966
36093
|
const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
35967
36094
|
if (inside !== undefined) {
|
|
35968
36095
|
return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
|
|
@@ -36512,6 +36639,23 @@ function engineFlagsNote(args) {
|
|
|
36512
36639
|
var EDITOR_FREE_INSTEAD = {
|
|
36513
36640
|
scene_tree: "runtime_inspect tree reads the nodes of a game that is running, and needs no editor; scene_tree reads a scene file through the editor"
|
|
36514
36641
|
};
|
|
36642
|
+
function editorAddonMissing(projectPath) {
|
|
36643
|
+
if (!existsSync17(join23(projectPath, "project.godot"))) {
|
|
36644
|
+
return null;
|
|
36645
|
+
}
|
|
36646
|
+
const report = inspectProject(projectPath);
|
|
36647
|
+
const installed = report.addons.some((addon) => addon.name === "gdharness_editor" && addon.installed);
|
|
36648
|
+
if (installed && report.pluginsEnabled.includes("gdharness_editor")) {
|
|
36649
|
+
return null;
|
|
36650
|
+
}
|
|
36651
|
+
return {
|
|
36652
|
+
why: `the gdharness editor addon is not ${installed ? "enabled" : "installed"} in ${projectPath}`,
|
|
36653
|
+
remedies: installed ? [
|
|
36654
|
+
"gdharness setup in the project enables it",
|
|
36655
|
+
"Or enable it under Project Settings, Plugins, in an editor opened by hand"
|
|
36656
|
+
] : ["gdharness setup in the project installs and enables it"]
|
|
36657
|
+
};
|
|
36658
|
+
}
|
|
36515
36659
|
var OPS_ELSEWHERE = {
|
|
36516
36660
|
runtime_invoke: { get: "runtime_inspect property", property: "runtime_inspect property" }
|
|
36517
36661
|
};
|
|
@@ -36778,7 +36922,7 @@ function profileFrameFunctions(projectPath) {
|
|
|
36778
36922
|
let projectGodot = null;
|
|
36779
36923
|
if (projectPath !== null) {
|
|
36780
36924
|
try {
|
|
36781
|
-
projectGodot = readFileSync21(
|
|
36925
|
+
projectGodot = readFileSync21(join23(projectPath, "project.godot"), "utf8");
|
|
36782
36926
|
} catch {}
|
|
36783
36927
|
}
|
|
36784
36928
|
return frameFunctionsFor(queuedMessagesOf(projectGodot));
|
|
@@ -36956,7 +37100,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
|
|
|
36956
37100
|
var ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd";
|
|
36957
37101
|
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
36958
37102
|
try {
|
|
36959
|
-
return settingKeys(readFileSync21(
|
|
37103
|
+
return settingKeys(readFileSync21(join23(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
36960
37104
|
} catch {
|
|
36961
37105
|
return true;
|
|
36962
37106
|
}
|
|
@@ -36971,7 +37115,7 @@ function uncaughtNote(uncaught) {
|
|
|
36971
37115
|
}
|
|
36972
37116
|
function elsewhereIn(projectPath, asked) {
|
|
36973
37117
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
36974
|
-
if (wanted === "" || existsSync17(
|
|
37118
|
+
if (wanted === "" || existsSync17(join23(projectPath, wanted))) {
|
|
36975
37119
|
return "";
|
|
36976
37120
|
}
|
|
36977
37121
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -37062,13 +37206,13 @@ function scriptsToDiagnose(projectPath, given) {
|
|
|
37062
37206
|
var EXTERNAL_MEMBER = 'Could not resolve external class member "';
|
|
37063
37207
|
function isDirectory(path) {
|
|
37064
37208
|
try {
|
|
37065
|
-
return
|
|
37209
|
+
return statSync9(path).isDirectory();
|
|
37066
37210
|
} catch {
|
|
37067
37211
|
return false;
|
|
37068
37212
|
}
|
|
37069
37213
|
}
|
|
37070
37214
|
function scriptsUnder(directory, prefix) {
|
|
37071
|
-
if (existsSync17(
|
|
37215
|
+
if (existsSync17(join23(directory, ".gdignore"))) {
|
|
37072
37216
|
return [];
|
|
37073
37217
|
}
|
|
37074
37218
|
const found = [];
|
|
@@ -37078,7 +37222,7 @@ function scriptsUnder(directory, prefix) {
|
|
|
37078
37222
|
}
|
|
37079
37223
|
const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
37080
37224
|
if (entry.isDirectory()) {
|
|
37081
|
-
found.push(...scriptsUnder(
|
|
37225
|
+
found.push(...scriptsUnder(join23(directory, entry.name), path));
|
|
37082
37226
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
37083
37227
|
found.push(path);
|
|
37084
37228
|
}
|
|
@@ -37094,7 +37238,7 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
37094
37238
|
return false;
|
|
37095
37239
|
}
|
|
37096
37240
|
try {
|
|
37097
|
-
return readFileSync21(
|
|
37241
|
+
return readFileSync21(join23(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
37098
37242
|
} catch {
|
|
37099
37243
|
return false;
|
|
37100
37244
|
}
|
|
@@ -37120,7 +37264,7 @@ var STATIC_LIST_HINT = { ttlMs: 5 * 60000, cacheScope: "private" };
|
|
|
37120
37264
|
class GodotServer {
|
|
37121
37265
|
stdio = null;
|
|
37122
37266
|
locator = new GodotLocator;
|
|
37123
|
-
operationsScript =
|
|
37267
|
+
operationsScript = join23(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
37124
37268
|
godotBridge;
|
|
37125
37269
|
tools = buildToolDefinitions();
|
|
37126
37270
|
activeProcess = null;
|
|
@@ -37144,6 +37288,11 @@ class GodotServer {
|
|
|
37144
37288
|
launchedFrom = null;
|
|
37145
37289
|
launchedDropped = null;
|
|
37146
37290
|
closedHere = null;
|
|
37291
|
+
inFlight = new Map;
|
|
37292
|
+
callsTaken = 0;
|
|
37293
|
+
clientNoted = null;
|
|
37294
|
+
parentCommand = null;
|
|
37295
|
+
endNoted = false;
|
|
37147
37296
|
ownProject;
|
|
37148
37297
|
announcedAt = null;
|
|
37149
37298
|
handingOverFrom = null;
|
|
@@ -37180,6 +37329,7 @@ class GodotServer {
|
|
|
37180
37329
|
return { content: [{ type: "text", text: answerJson(payload) }] };
|
|
37181
37330
|
}
|
|
37182
37331
|
async run() {
|
|
37332
|
+
this.noteTheStart();
|
|
37183
37333
|
const located = await this.locator.find();
|
|
37184
37334
|
if (!located.ok) {
|
|
37185
37335
|
console.error(`[SERVER] ${GodotLocator.refusal(located).message} Until that is fixed every tool that runs the engine will say so.`);
|
|
@@ -37296,6 +37446,7 @@ class GodotServer {
|
|
|
37296
37446
|
return;
|
|
37297
37447
|
}
|
|
37298
37448
|
console.error(`[SERVER] pid ${successor} now serves ${this.ownProject}; this server is finished and is stopping.`);
|
|
37449
|
+
this.noteTheEnd("superseded", { successor });
|
|
37299
37450
|
this.handleShutdown(`superseded by ${successor}`, 0);
|
|
37300
37451
|
}, SUCCESSOR_CHECK_MS);
|
|
37301
37452
|
watch.unref();
|
|
@@ -37367,10 +37518,67 @@ class GodotServer {
|
|
|
37367
37518
|
}
|
|
37368
37519
|
await this.stdio?.close();
|
|
37369
37520
|
}
|
|
37521
|
+
noteTheStart() {
|
|
37522
|
+
const cwd = process.cwd();
|
|
37523
|
+
const mcpJson = join23(cwd, ".mcp.json");
|
|
37524
|
+
const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
|
|
37525
|
+
const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
|
|
37526
|
+
writeLifecycle("started", {
|
|
37527
|
+
argv: process.argv.slice(1),
|
|
37528
|
+
cwd,
|
|
37529
|
+
project: this.ownProject,
|
|
37530
|
+
mcpJson: existsSync17(mcpJson) ? mcpJson : null
|
|
37531
|
+
});
|
|
37532
|
+
if (predecessor !== null) {
|
|
37533
|
+
writeLifecycle("replaced", { project: this.ownProject, predecessor });
|
|
37534
|
+
}
|
|
37535
|
+
const ask = setTimeout(() => {
|
|
37536
|
+
processTree().then((tree) => {
|
|
37537
|
+
this.parentCommand = tree?.get(process.ppid)?.command ?? null;
|
|
37538
|
+
writeLifecycle("parent", {
|
|
37539
|
+
command: this.parentCommand,
|
|
37540
|
+
...tree === undefined ? { why: whyTheProcessTreeFailed() } : {}
|
|
37541
|
+
});
|
|
37542
|
+
});
|
|
37543
|
+
}, PARENT_ASKED_AFTER_MS);
|
|
37544
|
+
ask.unref();
|
|
37545
|
+
}
|
|
37546
|
+
noteTheEnd(cause, more = {}) {
|
|
37547
|
+
if (this.endNoted) {
|
|
37548
|
+
return;
|
|
37549
|
+
}
|
|
37550
|
+
this.endNoted = true;
|
|
37551
|
+
writeLifecycle("ended", {
|
|
37552
|
+
cause,
|
|
37553
|
+
project: this.ownProject,
|
|
37554
|
+
...this.parentCommand === null ? {} : { parentCommand: this.parentCommand },
|
|
37555
|
+
...this.clientNoted === null ? {} : { client: this.clientNoted },
|
|
37556
|
+
inFlight: callsInFlight(this.inFlight.values()),
|
|
37557
|
+
...more
|
|
37558
|
+
});
|
|
37559
|
+
}
|
|
37560
|
+
noteTheClient(client) {
|
|
37561
|
+
if (client === undefined) {
|
|
37562
|
+
return;
|
|
37563
|
+
}
|
|
37564
|
+
const said = `${client.name} ${client.version}`;
|
|
37565
|
+
if (said === this.clientNoted) {
|
|
37566
|
+
return;
|
|
37567
|
+
}
|
|
37568
|
+
this.clientNoted = said;
|
|
37569
|
+
writeLifecycle("client", { client: { name: client.name, version: client.version } });
|
|
37570
|
+
}
|
|
37370
37571
|
setupShutdownHandlers() {
|
|
37371
37572
|
const requestShutdown = (source, exitCode) => {
|
|
37372
37573
|
this.handleShutdown(source, exitCode);
|
|
37373
37574
|
};
|
|
37575
|
+
process.on("uncaughtException", (error, origin) => {
|
|
37576
|
+
this.noteTheEnd(origin === "unhandledRejection" ? "unhandledRejection" : "uncaughtException", {
|
|
37577
|
+
error: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
37578
|
+
});
|
|
37579
|
+
console.error(error);
|
|
37580
|
+
process.exit(1);
|
|
37581
|
+
});
|
|
37374
37582
|
process.once("SIGINT", () => {
|
|
37375
37583
|
requestShutdown("SIGINT", 0);
|
|
37376
37584
|
});
|
|
@@ -37383,7 +37591,8 @@ class GodotServer {
|
|
|
37383
37591
|
process.once("beforeExit", (code) => {
|
|
37384
37592
|
requestShutdown(`beforeExit:${code}`);
|
|
37385
37593
|
});
|
|
37386
|
-
process.once("exit", () => {
|
|
37594
|
+
process.once("exit", (code) => {
|
|
37595
|
+
this.noteTheEnd(`exit:${code}`);
|
|
37387
37596
|
this.forceCleanupOnExit();
|
|
37388
37597
|
});
|
|
37389
37598
|
}
|
|
@@ -37392,6 +37601,7 @@ class GodotServer {
|
|
|
37392
37601
|
return;
|
|
37393
37602
|
}
|
|
37394
37603
|
this.shutdownInitiated = true;
|
|
37604
|
+
this.noteTheEnd(source);
|
|
37395
37605
|
this.logDebug(`Shutting down server via ${source}`);
|
|
37396
37606
|
try {
|
|
37397
37607
|
await this.cleanup();
|
|
@@ -37431,9 +37641,23 @@ class GodotServer {
|
|
|
37431
37641
|
return mcp;
|
|
37432
37642
|
}
|
|
37433
37643
|
setupToolHandlers(protocol) {
|
|
37434
|
-
|
|
37644
|
+
const clientOf = (envelope) => {
|
|
37645
|
+
const named = envelope?.["io.modelcontextprotocol/clientInfo"];
|
|
37646
|
+
if (typeof named === "object" && named !== null) {
|
|
37647
|
+
const { name, version } = named;
|
|
37648
|
+
if (typeof name === "string" && typeof version === "string") {
|
|
37649
|
+
return { name, version };
|
|
37650
|
+
}
|
|
37651
|
+
}
|
|
37652
|
+
return protocol.getClientVersion();
|
|
37653
|
+
};
|
|
37654
|
+
protocol.setRequestHandler("tools/list", (_request, ctx) => {
|
|
37655
|
+
this.noteTheClient(clientOf(ctx.mcpReq.envelope));
|
|
37656
|
+
return { tools: this.tools };
|
|
37657
|
+
});
|
|
37435
37658
|
protocol.setRequestHandler("tools/call", async (request, ctx) => {
|
|
37436
37659
|
this.logDebug(`Handling tool request: ${request.params.name}`);
|
|
37660
|
+
this.noteTheClient(clientOf(ctx.mcpReq.envelope));
|
|
37437
37661
|
const spec = toolSpec(request.params.name);
|
|
37438
37662
|
if (!spec) {
|
|
37439
37663
|
throw new ProtocolError(ProtocolErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}. Tools: ${TOOL_SPECS.map((tool) => tool.name).join(", ")}`);
|
|
@@ -37444,7 +37668,7 @@ class GodotServer {
|
|
|
37444
37668
|
return checked.response;
|
|
37445
37669
|
}
|
|
37446
37670
|
const named = args["projectPath"];
|
|
37447
|
-
if (typeof named === "string" && existsSync17(
|
|
37671
|
+
if (typeof named === "string" && existsSync17(join23(named, "project.godot"))) {
|
|
37448
37672
|
this.lastProjectPath = named;
|
|
37449
37673
|
}
|
|
37450
37674
|
this.updates.refresh();
|
|
@@ -37457,8 +37681,14 @@ class GodotServer {
|
|
|
37457
37681
|
this.logDebug(`Progress for ${spec.name} was not sent: ${errorMessage(error)}`);
|
|
37458
37682
|
});
|
|
37459
37683
|
};
|
|
37460
|
-
const
|
|
37461
|
-
|
|
37684
|
+
const call = this.callsTaken++;
|
|
37685
|
+
this.inFlight.set(call, { tool: spec.name, op: checked.op ?? "", startedAt: Date.now() });
|
|
37686
|
+
try {
|
|
37687
|
+
const answer = await withCallSignal(ctx.mcpReq.signal, () => this.answered(spec.name, checked.op ?? "", args), progress);
|
|
37688
|
+
return this.withFeedbackNotice(this.withUpdateNotice(answer));
|
|
37689
|
+
} finally {
|
|
37690
|
+
this.inFlight.delete(call);
|
|
37691
|
+
}
|
|
37462
37692
|
});
|
|
37463
37693
|
}
|
|
37464
37694
|
async answered(tool, op, args) {
|
|
@@ -37677,10 +37907,12 @@ class GodotServer {
|
|
|
37677
37907
|
]);
|
|
37678
37908
|
}
|
|
37679
37909
|
if (!this.godotBridge.isConnected()) {
|
|
37680
|
-
|
|
37681
|
-
|
|
37682
|
-
'Open the project with editor_launch, or pass from: "disk" to read the file instead'
|
|
37910
|
+
const refused = await this.noEditorYet(answerable, [
|
|
37911
|
+
`${tool} ${op} was asked of the editor; from: "disk" reads the file instead`
|
|
37683
37912
|
]);
|
|
37913
|
+
if (refused !== null) {
|
|
37914
|
+
return refused;
|
|
37915
|
+
}
|
|
37684
37916
|
}
|
|
37685
37917
|
const { op: _dropped, ...asks } = answerable;
|
|
37686
37918
|
const contained = this.containProjectFiles(asks);
|
|
@@ -37765,11 +37997,11 @@ class GodotServer {
|
|
|
37765
37997
|
case "script_info":
|
|
37766
37998
|
switch (op) {
|
|
37767
37999
|
case "symbols":
|
|
37768
|
-
return await this.
|
|
38000
|
+
return await this.askScriptInfo("lsp_get_symbols", "Symbols", args);
|
|
37769
38001
|
case "completion":
|
|
37770
|
-
return await this.
|
|
38002
|
+
return await this.askScriptInfo("lsp_get_completions", "Completion", args);
|
|
37771
38003
|
default:
|
|
37772
|
-
return await this.
|
|
38004
|
+
return await this.askScriptInfo("lsp_get_hover", "Hover", args);
|
|
37773
38005
|
}
|
|
37774
38006
|
case "script_diagnostics":
|
|
37775
38007
|
return await this.handleScriptDiagnostics(args);
|
|
@@ -37842,6 +38074,12 @@ class GodotServer {
|
|
|
37842
38074
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37843
38075
|
property: readNonEmptyString(args, "property") ?? ""
|
|
37844
38076
|
}, { tool: "runtime_inspect", op, patience });
|
|
38077
|
+
case "accessibility":
|
|
38078
|
+
return await this.handleRuntimeCommand("get_accessibility", whichGame(args), {
|
|
38079
|
+
tool: "runtime_inspect",
|
|
38080
|
+
op,
|
|
38081
|
+
patience: readPositiveNumber(args, "timeoutMs") ?? Math.max(30000, this.runtimeTimeoutMs())
|
|
38082
|
+
});
|
|
37845
38083
|
default:
|
|
37846
38084
|
return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, { tool: "runtime_inspect", op, patience });
|
|
37847
38085
|
}
|
|
@@ -37946,7 +38184,7 @@ class GodotServer {
|
|
|
37946
38184
|
if (path === undefined) {
|
|
37947
38185
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
37948
38186
|
}
|
|
37949
|
-
const file =
|
|
38187
|
+
const file = join23(path, "project.godot");
|
|
37950
38188
|
if (!existsSync17(file)) {
|
|
37951
38189
|
return {
|
|
37952
38190
|
ok: false,
|
|
@@ -38633,18 +38871,18 @@ class GodotServer {
|
|
|
38633
38871
|
if (!engine.ok) {
|
|
38634
38872
|
return engine.response;
|
|
38635
38873
|
}
|
|
38636
|
-
const folder =
|
|
38874
|
+
const folder = dirname16(output.absolutePath);
|
|
38637
38875
|
try {
|
|
38638
|
-
|
|
38876
|
+
mkdirSync14(folder, { recursive: true });
|
|
38639
38877
|
} catch (error) {
|
|
38640
|
-
return this.createErrorResponse(`Cannot create ${
|
|
38878
|
+
return this.createErrorResponse(`Cannot create ${dirname16(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
38641
38879
|
}
|
|
38642
38880
|
const debug = readBoolean(args, "debug") ?? false;
|
|
38643
38881
|
const exportLogs = scratchDirectory("export");
|
|
38644
38882
|
const exportArgs = [
|
|
38645
38883
|
"--headless",
|
|
38646
38884
|
"--log-file",
|
|
38647
|
-
|
|
38885
|
+
join23(exportLogs, "engine.log"),
|
|
38648
38886
|
"--path",
|
|
38649
38887
|
project.value.path,
|
|
38650
38888
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -38652,7 +38890,7 @@ class GodotServer {
|
|
|
38652
38890
|
output.absolutePath
|
|
38653
38891
|
];
|
|
38654
38892
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
38655
|
-
const writtenAt = () => existsSync17(output.absolutePath) ?
|
|
38893
|
+
const writtenAt = () => existsSync17(output.absolutePath) ? statSync9(output.absolutePath).mtimeMs : null;
|
|
38656
38894
|
const before = writtenAt();
|
|
38657
38895
|
let ending;
|
|
38658
38896
|
try {
|
|
@@ -38716,10 +38954,10 @@ class GodotServer {
|
|
|
38716
38954
|
if (movie !== null) {
|
|
38717
38955
|
return this.createErrorResponse(movie);
|
|
38718
38956
|
}
|
|
38719
|
-
const missing = paths.value.filter((path) => !existsSync17(
|
|
38957
|
+
const missing = paths.value.filter((path) => !existsSync17(join23(project.value.path, path.slice("res://".length))));
|
|
38720
38958
|
const asked = paths.value.filter((path) => !missing.includes(path));
|
|
38721
38959
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
38722
|
-
const noRunner = !existsSync17(
|
|
38960
|
+
const noRunner = !existsSync17(join23(project.value.path, runner));
|
|
38723
38961
|
if (asked.length === 0) {
|
|
38724
38962
|
const verdict = `nothing at ${missing.join(" and ")}`;
|
|
38725
38963
|
const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
|
|
@@ -38810,21 +39048,21 @@ class GodotServer {
|
|
|
38810
39048
|
await untilGone([run.process.pid]);
|
|
38811
39049
|
}
|
|
38812
39050
|
await run.settled;
|
|
38813
|
-
const reportsDir =
|
|
39051
|
+
const reportsDir = join23(project.value.path, ".godot", ...ours.split("/"));
|
|
38814
39052
|
let report = null;
|
|
38815
39053
|
let reportProblem = null;
|
|
38816
39054
|
try {
|
|
38817
39055
|
const written = existsSync17(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
38818
39056
|
const newest = written.at(-1);
|
|
38819
39057
|
if (newest !== undefined) {
|
|
38820
|
-
report = parseJUnit(readFileSync21(
|
|
39058
|
+
report = parseJUnit(readFileSync21(join23(reportsDir, newest, "results.xml"), "utf8"));
|
|
38821
39059
|
}
|
|
38822
39060
|
} catch (error) {
|
|
38823
39061
|
reportProblem = errorMessage(error);
|
|
38824
39062
|
} finally {
|
|
38825
39063
|
discard(reportsDir, userData);
|
|
38826
39064
|
try {
|
|
38827
|
-
rmdirSync2(
|
|
39065
|
+
rmdirSync2(dirname16(reportsDir));
|
|
38828
39066
|
} catch {}
|
|
38829
39067
|
}
|
|
38830
39068
|
const engineEntries = foldedRepeats(run.log.select({
|
|
@@ -39056,31 +39294,9 @@ class GodotServer {
|
|
|
39056
39294
|
async diagnosticsOf(args) {
|
|
39057
39295
|
const answer = await this.handleLSP("lsp_get_diagnostics", args);
|
|
39058
39296
|
const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
39059
|
-
|
|
39060
|
-
|
|
39061
|
-
|
|
39062
|
-
return { refusal: this.createErrorResponse(reason), perScript: true };
|
|
39063
|
-
}
|
|
39064
|
-
if (payload["stillReading"] === true) {
|
|
39065
|
-
return { refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`), perScript: false };
|
|
39066
|
-
}
|
|
39067
|
-
if (typeof payload["serves"] === "string") {
|
|
39068
|
-
return {
|
|
39069
|
-
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
39070
|
-
"editor_status names the editor on the bridge and the port it says it serves",
|
|
39071
|
-
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
39072
|
-
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
39073
|
-
]),
|
|
39074
|
-
perScript: false
|
|
39075
|
-
};
|
|
39076
|
-
}
|
|
39077
|
-
return {
|
|
39078
|
-
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
39079
|
-
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
39080
|
-
"GDHARNESS_LSP_PORT points this server at another one"
|
|
39081
|
-
]),
|
|
39082
|
-
perScript: payload["aboutThisFile"] === true
|
|
39083
|
-
};
|
|
39297
|
+
const refused = this.languageServerRefusal(payload, "Diagnostics");
|
|
39298
|
+
if (refused !== null) {
|
|
39299
|
+
return refused;
|
|
39084
39300
|
}
|
|
39085
39301
|
const diagnostics = readArray(payload, "diagnostics") ?? [];
|
|
39086
39302
|
const errors = diagnostics.filter((entry) => asParams(entry)["severity"] === "error").length;
|
|
@@ -39187,6 +39403,40 @@ class GodotServer {
|
|
|
39187
39403
|
async handleLSP(toolName, args) {
|
|
39188
39404
|
return handleLSPTool(await this.lsp(), toolName, args);
|
|
39189
39405
|
}
|
|
39406
|
+
async askScriptInfo(toolName, asked, args) {
|
|
39407
|
+
const answer = await this.handleLSP(toolName, args);
|
|
39408
|
+
const refused = this.languageServerRefusal(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), asked);
|
|
39409
|
+
return refused === null ? answer : refused.refusal;
|
|
39410
|
+
}
|
|
39411
|
+
languageServerRefusal(payload, asked) {
|
|
39412
|
+
if (payload["error"] === undefined) {
|
|
39413
|
+
return null;
|
|
39414
|
+
}
|
|
39415
|
+
const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
|
|
39416
|
+
if (payload["refusedArguments"] === true) {
|
|
39417
|
+
return { refusal: this.createErrorResponse(reason), perScript: true };
|
|
39418
|
+
}
|
|
39419
|
+
if (payload["stillReading"] === true) {
|
|
39420
|
+
return { refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`), perScript: false };
|
|
39421
|
+
}
|
|
39422
|
+
if (typeof payload["serves"] === "string") {
|
|
39423
|
+
return {
|
|
39424
|
+
refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
|
|
39425
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
39426
|
+
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
39427
|
+
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
39428
|
+
]),
|
|
39429
|
+
perScript: false
|
|
39430
|
+
};
|
|
39431
|
+
}
|
|
39432
|
+
return {
|
|
39433
|
+
refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
|
|
39434
|
+
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
39435
|
+
"GDHARNESS_LSP_PORT points this server at another one"
|
|
39436
|
+
]),
|
|
39437
|
+
perScript: payload["aboutThisFile"] === true
|
|
39438
|
+
};
|
|
39439
|
+
}
|
|
39190
39440
|
async lsp() {
|
|
39191
39441
|
const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
|
|
39192
39442
|
if (this.lspClient !== null && this.lspClient.port !== port) {
|
|
@@ -39287,7 +39537,7 @@ class GodotServer {
|
|
|
39287
39537
|
return;
|
|
39288
39538
|
}
|
|
39289
39539
|
try {
|
|
39290
|
-
|
|
39540
|
+
appendFileSync2(run.transcript, line.endsWith(`
|
|
39291
39541
|
`) ? line : `${line}
|
|
39292
39542
|
`);
|
|
39293
39543
|
} catch (error) {
|
|
@@ -39498,7 +39748,8 @@ class GodotServer {
|
|
|
39498
39748
|
serverSpeaks: RUNTIME_PROTOCOL,
|
|
39499
39749
|
behind: game.protocol > RUNTIME_PROTOCOL ? "server" : "addon"
|
|
39500
39750
|
}))
|
|
39501
|
-
}
|
|
39751
|
+
},
|
|
39752
|
+
server: { pid: process.pid, version: SERVER_VERSION, lifecycleLog: lifecycleLogPath() }
|
|
39502
39753
|
});
|
|
39503
39754
|
}
|
|
39504
39755
|
async editorServersListening() {
|
|
@@ -39530,7 +39781,7 @@ class GodotServer {
|
|
|
39530
39781
|
return null;
|
|
39531
39782
|
}
|
|
39532
39783
|
try {
|
|
39533
|
-
return settingKeys(readFileSync21(
|
|
39784
|
+
return settingKeys(readFileSync21(join23(projectPath, "project.godot"), "utf8"));
|
|
39534
39785
|
} catch {
|
|
39535
39786
|
return null;
|
|
39536
39787
|
}
|
|
@@ -39586,6 +39837,10 @@ class GodotServer {
|
|
|
39586
39837
|
async handleCloseEditor() {
|
|
39587
39838
|
const before = this.godotBridge.getStatus();
|
|
39588
39839
|
if (!before.connected) {
|
|
39840
|
+
const launched = this.launchedEditor;
|
|
39841
|
+
if (launched !== null && await this.launchedEditorIsUp() && !this.godotBridge.isConnected()) {
|
|
39842
|
+
return await this.stopTheLaunchedEditor(launched);
|
|
39843
|
+
}
|
|
39589
39844
|
return this.createErrorResponse("No editor is connected, so there is nothing to close.", [
|
|
39590
39845
|
"editor_status says whether the bridge is up and what has reached it"
|
|
39591
39846
|
]);
|
|
@@ -39619,6 +39874,31 @@ class GodotServer {
|
|
|
39619
39874
|
tookMs: Date.now() - began
|
|
39620
39875
|
});
|
|
39621
39876
|
}
|
|
39877
|
+
async stopTheLaunchedEditor(launched) {
|
|
39878
|
+
const began = Date.now();
|
|
39879
|
+
try {
|
|
39880
|
+
process.kill(launched.pid);
|
|
39881
|
+
} catch (error) {
|
|
39882
|
+
if (alive(launched.pid)) {
|
|
39883
|
+
return this.createErrorResponse(`The editor this server opened, pid ${launched.pid}, has not connected, and stopping it failed: ${errorMessage(error)}`, ["editor_status says whether it has since connected"]);
|
|
39884
|
+
}
|
|
39885
|
+
}
|
|
39886
|
+
const stopWaitMs = 1e4;
|
|
39887
|
+
if (!await this.waitForBridge(() => !alive(launched.pid), began + stopWaitMs)) {
|
|
39888
|
+
return this.createErrorResponse(`The editor this server opened, pid ${launched.pid}, had not connected and was told to stop, and is still running after ${stopWaitMs / 1000}s.`, ["editor_status says whether it has since connected"]);
|
|
39889
|
+
}
|
|
39890
|
+
this.closedHere = { editorPid: launched.pid, at: new Date().toISOString() };
|
|
39891
|
+
this.launchedEditor = null;
|
|
39892
|
+
this.launchedFrom = null;
|
|
39893
|
+
return this.jsonTextResponse({
|
|
39894
|
+
closed: true,
|
|
39895
|
+
editorPid: launched.pid,
|
|
39896
|
+
projectPath: launched.projectPath,
|
|
39897
|
+
beforeConnecting: true,
|
|
39898
|
+
note: "It had not reached the bridge, so it was stopped rather than asked to save and quit, and held nothing of this server's to save.",
|
|
39899
|
+
tookMs: Date.now() - began
|
|
39900
|
+
});
|
|
39901
|
+
}
|
|
39622
39902
|
settleTheLaunchReading() {
|
|
39623
39903
|
const watched = this.launchedFrom;
|
|
39624
39904
|
const status = this.godotBridge.getStatus();
|
|
@@ -39754,13 +40034,9 @@ class GodotServer {
|
|
|
39754
40034
|
`editor_launch with projectPath ${this.ownProject} opens the editor this server serves`
|
|
39755
40035
|
]);
|
|
39756
40036
|
}
|
|
39757
|
-
const
|
|
39758
|
-
|
|
39759
|
-
|
|
39760
|
-
return this.createErrorResponse(`The gdharness editor addon is not ${installed ? "enabled" : "installed"} in ${projectPath}, so an editor opened on it would never reach this server.`, installed ? [
|
|
39761
|
-
"gdharness setup in the project enables it",
|
|
39762
|
-
"Or enable it under Project Settings, Plugins, in an editor opened by hand"
|
|
39763
|
-
] : ["gdharness setup in the project installs and enables it"]);
|
|
40037
|
+
const missing = editorAddonMissing(projectPath);
|
|
40038
|
+
if (missing !== null) {
|
|
40039
|
+
return this.createErrorResponse(`${missing.why}, so an editor opened on it would never reach this server.`, missing.remedies);
|
|
39764
40040
|
}
|
|
39765
40041
|
const owed = restartOwed(projectPath);
|
|
39766
40042
|
if (owed !== null && restartUnderway(owed)) {
|
|
@@ -39788,7 +40064,8 @@ class GodotServer {
|
|
|
39788
40064
|
writeEditorLogNote(projectPath, launched.pid);
|
|
39789
40065
|
this.launchedEditor = {
|
|
39790
40066
|
pid: launched.pid,
|
|
39791
|
-
startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null
|
|
40067
|
+
startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null,
|
|
40068
|
+
projectPath
|
|
39792
40069
|
};
|
|
39793
40070
|
this.closedHere = null;
|
|
39794
40071
|
return { pid: launched.pid, error: null };
|
|
@@ -39925,7 +40202,7 @@ class GodotServer {
|
|
|
39925
40202
|
}
|
|
39926
40203
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
39927
40204
|
const budgetMs = budget.ms;
|
|
39928
|
-
if (!existsSync17(
|
|
40205
|
+
if (!existsSync17(join23(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
39929
40206
|
return runtimeVerdict(null, {
|
|
39930
40207
|
addon: false,
|
|
39931
40208
|
budgetMs,
|
|
@@ -40010,7 +40287,7 @@ class GodotServer {
|
|
|
40010
40287
|
}
|
|
40011
40288
|
let announcedAt;
|
|
40012
40289
|
try {
|
|
40013
|
-
announcedAt =
|
|
40290
|
+
announcedAt = statSync9(endpoint.file).mtimeMs;
|
|
40014
40291
|
} catch {
|
|
40015
40292
|
return;
|
|
40016
40293
|
}
|
|
@@ -40179,7 +40456,7 @@ class GodotServer {
|
|
|
40179
40456
|
return;
|
|
40180
40457
|
}
|
|
40181
40458
|
if (game.transcript !== null) {
|
|
40182
|
-
|
|
40459
|
+
appendFileSync2(game.transcript, reported);
|
|
40183
40460
|
} else {
|
|
40184
40461
|
game.log.append("stderr", reported);
|
|
40185
40462
|
}
|
|
@@ -40883,7 +41160,7 @@ class GodotServer {
|
|
|
40883
41160
|
}
|
|
40884
41161
|
const going = await this.runStillGoing(run);
|
|
40885
41162
|
const projectPath = run.projectPath;
|
|
40886
|
-
const projectGone = projectPath !== null && !existsSync17(
|
|
41163
|
+
const projectGone = projectPath !== null && !existsSync17(join23(projectPath, "project.godot"));
|
|
40887
41164
|
if (projectGone && callersName !== undefined) {
|
|
40888
41165
|
return this.createErrorResponse(`callersOf reads the project's scripts, and the run's project at ${projectPath} has no project.godot any more.`, ["editor_run profile without callersOf answers with the profile itself"]);
|
|
40889
41166
|
}
|
|
@@ -41054,7 +41331,11 @@ class GodotServer {
|
|
|
41054
41331
|
if (!greeted && launched === null && status.connected) {
|
|
41055
41332
|
return this.createErrorResponse("The editor has connected and has not yet said who it is, so whose console to read cannot be told yet.", ["Ask again in a moment; editor_status shows greeting until it has"]);
|
|
41056
41333
|
}
|
|
41057
|
-
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
41334
|
+
const projectPath = status.projectPath ?? this.launchedEditor?.projectPath ?? this.ownProject ?? undefined;
|
|
41335
|
+
const elsewhere = this.anotherProjectNamed(args, projectPath);
|
|
41336
|
+
if (elsewhere !== null) {
|
|
41337
|
+
return elsewhere;
|
|
41338
|
+
}
|
|
41058
41339
|
if (projectPath === undefined || !greeted && launched === null) {
|
|
41059
41340
|
return this.createErrorResponse("No editor is connected, so there is no console to read.", [
|
|
41060
41341
|
"editor_status says whether one is connected and whether it may yet be",
|
|
@@ -41105,6 +41386,19 @@ class GodotServer {
|
|
|
41105
41386
|
omitted: selected.omitted === 0 ? undefined : selected.omitted
|
|
41106
41387
|
});
|
|
41107
41388
|
}
|
|
41389
|
+
anotherProjectNamed(args, ofWhatIsRead) {
|
|
41390
|
+
const named = readNonEmptyString(args, "projectPath");
|
|
41391
|
+
if (named === undefined || ofWhatIsRead === null || ofWhatIsRead === undefined || ofWhatIsRead === "") {
|
|
41392
|
+
return null;
|
|
41393
|
+
}
|
|
41394
|
+
if (isSameDirectory(named, ofWhatIsRead)) {
|
|
41395
|
+
return null;
|
|
41396
|
+
}
|
|
41397
|
+
return this.createErrorResponse(`editor_output reads what this server holds, which is of ${ofWhatIsRead}, and this call names ${named}.`, [
|
|
41398
|
+
"Another project's run is read through that project's own gdharness server",
|
|
41399
|
+
"editor_status names every game running on the machine under runtimes"
|
|
41400
|
+
]);
|
|
41401
|
+
}
|
|
41108
41402
|
async handleGetDebugOutput(args, waitedMs) {
|
|
41109
41403
|
if (readString(args, "op") === "editor") {
|
|
41110
41404
|
return this.handleEditorConsole(args);
|
|
@@ -41114,6 +41408,10 @@ class GodotServer {
|
|
|
41114
41408
|
if (!run) {
|
|
41115
41409
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
41116
41410
|
}
|
|
41411
|
+
const elsewhere = this.anotherProjectNamed(args, run.projectPath ?? this.godotBridge.getStatus().projectPath);
|
|
41412
|
+
if (elsewhere !== null) {
|
|
41413
|
+
return elsewhere;
|
|
41414
|
+
}
|
|
41117
41415
|
await this.tieThePlayedGame(run);
|
|
41118
41416
|
this.drainEditorOutput(run);
|
|
41119
41417
|
this.drainTranscript(run);
|
|
@@ -41150,7 +41448,7 @@ class GodotServer {
|
|
|
41150
41448
|
notes.push(endedWithoutACode({
|
|
41151
41449
|
throughEditor: run.throughEditor,
|
|
41152
41450
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
41153
|
-
couldAnnounce: run.projectPath !== null && existsSync17(
|
|
41451
|
+
couldAnnounce: run.projectPath !== null && existsSync17(join23(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
41154
41452
|
endedHere: typeof run.endedHere === "string",
|
|
41155
41453
|
pickedUp: run.pickedUp === true
|
|
41156
41454
|
}));
|
|
@@ -41930,15 +42228,47 @@ class GodotServer {
|
|
|
41930
42228
|
const holds = held["classes"].map(String);
|
|
41931
42229
|
return { unseen: unseenByEditor(projectPath, holds), stillHeld: heldButGone(projectPath, holds) };
|
|
41932
42230
|
}
|
|
42231
|
+
async noEditorYet(args, instead) {
|
|
42232
|
+
const project = readNonEmptyString(args, "projectPath") ?? this.ownProject;
|
|
42233
|
+
const missing = project === null ? null : editorAddonMissing(project);
|
|
42234
|
+
if (missing !== null) {
|
|
42235
|
+
return this.createErrorResponse(`Editor not connected, and no editor of this project can connect: ${missing.why}.`, [...instead, ...missing.remedies, "editor_launch then opens an editor that connects"]);
|
|
42236
|
+
}
|
|
42237
|
+
const launched = this.launchedEditor;
|
|
42238
|
+
if (launched !== null && await this.launchedEditorIsUp()) {
|
|
42239
|
+
const began = Date.now();
|
|
42240
|
+
await this.waitForBridge(() => hasSaidWhoItIs(this.godotBridge.getStatus()) || !alive(launched.pid), began + editorRestartTimeoutMs());
|
|
42241
|
+
if (this.godotBridge.isConnected()) {
|
|
42242
|
+
return null;
|
|
42243
|
+
}
|
|
42244
|
+
const opened = `the editor this server opened on ${launched.projectPath}, pid ${launched.pid}`;
|
|
42245
|
+
if (alive(launched.pid)) {
|
|
42246
|
+
return this.createErrorResponse(`Editor not connected: ${opened}, is still running and has not reached the bridge in the ${String(Math.round((Date.now() - began) / 1000))}s this call waited. An editor dials in once it has imported the project and loaded its plugins, and the first open of a large project imports for minutes.`, [
|
|
42247
|
+
...instead,
|
|
42248
|
+
"editor_status names it under awaitingLaunchedEditor until it has connected",
|
|
42249
|
+
"editor_output op editor shows what it is printing meanwhile"
|
|
42250
|
+
]);
|
|
42251
|
+
}
|
|
42252
|
+
return this.createErrorResponse(`Editor not connected: ${opened}, exited without reaching the bridge.`, [
|
|
42253
|
+
...instead,
|
|
42254
|
+
"editor_output op editor shows what it printed before it went",
|
|
42255
|
+
"editor_launch open starts another"
|
|
42256
|
+
]);
|
|
42257
|
+
}
|
|
42258
|
+
return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
|
|
42259
|
+
...instead,
|
|
42260
|
+
"Open the project in the editor with editor_launch, or by hand",
|
|
42261
|
+
"Enable the addon under Project > Project Settings > Plugins",
|
|
42262
|
+
"editor_status says whether the bridge is up and what it is listening on"
|
|
42263
|
+
]);
|
|
42264
|
+
}
|
|
41933
42265
|
async handleViaBridge(toolName, args, askedAs) {
|
|
41934
42266
|
if (!this.godotBridge.isConnected()) {
|
|
41935
|
-
const
|
|
41936
|
-
|
|
41937
|
-
|
|
41938
|
-
|
|
41939
|
-
|
|
41940
|
-
"editor_status says whether the bridge is up and what it is listening on"
|
|
41941
|
-
]);
|
|
42267
|
+
const alternative = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
|
|
42268
|
+
const refused = await this.noEditorYet(args, alternative === undefined ? [] : [alternative]);
|
|
42269
|
+
if (refused !== null) {
|
|
42270
|
+
return refused;
|
|
42271
|
+
}
|
|
41942
42272
|
}
|
|
41943
42273
|
const open = this.godotBridge.getStatus().projectPath;
|
|
41944
42274
|
const meant = readNonEmptyString(args, "projectPath");
|
|
@@ -42011,7 +42341,7 @@ class GodotServer {
|
|
|
42011
42341
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
42012
42342
|
const run = await this.currentRun();
|
|
42013
42343
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
42014
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync17(
|
|
42344
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync17(join23(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
|
|
42015
42345
|
return null;
|
|
42016
42346
|
}
|
|
42017
42347
|
const already = runtimesAnnounced().running;
|
|
@@ -42020,7 +42350,7 @@ class GodotServer {
|
|
|
42020
42350
|
return !run.announcedBefore.has(one.pid);
|
|
42021
42351
|
}
|
|
42022
42352
|
try {
|
|
42023
|
-
return
|
|
42353
|
+
return statSync9(one.file).mtimeMs >= run.startedAt;
|
|
42024
42354
|
} catch {
|
|
42025
42355
|
return false;
|
|
42026
42356
|
}
|
|
@@ -42096,7 +42426,7 @@ class GodotServer {
|
|
|
42096
42426
|
}
|
|
42097
42427
|
}
|
|
42098
42428
|
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
42099
|
-
const screenshotPath = screenshotDir ?
|
|
42429
|
+
const screenshotPath = screenshotDir ? join23(screenshotDir, "capture.png") : null;
|
|
42100
42430
|
const staleNote = this.runtimeStaleNote(choice.endpoint);
|
|
42101
42431
|
const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
|
|
42102
42432
|
const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
|