gdharness 1.2.23 → 1.2.25
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 +527 -178
- package/build/godot/addons/gdharness_editor/serialisation.gd +29 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +24 -4
- 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/serialisation.gd +29 -0
- package/build/index.js +538 -193
- package/build/keeper.js +14 -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
|
|
|
@@ -24932,8 +24932,14 @@ var ENGINE_FLAGS = Object.freeze({
|
|
|
24932
24932
|
"--time-scale": "number",
|
|
24933
24933
|
"--fixed-fps": "number",
|
|
24934
24934
|
"--max-fps": "number",
|
|
24935
|
-
"--resolution": "size"
|
|
24935
|
+
"--resolution": "size",
|
|
24936
|
+
"--write-movie": "file"
|
|
24936
24937
|
});
|
|
24938
|
+
var VALUE_KIND = {
|
|
24939
|
+
number: { pattern: /^\d+(?:\.\d+)?$/, named: "a number" },
|
|
24940
|
+
size: { pattern: /^\d+x\d+$/, named: "a size such as 1280x720" },
|
|
24941
|
+
file: { pattern: /^[^-]/, named: "a file path" }
|
|
24942
|
+
};
|
|
24937
24943
|
var SET_BY_ARGUMENT = Object.freeze({
|
|
24938
24944
|
"--headless": "headless",
|
|
24939
24945
|
"--audio-driver": "silent",
|
|
@@ -24948,18 +24954,18 @@ function engineFlags(given) {
|
|
|
24948
24954
|
const setBy = Object.hasOwn(SET_BY_ARGUMENT, flag) ? SET_BY_ARGUMENT[flag] : undefined;
|
|
24949
24955
|
return {
|
|
24950
24956
|
ok: false,
|
|
24951
|
-
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 or how it times frames: ${offered}. A value goes as the next entry, as in ["--fixed-fps", "60"].`
|
|
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 or how it times frames, and Movie Maker's --write-movie: ${offered}. A value goes as the next entry, as in ["--fixed-fps", "60"].`
|
|
24952
24958
|
};
|
|
24953
24959
|
}
|
|
24954
24960
|
if (takes === "none") {
|
|
24955
24961
|
continue;
|
|
24956
24962
|
}
|
|
24957
24963
|
const value = given[at + 1];
|
|
24958
|
-
const
|
|
24959
|
-
if (!
|
|
24964
|
+
const kind = VALUE_KIND[takes];
|
|
24965
|
+
if (value === undefined || !kind.pattern.test(value)) {
|
|
24960
24966
|
return {
|
|
24961
24967
|
ok: false,
|
|
24962
|
-
reason: `${flag} takes ${
|
|
24968
|
+
reason: `${flag} takes ${kind.named} as the next entry, and ${value === undefined ? "nothing followed it" : `${JSON.stringify(value)} is not one`}.`
|
|
24963
24969
|
};
|
|
24964
24970
|
}
|
|
24965
24971
|
at += 1;
|
|
@@ -25099,6 +25105,7 @@ import { promisify } from "node:util";
|
|
|
25099
25105
|
|
|
25100
25106
|
// src/windows-ask.ts
|
|
25101
25107
|
import { spawn } from "node:child_process";
|
|
25108
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
25102
25109
|
import process6 from "node:process";
|
|
25103
25110
|
var SCRIPT = [
|
|
25104
25111
|
"[Console]::OutputEncoding = [Text.Encoding]::UTF8",
|
|
@@ -25153,7 +25160,7 @@ function running() {
|
|
|
25153
25160
|
"Bypass",
|
|
25154
25161
|
"-EncodedCommand",
|
|
25155
25162
|
Buffer.from(SCRIPT, "utf16le").toString("base64")
|
|
25156
|
-
], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
25163
|
+
], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, cwd: tmpdir2() });
|
|
25157
25164
|
buffered = "";
|
|
25158
25165
|
child.stdout.setEncoding("utf8");
|
|
25159
25166
|
child.stdout.on("data", (chunk) => {
|
|
@@ -25550,7 +25557,7 @@ function runtimeDirectory(variables = process.env) {
|
|
|
25550
25557
|
return explicit;
|
|
25551
25558
|
}
|
|
25552
25559
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
25553
|
-
return join2(perUser ??
|
|
25560
|
+
return join2(perUser ?? tmpdir3(), "gdharness");
|
|
25554
25561
|
}
|
|
25555
25562
|
function runtimeDirectories(variables = process.env) {
|
|
25556
25563
|
const candidates = [runtimeDirectory(variables)];
|
|
@@ -29492,7 +29499,7 @@ function importRecord(projectPath, resourcePath) {
|
|
|
29492
29499
|
|
|
29493
29500
|
// src/scratch.ts
|
|
29494
29501
|
import { existsSync as existsSync10, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync6 } from "node:fs";
|
|
29495
|
-
import { tmpdir as
|
|
29502
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
29496
29503
|
import { join as join13 } from "node:path";
|
|
29497
29504
|
import process7 from "node:process";
|
|
29498
29505
|
var SWEPT = { recursive: true, force: true };
|
|
@@ -29508,9 +29515,9 @@ var KINDS = [
|
|
|
29508
29515
|
"tests"
|
|
29509
29516
|
];
|
|
29510
29517
|
function scratchDirectory(kind) {
|
|
29511
|
-
return mkdtempSync(join13(
|
|
29518
|
+
return mkdtempSync(join13(tmpdir4(), `gdharness-${kind}-${process7.pid}-`));
|
|
29512
29519
|
}
|
|
29513
|
-
function sweepAbandonedScratch(directory =
|
|
29520
|
+
function sweepAbandonedScratch(directory = tmpdir4()) {
|
|
29514
29521
|
let entries;
|
|
29515
29522
|
try {
|
|
29516
29523
|
entries = readdirSync4(directory);
|
|
@@ -29603,17 +29610,25 @@ function snakeCased(params) {
|
|
|
29603
29610
|
return result;
|
|
29604
29611
|
}
|
|
29605
29612
|
function engineExtras(outcome) {
|
|
29613
|
+
const after = outcome.afterAnswerMessages ?? [];
|
|
29606
29614
|
return {
|
|
29607
29615
|
...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
|
|
29608
29616
|
...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
|
|
29617
|
+
...after.length > 0 ? { engine_messages_after_answer: after, engine_messages_after_answer_note: AFTER_ANSWER_NOTE } : {},
|
|
29618
|
+
...outcome.afterAnswerOmitted === undefined ? {} : { engine_messages_after_answer_omitted: outcome.afterAnswerOmitted },
|
|
29609
29619
|
...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
|
|
29610
29620
|
};
|
|
29611
29621
|
}
|
|
29622
|
+
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.";
|
|
29612
29623
|
var OWN_ERROR = "[ERROR] ";
|
|
29624
|
+
var ANSWERED = "[gdharness] answered";
|
|
29613
29625
|
var MESSAGES_KEPT = 200;
|
|
29614
29626
|
var ERRORS_NAMED = 3;
|
|
29615
29627
|
function problemsIn(log) {
|
|
29616
|
-
return log.everything()
|
|
29628
|
+
return problemsAmong(log.everything());
|
|
29629
|
+
}
|
|
29630
|
+
function problemsAmong(entries) {
|
|
29631
|
+
return entries.flatMap((entry) => {
|
|
29617
29632
|
if (entry.severity !== "info") {
|
|
29618
29633
|
return [entry];
|
|
29619
29634
|
}
|
|
@@ -29742,12 +29757,20 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
29742
29757
|
} finally {
|
|
29743
29758
|
discard(paramsDir);
|
|
29744
29759
|
}
|
|
29745
|
-
const
|
|
29760
|
+
const everything = ran.log.everything();
|
|
29761
|
+
const answered = everything.findIndex((entry) => entry.source === "stderr" && entry.text === ANSWERED);
|
|
29762
|
+
const problems = problemsAmong(answered === -1 ? everything : everything.slice(0, answered));
|
|
29763
|
+
const after = answered === -1 ? [] : problemsAmong(everything.slice(answered + 1));
|
|
29764
|
+
const afterAnswered = {
|
|
29765
|
+
...after.length === 0 ? {} : { afterAnswerMessages: after.slice(-MESSAGES_KEPT) },
|
|
29766
|
+
...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {}
|
|
29767
|
+
};
|
|
29746
29768
|
if (answer.kind === "unreadable") {
|
|
29747
29769
|
return {
|
|
29748
29770
|
ok: false,
|
|
29749
29771
|
message: `${operation} wrote an answer that could not be read (${answer.why})`,
|
|
29750
|
-
...reported2(problems)
|
|
29772
|
+
...reported2(problems),
|
|
29773
|
+
...afterAnswered
|
|
29751
29774
|
};
|
|
29752
29775
|
}
|
|
29753
29776
|
if (answer.kind === "none") {
|
|
@@ -29763,14 +29786,16 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
29763
29786
|
return {
|
|
29764
29787
|
ok: false,
|
|
29765
29788
|
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"})`,
|
|
29766
|
-
...reported2(problems)
|
|
29789
|
+
...reported2(problems),
|
|
29790
|
+
...afterAnswered
|
|
29767
29791
|
};
|
|
29768
29792
|
}
|
|
29769
|
-
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.`;
|
|
29793
|
+
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"}.`;
|
|
29770
29794
|
return {
|
|
29771
29795
|
ok: true,
|
|
29772
29796
|
payload: answer.payload,
|
|
29773
29797
|
...reported2(problems),
|
|
29798
|
+
...afterAnswered,
|
|
29774
29799
|
...afterAnswer === undefined ? {} : { afterAnswer }
|
|
29775
29800
|
};
|
|
29776
29801
|
}
|
|
@@ -29963,6 +29988,23 @@ function count(attributes, name) {
|
|
|
29963
29988
|
const value = Number(attributes[name] ?? "0");
|
|
29964
29989
|
return Number.isFinite(value) ? value : 0;
|
|
29965
29990
|
}
|
|
29991
|
+
function ignoredYetRan(report, ignored) {
|
|
29992
|
+
const found = [];
|
|
29993
|
+
for (const entry of ignored) {
|
|
29994
|
+
const bare = entry.replace("res://", "");
|
|
29995
|
+
const at = bare.lastIndexOf(":");
|
|
29996
|
+
const suiteNamed = at === -1 ? null : bare.slice(0, at);
|
|
29997
|
+
const testNamed = at === -1 ? bare : bare.slice(at + 1);
|
|
29998
|
+
for (const suite of report.suites) {
|
|
29999
|
+
const suiteMatches = suiteNamed === null || suite.name === suiteNamed || (suite.path ?? "").includes(suiteNamed);
|
|
30000
|
+
const ran = suiteMatches ? suite.cases.find((one) => one.name === testNamed) : undefined;
|
|
30001
|
+
if (ran !== undefined) {
|
|
30002
|
+
found.push({ entry, ran: `${suite.name}:${ran.name}` });
|
|
30003
|
+
}
|
|
30004
|
+
}
|
|
30005
|
+
}
|
|
30006
|
+
return found;
|
|
30007
|
+
}
|
|
29966
30008
|
function caseOf(suite, element) {
|
|
29967
30009
|
const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
|
|
29968
30010
|
const detail = child.text.trim();
|
|
@@ -30310,11 +30352,54 @@ function askTheKeeperToStop(address, withinMs) {
|
|
|
30310
30352
|
});
|
|
30311
30353
|
}
|
|
30312
30354
|
|
|
30355
|
+
// src/lifecycle-log.ts
|
|
30356
|
+
import { appendFileSync, mkdirSync as mkdirSync8, renameSync, statSync as statSync6 } from "node:fs";
|
|
30357
|
+
import { dirname as dirname9, join as join15 } from "node:path";
|
|
30358
|
+
import process8 from "node:process";
|
|
30359
|
+
function lifecycleLogPath() {
|
|
30360
|
+
return join15(runtimeDirectory(), "servers.log");
|
|
30361
|
+
}
|
|
30362
|
+
var LIFECYCLE_LOG_LIMIT = 1e6;
|
|
30363
|
+
function writeLifecycle(event, fields = {}) {
|
|
30364
|
+
const path = lifecycleLogPath();
|
|
30365
|
+
try {
|
|
30366
|
+
mkdirSync8(dirname9(path), { recursive: true });
|
|
30367
|
+
if ((statSync6(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
|
|
30368
|
+
try {
|
|
30369
|
+
renameSync(path, `${path}.1`);
|
|
30370
|
+
} catch {}
|
|
30371
|
+
}
|
|
30372
|
+
const line = {
|
|
30373
|
+
at: new Date().toISOString(),
|
|
30374
|
+
pid: process8.pid,
|
|
30375
|
+
ppid: process8.ppid,
|
|
30376
|
+
version: SERVER_VERSION,
|
|
30377
|
+
event
|
|
30378
|
+
};
|
|
30379
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
30380
|
+
if (!Object.hasOwn(line, name)) {
|
|
30381
|
+
line[name] = value;
|
|
30382
|
+
}
|
|
30383
|
+
}
|
|
30384
|
+
appendFileSync(path, `${JSON.stringify(line)}
|
|
30385
|
+
`, "utf8");
|
|
30386
|
+
} catch {
|
|
30387
|
+
console.error(`[SERVER] Could not write the lifecycle log at ${path}.`);
|
|
30388
|
+
}
|
|
30389
|
+
}
|
|
30390
|
+
function callsInFlight(calls, now = Date.now()) {
|
|
30391
|
+
return [...calls].map((call) => ({
|
|
30392
|
+
tool: call.tool,
|
|
30393
|
+
...call.op === "" ? {} : { op: call.op },
|
|
30394
|
+
runningMs: now - call.startedAt
|
|
30395
|
+
}));
|
|
30396
|
+
}
|
|
30397
|
+
|
|
30313
30398
|
// src/lsp_client.ts
|
|
30314
30399
|
import { existsSync as existsSync12, realpathSync as realpathSync3 } from "node:fs";
|
|
30315
30400
|
import { readFile, realpath } from "node:fs/promises";
|
|
30316
30401
|
import { createConnection as createConnection3 } from "node:net";
|
|
30317
|
-
import { dirname as
|
|
30402
|
+
import { dirname as dirname10, join as join16, resolve as resolve3 } from "node:path";
|
|
30318
30403
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
30319
30404
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
30320
30405
|
var DEFAULT_LSP_PORT = 6005;
|
|
@@ -30512,7 +30597,7 @@ class GodotLSPClient {
|
|
|
30512
30597
|
if (this.initialized) {
|
|
30513
30598
|
return;
|
|
30514
30599
|
}
|
|
30515
|
-
const rootPath = this.rootPath ??
|
|
30600
|
+
const rootPath = this.rootPath ?? dirname10(resolve3(filePath));
|
|
30516
30601
|
await this.initialize(rootPath);
|
|
30517
30602
|
}
|
|
30518
30603
|
async sendRequest(method, params, timeoutMs = this.timeouts.requestMs) {
|
|
@@ -31070,7 +31155,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
31070
31155
|
} catch {
|
|
31071
31156
|
throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
|
|
31072
31157
|
}
|
|
31073
|
-
if (!existsSync12(
|
|
31158
|
+
if (!existsSync12(join16(projectPath, "project.godot"))) {
|
|
31074
31159
|
throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
|
|
31075
31160
|
}
|
|
31076
31161
|
const contained = resolveWithinProject(projectPath, scriptPathValue);
|
|
@@ -31278,7 +31363,7 @@ function secondsFromClock(printed) {
|
|
|
31278
31363
|
|
|
31279
31364
|
// src/project-scan.ts
|
|
31280
31365
|
import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync14 } from "node:fs";
|
|
31281
|
-
import { extname, join as
|
|
31366
|
+
import { extname, join as join17 } from "node:path";
|
|
31282
31367
|
var SKIPPED = new Set(["node_modules"]);
|
|
31283
31368
|
function skipped(name) {
|
|
31284
31369
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
@@ -31288,10 +31373,10 @@ function steppedOver(directory) {
|
|
|
31288
31373
|
return gdignored(directory) || installedByUs(directory);
|
|
31289
31374
|
}
|
|
31290
31375
|
function gdignored(directory) {
|
|
31291
|
-
return existsSync13(
|
|
31376
|
+
return existsSync13(join17(directory, ".gdignore"));
|
|
31292
31377
|
}
|
|
31293
31378
|
function installedByUs(directory) {
|
|
31294
|
-
return existsSync13(
|
|
31379
|
+
return existsSync13(join17(directory, VERSION_MARKER));
|
|
31295
31380
|
}
|
|
31296
31381
|
var SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
|
|
31297
31382
|
var ASSET_EXTENSIONS = new Set([
|
|
@@ -31335,7 +31420,7 @@ function projectStructure(projectPath) {
|
|
|
31335
31420
|
continue;
|
|
31336
31421
|
}
|
|
31337
31422
|
if (entry.isDirectory()) {
|
|
31338
|
-
visit(
|
|
31423
|
+
visit(join17(directory, entry.name));
|
|
31339
31424
|
} else if (entry.isFile()) {
|
|
31340
31425
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
31341
31426
|
if (SCENE_EXTENSIONS.has(extension)) {
|
|
@@ -31365,7 +31450,7 @@ function projectFiles(projectPath, extensions) {
|
|
|
31365
31450
|
}
|
|
31366
31451
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
31367
31452
|
if (entry.isDirectory()) {
|
|
31368
|
-
visit(
|
|
31453
|
+
visit(join17(directory, entry.name), spelled);
|
|
31369
31454
|
} else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
|
|
31370
31455
|
found.push(spelled);
|
|
31371
31456
|
}
|
|
@@ -31387,14 +31472,14 @@ function scriptsWithoutUid(projectPath) {
|
|
|
31387
31472
|
}
|
|
31388
31473
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
31389
31474
|
if (entry.isDirectory()) {
|
|
31390
|
-
visit(
|
|
31475
|
+
visit(join17(directory, entry.name), spelled);
|
|
31391
31476
|
continue;
|
|
31392
31477
|
}
|
|
31393
31478
|
if (!entry.isFile()) {
|
|
31394
31479
|
continue;
|
|
31395
31480
|
}
|
|
31396
31481
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
31397
|
-
if (CARRIES_A_UID.has(extension) && !existsSync13(
|
|
31482
|
+
if (CARRIES_A_UID.has(extension) && !existsSync13(join17(directory, `${entry.name}.uid`))) {
|
|
31398
31483
|
found.push(spelled);
|
|
31399
31484
|
}
|
|
31400
31485
|
}
|
|
@@ -31426,7 +31511,7 @@ function projectTexts(projectPath) {
|
|
|
31426
31511
|
if (skipped(entry.name)) {
|
|
31427
31512
|
continue;
|
|
31428
31513
|
}
|
|
31429
|
-
const absolute =
|
|
31514
|
+
const absolute = join17(directory, entry.name);
|
|
31430
31515
|
const spelled = `${prefix}${entry.name}`;
|
|
31431
31516
|
if (entry.isDirectory()) {
|
|
31432
31517
|
visit(absolute, `${spelled}/`);
|
|
@@ -31456,7 +31541,7 @@ function ignoredTexts(projectPath) {
|
|
|
31456
31541
|
if (skipped(entry.name)) {
|
|
31457
31542
|
continue;
|
|
31458
31543
|
}
|
|
31459
|
-
const absolute =
|
|
31544
|
+
const absolute = join17(directory, entry.name);
|
|
31460
31545
|
const spelled = `${prefix}${entry.name}`;
|
|
31461
31546
|
if (entry.isDirectory()) {
|
|
31462
31547
|
visit(absolute, `${spelled}/`, under);
|
|
@@ -31496,7 +31581,7 @@ function searchProject(projectPath, options) {
|
|
|
31496
31581
|
if (skipped(entry.name)) {
|
|
31497
31582
|
continue;
|
|
31498
31583
|
}
|
|
31499
|
-
const entryPath =
|
|
31584
|
+
const entryPath = join17(directory, entry.name);
|
|
31500
31585
|
if (entry.isDirectory()) {
|
|
31501
31586
|
visit(entryPath);
|
|
31502
31587
|
continue;
|
|
@@ -31667,8 +31752,8 @@ function setupPromptHandlers(server) {
|
|
|
31667
31752
|
}
|
|
31668
31753
|
|
|
31669
31754
|
// src/rename.ts
|
|
31670
|
-
import { existsSync as existsSync14, mkdirSync as
|
|
31671
|
-
import { dirname as
|
|
31755
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync15, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
|
|
31756
|
+
import { dirname as dirname11, posix as posix2 } from "node:path";
|
|
31672
31757
|
class Script {
|
|
31673
31758
|
path;
|
|
31674
31759
|
text;
|
|
@@ -32401,15 +32486,15 @@ function applyRewrites(rewrites) {
|
|
|
32401
32486
|
const done = [];
|
|
32402
32487
|
try {
|
|
32403
32488
|
for (const rewrite of rewrites) {
|
|
32404
|
-
|
|
32489
|
+
mkdirSync9(dirname11(rewrite.writeTo), { recursive: true });
|
|
32405
32490
|
const staging = `${rewrite.writeTo}.gdharness-rename`;
|
|
32406
32491
|
writeFileSync10(staging, rewrite.after, "utf8");
|
|
32407
|
-
|
|
32492
|
+
renameSync2(staging, rewrite.writeTo);
|
|
32408
32493
|
let movedUid = false;
|
|
32409
32494
|
if (rewrite.writeTo !== rewrite.absolute) {
|
|
32410
32495
|
unlinkSync2(rewrite.absolute);
|
|
32411
32496
|
if (existsSync14(`${rewrite.absolute}.uid`)) {
|
|
32412
|
-
|
|
32497
|
+
renameSync2(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
|
|
32413
32498
|
movedUid = true;
|
|
32414
32499
|
}
|
|
32415
32500
|
}
|
|
@@ -32422,7 +32507,7 @@ function applyRewrites(rewrites) {
|
|
|
32422
32507
|
if (rewrite.writeTo !== rewrite.absolute) {
|
|
32423
32508
|
unlinkSync2(rewrite.writeTo);
|
|
32424
32509
|
if (movedUid) {
|
|
32425
|
-
|
|
32510
|
+
renameSync2(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
|
|
32426
32511
|
}
|
|
32427
32512
|
}
|
|
32428
32513
|
} catch {}
|
|
@@ -32432,8 +32517,8 @@ function applyRewrites(rewrites) {
|
|
|
32432
32517
|
}
|
|
32433
32518
|
|
|
32434
32519
|
// src/rename-symbol.ts
|
|
32435
|
-
import { existsSync as existsSync15, mkdirSync as
|
|
32436
|
-
import { basename as basename3, dirname as
|
|
32520
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, readdirSync as readdirSync6, realpathSync as realpathSync5, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
|
|
32521
|
+
import { basename as basename3, dirname as dirname12, join as join18, relative as relative3, resolve as resolve5 } from "node:path";
|
|
32437
32522
|
|
|
32438
32523
|
// src/resources.ts
|
|
32439
32524
|
import { readFileSync as readFileSync16, realpathSync as realpathSync4 } from "node:fs";
|
|
@@ -32741,9 +32826,9 @@ function onDiskSpelling(path) {
|
|
|
32741
32826
|
let at = resolve5(path);
|
|
32742
32827
|
for (;; ) {
|
|
32743
32828
|
try {
|
|
32744
|
-
return
|
|
32829
|
+
return join18(realpathSync5.native(at), ...missing.reverse());
|
|
32745
32830
|
} catch {
|
|
32746
|
-
const parent =
|
|
32831
|
+
const parent = dirname12(at);
|
|
32747
32832
|
if (parent === at) {
|
|
32748
32833
|
return resolve5(path);
|
|
32749
32834
|
}
|
|
@@ -33171,15 +33256,15 @@ ${mention.kind}`;
|
|
|
33171
33256
|
var LEFT_ALONE_ANSWER_LINES = 300;
|
|
33172
33257
|
var detailWrites = 0;
|
|
33173
33258
|
function writeLeftAloneDetail(projectPath, symbol, detail) {
|
|
33174
|
-
const directory =
|
|
33175
|
-
|
|
33259
|
+
const directory = join18(projectPath, ".godot", "gdharness-renames");
|
|
33260
|
+
mkdirSync10(directory, { recursive: true });
|
|
33176
33261
|
detailWrites += 1;
|
|
33177
|
-
const path =
|
|
33262
|
+
const path = join18(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
|
|
33178
33263
|
writeFileSync11(path, `${JSON.stringify(detail, null, 2)}
|
|
33179
33264
|
`, "utf8");
|
|
33180
33265
|
const kept = readdirSync6(directory).filter((name) => name.endsWith(".json")).sort().reverse();
|
|
33181
33266
|
for (const old of kept.slice(10)) {
|
|
33182
|
-
rmSync7(
|
|
33267
|
+
rmSync7(join18(directory, old), { force: true });
|
|
33183
33268
|
}
|
|
33184
33269
|
return path;
|
|
33185
33270
|
}
|
|
@@ -33202,15 +33287,15 @@ function summaryOf(edits, mentions, rewrites, left) {
|
|
|
33202
33287
|
}
|
|
33203
33288
|
|
|
33204
33289
|
// src/restart-note.ts
|
|
33205
|
-
import { mkdirSync as
|
|
33206
|
-
import { dirname as
|
|
33290
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync17, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "node:fs";
|
|
33291
|
+
import { dirname as dirname13, join as join19 } from "node:path";
|
|
33207
33292
|
function restartNotePath(projectPath) {
|
|
33208
|
-
return
|
|
33293
|
+
return join19(projectPath, ".godot", "gdharness-restart.json");
|
|
33209
33294
|
}
|
|
33210
33295
|
function noteRestartBegun(note) {
|
|
33211
33296
|
const path = restartNotePath(note.projectPath);
|
|
33212
33297
|
try {
|
|
33213
|
-
|
|
33298
|
+
mkdirSync11(dirname13(path), { recursive: true });
|
|
33214
33299
|
writeFileSync12(path, `${JSON.stringify(note, null, 2)}
|
|
33215
33300
|
`, "utf8");
|
|
33216
33301
|
} catch (error) {
|
|
@@ -33243,12 +33328,12 @@ function restartSettled(projectPath) {
|
|
|
33243
33328
|
// src/run-record.ts
|
|
33244
33329
|
import { execFile as execFile4 } from "node:child_process";
|
|
33245
33330
|
import { createHash as createHash3 } from "node:crypto";
|
|
33246
|
-
import { mkdirSync as
|
|
33247
|
-
import { basename as basename4, dirname as
|
|
33331
|
+
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";
|
|
33332
|
+
import { basename as basename4, dirname as dirname14, join as join20, resolve as resolve6 } from "node:path";
|
|
33248
33333
|
import { promisify as promisify4 } from "node:util";
|
|
33249
33334
|
|
|
33250
33335
|
// src/whole-file.ts
|
|
33251
|
-
import { renameSync as
|
|
33336
|
+
import { renameSync as renameSync3, rmSync as rmSync9, writeFileSync as writeFileSync13 } from "node:fs";
|
|
33252
33337
|
var HELD_FILE_MS = 1000;
|
|
33253
33338
|
var HELD_FILE_RETRY_MS = 10;
|
|
33254
33339
|
var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
@@ -33262,7 +33347,7 @@ function writeWhole(path, text) {
|
|
|
33262
33347
|
const until = Date.now() + HELD_FILE_MS;
|
|
33263
33348
|
for (;; ) {
|
|
33264
33349
|
try {
|
|
33265
|
-
|
|
33350
|
+
renameSync3(partial, path);
|
|
33266
33351
|
return true;
|
|
33267
33352
|
} catch (error) {
|
|
33268
33353
|
const code = error.code ?? "";
|
|
@@ -33280,16 +33365,16 @@ function writeWhole(path, text) {
|
|
|
33280
33365
|
// src/run-record.ts
|
|
33281
33366
|
var execFileAsync = promisify4(execFile4);
|
|
33282
33367
|
function runsDirectory() {
|
|
33283
|
-
return
|
|
33368
|
+
return join20(runtimeDirectory(), "runs");
|
|
33284
33369
|
}
|
|
33285
33370
|
function realAsFarAsItExists(path) {
|
|
33286
33371
|
const missing = [];
|
|
33287
33372
|
let at = path;
|
|
33288
33373
|
for (;; ) {
|
|
33289
33374
|
try {
|
|
33290
|
-
return
|
|
33375
|
+
return join20(realpathSync6.native(at), ...missing);
|
|
33291
33376
|
} catch {
|
|
33292
|
-
const parent =
|
|
33377
|
+
const parent = dirname14(at);
|
|
33293
33378
|
if (parent === at) {
|
|
33294
33379
|
return path;
|
|
33295
33380
|
}
|
|
@@ -33304,14 +33389,14 @@ function projectKey(projectPath) {
|
|
|
33304
33389
|
return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
33305
33390
|
}
|
|
33306
33391
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
33307
|
-
return
|
|
33392
|
+
return join20(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
33308
33393
|
}
|
|
33309
33394
|
function latestRunPath(servedBy, directory = runtimeDirectory()) {
|
|
33310
|
-
return
|
|
33395
|
+
return join20(directory, "runs", `latest-${projectKey(servedBy)}.json`);
|
|
33311
33396
|
}
|
|
33312
33397
|
function writeLatestRun(servedBy, latest) {
|
|
33313
33398
|
try {
|
|
33314
|
-
|
|
33399
|
+
mkdirSync12(runsDirectory(), { recursive: true });
|
|
33315
33400
|
writeWhole(latestRunPath(servedBy), JSON.stringify(latest, null, 2));
|
|
33316
33401
|
} catch {}
|
|
33317
33402
|
}
|
|
@@ -33332,21 +33417,21 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
|
|
|
33332
33417
|
}
|
|
33333
33418
|
var SHARED_NOTE = "run.json";
|
|
33334
33419
|
function openTranscript(startedAt) {
|
|
33335
|
-
|
|
33336
|
-
const path =
|
|
33420
|
+
mkdirSync12(runsDirectory(), { recursive: true });
|
|
33421
|
+
const path = join20(runsDirectory(), `run-${startedAt}.log`);
|
|
33337
33422
|
return { path, fd: openSync2(path, "a") };
|
|
33338
33423
|
}
|
|
33339
33424
|
function profilePath(startedAt) {
|
|
33340
|
-
return
|
|
33425
|
+
return join20(runsDirectory(), `run-${startedAt}.profile.json`);
|
|
33341
33426
|
}
|
|
33342
33427
|
function noteIsAbout(note, playing) {
|
|
33343
33428
|
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
33344
33429
|
}
|
|
33345
33430
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
33346
|
-
return
|
|
33431
|
+
return join20(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
33347
33432
|
}
|
|
33348
33433
|
function writeEditorRunNote(note) {
|
|
33349
|
-
|
|
33434
|
+
mkdirSync12(runsDirectory(), { recursive: true });
|
|
33350
33435
|
const path = editorNotePath(note.projectPath);
|
|
33351
33436
|
if (!writeWhole(path, JSON.stringify(note, null, 2))) {
|
|
33352
33437
|
throw new Error(`the editor run note could not be written at ${path}`);
|
|
@@ -33354,7 +33439,7 @@ function writeEditorRunNote(note) {
|
|
|
33354
33439
|
}
|
|
33355
33440
|
function readEditorRunNote(projectPath) {
|
|
33356
33441
|
for (const directory of runtimeDirectories()) {
|
|
33357
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
33442
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join20(directory, "runs", "editor-run.json"));
|
|
33358
33443
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
33359
33444
|
return note;
|
|
33360
33445
|
}
|
|
@@ -33388,7 +33473,7 @@ function readRunRecord(projectPath) {
|
|
|
33388
33473
|
if (own !== null) {
|
|
33389
33474
|
return own;
|
|
33390
33475
|
}
|
|
33391
|
-
const shared = recordAt(
|
|
33476
|
+
const shared = recordAt(join20(directory, "runs", SHARED_NOTE));
|
|
33392
33477
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
33393
33478
|
return shared;
|
|
33394
33479
|
}
|
|
@@ -33400,13 +33485,13 @@ function everyRunRecord() {
|
|
|
33400
33485
|
for (const directory of runtimeDirectories()) {
|
|
33401
33486
|
let entries;
|
|
33402
33487
|
try {
|
|
33403
|
-
entries = readdirSync7(
|
|
33488
|
+
entries = readdirSync7(join20(directory, "runs"));
|
|
33404
33489
|
} catch {
|
|
33405
33490
|
continue;
|
|
33406
33491
|
}
|
|
33407
33492
|
for (const entry of entries) {
|
|
33408
33493
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
33409
|
-
const record = recordAt(
|
|
33494
|
+
const record = recordAt(join20(directory, "runs", entry));
|
|
33410
33495
|
if (record !== null) {
|
|
33411
33496
|
found.push(record);
|
|
33412
33497
|
}
|
|
@@ -33617,7 +33702,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
33617
33702
|
}
|
|
33618
33703
|
function clearRunRecord(projectPath) {
|
|
33619
33704
|
for (const directory of runtimeDirectories()) {
|
|
33620
|
-
const shared =
|
|
33705
|
+
const shared = join20(directory, "runs", SHARED_NOTE);
|
|
33621
33706
|
const sharedRecord = recordAt(shared);
|
|
33622
33707
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
33623
33708
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
@@ -33629,7 +33714,7 @@ function clearRunRecord(projectPath) {
|
|
|
33629
33714
|
}
|
|
33630
33715
|
function ageMs(path, now) {
|
|
33631
33716
|
try {
|
|
33632
|
-
return now -
|
|
33717
|
+
return now - statSync7(path).mtimeMs;
|
|
33633
33718
|
} catch {
|
|
33634
33719
|
return 0;
|
|
33635
33720
|
}
|
|
@@ -33651,7 +33736,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
33651
33736
|
if (!transcript && !entry.endsWith(".partial")) {
|
|
33652
33737
|
continue;
|
|
33653
33738
|
}
|
|
33654
|
-
const path =
|
|
33739
|
+
const path = join20(directory, entry);
|
|
33655
33740
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
33656
33741
|
continue;
|
|
33657
33742
|
}
|
|
@@ -33662,7 +33747,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
33662
33747
|
}
|
|
33663
33748
|
|
|
33664
33749
|
// src/script-origins.ts
|
|
33665
|
-
import { join as
|
|
33750
|
+
import { join as join21 } from "node:path";
|
|
33666
33751
|
var UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
|
|
33667
33752
|
function unresolvedClassIn(message) {
|
|
33668
33753
|
return UNRESOLVED.exec(message)?.[1] ?? null;
|
|
@@ -33718,7 +33803,7 @@ function checkWithEngine(godotPath, projectPath) {
|
|
|
33718
33803
|
const ran = await runEngine(godotPath, [
|
|
33719
33804
|
"--headless",
|
|
33720
33805
|
"--log-file",
|
|
33721
|
-
|
|
33806
|
+
join21(logDir, "engine.log"),
|
|
33722
33807
|
"--path",
|
|
33723
33808
|
projectPath,
|
|
33724
33809
|
"--check-only",
|
|
@@ -34404,7 +34489,12 @@ var TOOL_SPECS = [
|
|
|
34404
34489
|
ignore: {
|
|
34405
34490
|
type: "array",
|
|
34406
34491
|
items: { type: "string" },
|
|
34407
|
-
description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name".'
|
|
34492
|
+
description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name". A bare test name is read as a suite name and leaves out nothing; an entry that named a case which ran all the same comes back under ignoreNotApplied with the form that would have left it out.'
|
|
34493
|
+
},
|
|
34494
|
+
engineArgs: {
|
|
34495
|
+
type: "array",
|
|
34496
|
+
items: { type: "string" },
|
|
34497
|
+
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 or how it times frames: ${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.`
|
|
34408
34498
|
},
|
|
34409
34499
|
failFast: {
|
|
34410
34500
|
type: "boolean",
|
|
@@ -34831,7 +34921,7 @@ var TOOL_SPECS = [
|
|
|
34831
34921
|
{
|
|
34832
34922
|
name: "editor_launch",
|
|
34833
34923
|
title: "Open, restart or close the editor",
|
|
34834
|
-
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.",
|
|
34924
|
+
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.",
|
|
34835
34925
|
parameters: {
|
|
34836
34926
|
projectPath: { ...PROJECT_PATH2, ops: ["open"] },
|
|
34837
34927
|
hidden: {
|
|
@@ -34881,7 +34971,7 @@ var TOOL_SPECS = [
|
|
|
34881
34971
|
type: "array",
|
|
34882
34972
|
items: { type: "string" },
|
|
34883
34973
|
ops: ["start", "check"],
|
|
34884
|
-
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 or how it times frames: ${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. 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.`
|
|
34974
|
+
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 or how it times frames, 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. --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.`
|
|
34885
34975
|
},
|
|
34886
34976
|
headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
|
|
34887
34977
|
visible: {
|
|
@@ -34986,6 +35076,10 @@ var TOOL_SPECS = [
|
|
|
34986
35076
|
title: "Game and editor output",
|
|
34987
35077
|
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.",
|
|
34988
35078
|
parameters: {
|
|
35079
|
+
projectPath: {
|
|
35080
|
+
type: "string",
|
|
35081
|
+
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."
|
|
35082
|
+
},
|
|
34989
35083
|
severity: {
|
|
34990
35084
|
type: "string",
|
|
34991
35085
|
enum: ["error", "warning", "info"],
|
|
@@ -35037,7 +35131,7 @@ var TOOL_SPECS = [
|
|
|
35037
35131
|
title: "Editor status",
|
|
35038
35132
|
effect: "read",
|
|
35039
35133
|
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.",
|
|
35040
|
-
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.",
|
|
35134
|
+
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.",
|
|
35041
35135
|
parameters: {},
|
|
35042
35136
|
requires: []
|
|
35043
35137
|
},
|
|
@@ -35240,7 +35334,7 @@ var TOOL_SPECS = [
|
|
|
35240
35334
|
value: {
|
|
35241
35335
|
ops: ["set"],
|
|
35242
35336
|
blank: true,
|
|
35243
|
-
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.`
|
|
35337
|
+
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.`
|
|
35244
35338
|
},
|
|
35245
35339
|
method: {
|
|
35246
35340
|
type: "string",
|
|
@@ -35250,7 +35344,7 @@ var TOOL_SPECS = [
|
|
|
35250
35344
|
args: {
|
|
35251
35345
|
type: "array",
|
|
35252
35346
|
ops: ["call"],
|
|
35253
|
-
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.`
|
|
35347
|
+
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.`
|
|
35254
35348
|
},
|
|
35255
35349
|
properties: {
|
|
35256
35350
|
type: ["boolean", "array"],
|
|
@@ -35344,7 +35438,7 @@ var TOOL_SPECS = [
|
|
|
35344
35438
|
name: "runtime_input",
|
|
35345
35439
|
title: "Send input to the game",
|
|
35346
35440
|
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.",
|
|
35347
|
-
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.',
|
|
35441
|
+
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 and mouse_click: the event as it was sent, with whole saying whether the press was let go of again, and for a key the keycode, physical_keycode and unicode it carried. mouse_motion: the position and the relative movement sent. 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.',
|
|
35348
35442
|
parameters: {
|
|
35349
35443
|
projectPath: RUNNING_PROJECT_PATH,
|
|
35350
35444
|
pid: RUNNING_PID,
|
|
@@ -35668,15 +35762,15 @@ import { createHash as createHash4 } from "node:crypto";
|
|
|
35668
35762
|
import {
|
|
35669
35763
|
cpSync,
|
|
35670
35764
|
existsSync as existsSync16,
|
|
35671
|
-
mkdirSync as
|
|
35765
|
+
mkdirSync as mkdirSync13,
|
|
35672
35766
|
readdirSync as readdirSync8,
|
|
35673
35767
|
readFileSync as readFileSync20,
|
|
35674
35768
|
rmdirSync,
|
|
35675
35769
|
rmSync as rmSync11,
|
|
35676
|
-
statSync as
|
|
35770
|
+
statSync as statSync8,
|
|
35677
35771
|
writeFileSync as writeFileSync14
|
|
35678
35772
|
} from "node:fs";
|
|
35679
|
-
import { dirname as
|
|
35773
|
+
import { dirname as dirname15, join as join22 } from "node:path";
|
|
35680
35774
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
35681
35775
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
35682
35776
|
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
@@ -35699,9 +35793,9 @@ function addonsDigest(names, from) {
|
|
|
35699
35793
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
35700
35794
|
continue;
|
|
35701
35795
|
}
|
|
35702
|
-
const path =
|
|
35796
|
+
const path = join22(directory, name);
|
|
35703
35797
|
const inside = `${relative}/${name}`;
|
|
35704
|
-
if (
|
|
35798
|
+
if (statSync8(path).isDirectory()) {
|
|
35705
35799
|
walk(path, inside);
|
|
35706
35800
|
} else {
|
|
35707
35801
|
hash.update(`${inside}
|
|
@@ -35713,7 +35807,7 @@ function addonsDigest(names, from) {
|
|
|
35713
35807
|
}
|
|
35714
35808
|
};
|
|
35715
35809
|
for (const name of names) {
|
|
35716
|
-
walk(
|
|
35810
|
+
walk(join22(from, name), name);
|
|
35717
35811
|
}
|
|
35718
35812
|
return hash.digest("hex");
|
|
35719
35813
|
}
|
|
@@ -35746,7 +35840,7 @@ function installedRuntimeDigest(projectPath) {
|
|
|
35746
35840
|
return digestMarker(projectPath, RUNTIME_ADDON);
|
|
35747
35841
|
}
|
|
35748
35842
|
function digestMarker(projectPath, addon) {
|
|
35749
|
-
const marker =
|
|
35843
|
+
const marker = join22(projectPath, "addons", addon, DIGEST_MARKER);
|
|
35750
35844
|
if (!existsSync16(marker)) {
|
|
35751
35845
|
return null;
|
|
35752
35846
|
}
|
|
@@ -35754,10 +35848,10 @@ function digestMarker(projectPath, addon) {
|
|
|
35754
35848
|
return said === "" ? null : said;
|
|
35755
35849
|
}
|
|
35756
35850
|
function shippedAddonsDirectory() {
|
|
35757
|
-
return
|
|
35851
|
+
return join22(dirname15(fileURLToPath3(import.meta.url)), "godot", "addons");
|
|
35758
35852
|
}
|
|
35759
35853
|
function installedAddonVersion(projectPath) {
|
|
35760
|
-
const marker =
|
|
35854
|
+
const marker = join22(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
35761
35855
|
if (!existsSync16(marker)) {
|
|
35762
35856
|
return null;
|
|
35763
35857
|
}
|
|
@@ -35772,7 +35866,7 @@ function loaderAmong(projectPath, named) {
|
|
|
35772
35866
|
}
|
|
35773
35867
|
let source = null;
|
|
35774
35868
|
try {
|
|
35775
|
-
source = readFileSync20(
|
|
35869
|
+
source = readFileSync20(join22(projectPath, path), "utf8");
|
|
35776
35870
|
} catch {}
|
|
35777
35871
|
if (source?.includes(RUNTIME_AUTOLOAD.path)) {
|
|
35778
35872
|
return { name: entry, path, exists: true };
|
|
@@ -35814,8 +35908,8 @@ function enabledPlugins(settings) {
|
|
|
35814
35908
|
function inspectProject(projectPath) {
|
|
35815
35909
|
const problems = [];
|
|
35816
35910
|
const addons = ADDONS.map((name) => {
|
|
35817
|
-
const directory =
|
|
35818
|
-
const marker =
|
|
35911
|
+
const directory = join22(projectPath, "addons", name);
|
|
35912
|
+
const marker = join22(directory, VERSION_MARKER);
|
|
35819
35913
|
const installed = existsSync16(directory);
|
|
35820
35914
|
const version = existsSync16(marker) ? readFileSync20(marker, "utf8").trim() : null;
|
|
35821
35915
|
const current = version === SERVER_VERSION;
|
|
@@ -35826,7 +35920,7 @@ function inspectProject(projectPath) {
|
|
|
35826
35920
|
}
|
|
35827
35921
|
return { name, installed, version, current };
|
|
35828
35922
|
});
|
|
35829
|
-
const settings = parseProjectGodot(readFileSync20(
|
|
35923
|
+
const settings = parseProjectGodot(readFileSync20(join22(projectPath, "project.godot"), "utf8"));
|
|
35830
35924
|
const pluginsEnabled = enabledPlugins(settings);
|
|
35831
35925
|
for (const name of EDITOR_PLUGINS) {
|
|
35832
35926
|
if (!pluginsEnabled.includes(name)) {
|
|
@@ -35842,7 +35936,7 @@ function inspectProject(projectPath) {
|
|
|
35842
35936
|
const tracked = trackedByGit(projectPath, [...named.values()]);
|
|
35843
35937
|
const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
|
|
35844
35938
|
for (const [name, path] of named) {
|
|
35845
|
-
if (!existsSync16(
|
|
35939
|
+
if (!existsSync16(join22(projectPath, path))) {
|
|
35846
35940
|
if (!addonMissing(path)) {
|
|
35847
35941
|
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`);
|
|
35848
35942
|
}
|
|
@@ -35893,7 +35987,8 @@ function argumentShape(schema) {
|
|
|
35893
35987
|
var UPDATE_NOTICE_EVERY = 500;
|
|
35894
35988
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
35895
35989
|
var run4 = promisify5(execFile5);
|
|
35896
|
-
var __dirname2 =
|
|
35990
|
+
var __dirname2 = dirname16(fileURLToPath4(import.meta.url));
|
|
35991
|
+
var PARENT_ASKED_AFTER_MS = 5000;
|
|
35897
35992
|
function editorRestartTimeoutMs() {
|
|
35898
35993
|
return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
|
|
35899
35994
|
}
|
|
@@ -35911,29 +36006,62 @@ function restartUnderway(note, now = Date.now()) {
|
|
|
35911
36006
|
var HOLD_PING_MS = 2000;
|
|
35912
36007
|
var SLOWEST_FRAME_RATE = 20;
|
|
35913
36008
|
function captureDestinationRefusal(outputPath, projects) {
|
|
35914
|
-
|
|
35915
|
-
|
|
36009
|
+
return writtenFileRefusal(outputPath, {
|
|
36010
|
+
named: "outputPath",
|
|
36011
|
+
what: "a capture",
|
|
36012
|
+
extensions: [".png"],
|
|
36013
|
+
projects,
|
|
36014
|
+
writes: (where) => [where]
|
|
36015
|
+
});
|
|
36016
|
+
}
|
|
36017
|
+
function writtenFileRefusal(path, rule) {
|
|
36018
|
+
const { named, what } = rule;
|
|
36019
|
+
if (/^(res|user):\/\//.test(path)) {
|
|
36020
|
+
return `${named} ${path} is a Godot path; ${what} is saved outside the project, at an absolute path on this machine.`;
|
|
35916
36021
|
}
|
|
35917
|
-
if (!isAbsolute3(
|
|
35918
|
-
return
|
|
36022
|
+
if (!isAbsolute3(path)) {
|
|
36023
|
+
return `${named} ${path} is not absolute; name the whole path, so where it is written does not depend on this server's directory.`;
|
|
35919
36024
|
}
|
|
35920
|
-
if (extname3(
|
|
35921
|
-
return
|
|
36025
|
+
if (!rule.extensions.includes(extname3(path).toLowerCase())) {
|
|
36026
|
+
return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
|
|
35922
36027
|
}
|
|
35923
|
-
const directory = realPathOr(
|
|
36028
|
+
const directory = realPathOr(dirname16(path));
|
|
35924
36029
|
if (!existsSync17(directory)) {
|
|
35925
|
-
return
|
|
36030
|
+
return `${named}'s directory ${dirname16(path)} does not exist; ${what} creates no directories.`;
|
|
35926
36031
|
}
|
|
35927
|
-
const where =
|
|
35928
|
-
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
36032
|
+
const where = join23(directory, basename5(path));
|
|
36033
|
+
const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
35929
36034
|
if (inside !== undefined) {
|
|
35930
|
-
return
|
|
36035
|
+
return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
|
|
35931
36036
|
}
|
|
35932
|
-
|
|
35933
|
-
|
|
36037
|
+
const taken = rule.writes(where).find((file) => existsSync17(file));
|
|
36038
|
+
if (taken !== undefined) {
|
|
36039
|
+
return taken === where ? `${named} ${path} already exists; ${what} is not written over a file. Name a new one.` : `${named} ${path} would write ${taken}, which already exists; ${what} is not written over a file. Name a new one.`;
|
|
35934
36040
|
}
|
|
35935
36041
|
return null;
|
|
35936
36042
|
}
|
|
36043
|
+
function movieRefusal(engineArgs, projects, headless) {
|
|
36044
|
+
const at = engineArgs.indexOf("--write-movie");
|
|
36045
|
+
if (at === -1) {
|
|
36046
|
+
return null;
|
|
36047
|
+
}
|
|
36048
|
+
if (headless) {
|
|
36049
|
+
return "--write-movie needs a run that draws, and this one is headless: Movie Maker reads every frame back from the renderer, and the engine crashed on the first frame when asked headless. Start the run with headless: false; it is drawn on a desktop of its own and does not show.";
|
|
36050
|
+
}
|
|
36051
|
+
return writtenFileRefusal(engineArgs[at + 1] ?? "", {
|
|
36052
|
+
named: "--write-movie",
|
|
36053
|
+
what: "a movie",
|
|
36054
|
+
extensions: [".avi", ".png"],
|
|
36055
|
+
projects,
|
|
36056
|
+
writes: (where) => {
|
|
36057
|
+
if (extname3(where).toLowerCase() !== ".png") {
|
|
36058
|
+
return [where];
|
|
36059
|
+
}
|
|
36060
|
+
const base = where.slice(0, -".png".length);
|
|
36061
|
+
return [`${base}00000000.png`, `${base}.wav`];
|
|
36062
|
+
}
|
|
36063
|
+
});
|
|
36064
|
+
}
|
|
35937
36065
|
function captureDetail(payload) {
|
|
35938
36066
|
const parts = [];
|
|
35939
36067
|
const region = payload["region"];
|
|
@@ -36451,6 +36579,23 @@ function engineFlagsNote(args) {
|
|
|
36451
36579
|
var EDITOR_FREE_INSTEAD = {
|
|
36452
36580
|
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"
|
|
36453
36581
|
};
|
|
36582
|
+
function editorAddonMissing(projectPath) {
|
|
36583
|
+
if (!existsSync17(join23(projectPath, "project.godot"))) {
|
|
36584
|
+
return null;
|
|
36585
|
+
}
|
|
36586
|
+
const report = inspectProject(projectPath);
|
|
36587
|
+
const installed = report.addons.some((addon) => addon.name === "gdharness_editor" && addon.installed);
|
|
36588
|
+
if (installed && report.pluginsEnabled.includes("gdharness_editor")) {
|
|
36589
|
+
return null;
|
|
36590
|
+
}
|
|
36591
|
+
return {
|
|
36592
|
+
why: `the gdharness editor addon is not ${installed ? "enabled" : "installed"} in ${projectPath}`,
|
|
36593
|
+
remedies: installed ? [
|
|
36594
|
+
"gdharness setup in the project enables it",
|
|
36595
|
+
"Or enable it under Project Settings, Plugins, in an editor opened by hand"
|
|
36596
|
+
] : ["gdharness setup in the project installs and enables it"]
|
|
36597
|
+
};
|
|
36598
|
+
}
|
|
36454
36599
|
var OPS_ELSEWHERE = {
|
|
36455
36600
|
runtime_invoke: { get: "runtime_inspect property", property: "runtime_inspect property" }
|
|
36456
36601
|
};
|
|
@@ -36717,7 +36862,7 @@ function profileFrameFunctions(projectPath) {
|
|
|
36717
36862
|
let projectGodot = null;
|
|
36718
36863
|
if (projectPath !== null) {
|
|
36719
36864
|
try {
|
|
36720
|
-
projectGodot = readFileSync21(
|
|
36865
|
+
projectGodot = readFileSync21(join23(projectPath, "project.godot"), "utf8");
|
|
36721
36866
|
} catch {}
|
|
36722
36867
|
}
|
|
36723
36868
|
return frameFunctionsFor(queuedMessagesOf(projectGodot));
|
|
@@ -36895,7 +37040,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
|
|
|
36895
37040
|
var ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd";
|
|
36896
37041
|
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
36897
37042
|
try {
|
|
36898
|
-
return settingKeys(readFileSync21(
|
|
37043
|
+
return settingKeys(readFileSync21(join23(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
36899
37044
|
} catch {
|
|
36900
37045
|
return true;
|
|
36901
37046
|
}
|
|
@@ -36910,7 +37055,7 @@ function uncaughtNote(uncaught) {
|
|
|
36910
37055
|
}
|
|
36911
37056
|
function elsewhereIn(projectPath, asked) {
|
|
36912
37057
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
36913
|
-
if (wanted === "" || existsSync17(
|
|
37058
|
+
if (wanted === "" || existsSync17(join23(projectPath, wanted))) {
|
|
36914
37059
|
return "";
|
|
36915
37060
|
}
|
|
36916
37061
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -37001,13 +37146,13 @@ function scriptsToDiagnose(projectPath, given) {
|
|
|
37001
37146
|
var EXTERNAL_MEMBER = 'Could not resolve external class member "';
|
|
37002
37147
|
function isDirectory(path) {
|
|
37003
37148
|
try {
|
|
37004
|
-
return
|
|
37149
|
+
return statSync9(path).isDirectory();
|
|
37005
37150
|
} catch {
|
|
37006
37151
|
return false;
|
|
37007
37152
|
}
|
|
37008
37153
|
}
|
|
37009
37154
|
function scriptsUnder(directory, prefix) {
|
|
37010
|
-
if (existsSync17(
|
|
37155
|
+
if (existsSync17(join23(directory, ".gdignore"))) {
|
|
37011
37156
|
return [];
|
|
37012
37157
|
}
|
|
37013
37158
|
const found = [];
|
|
@@ -37017,7 +37162,7 @@ function scriptsUnder(directory, prefix) {
|
|
|
37017
37162
|
}
|
|
37018
37163
|
const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
37019
37164
|
if (entry.isDirectory()) {
|
|
37020
|
-
found.push(...scriptsUnder(
|
|
37165
|
+
found.push(...scriptsUnder(join23(directory, entry.name), path));
|
|
37021
37166
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
37022
37167
|
found.push(path);
|
|
37023
37168
|
}
|
|
@@ -37033,7 +37178,7 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
37033
37178
|
return false;
|
|
37034
37179
|
}
|
|
37035
37180
|
try {
|
|
37036
|
-
return readFileSync21(
|
|
37181
|
+
return readFileSync21(join23(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
37037
37182
|
} catch {
|
|
37038
37183
|
return false;
|
|
37039
37184
|
}
|
|
@@ -37059,7 +37204,7 @@ var STATIC_LIST_HINT = { ttlMs: 5 * 60000, cacheScope: "private" };
|
|
|
37059
37204
|
class GodotServer {
|
|
37060
37205
|
stdio = null;
|
|
37061
37206
|
locator = new GodotLocator;
|
|
37062
|
-
operationsScript =
|
|
37207
|
+
operationsScript = join23(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
37063
37208
|
godotBridge;
|
|
37064
37209
|
tools = buildToolDefinitions();
|
|
37065
37210
|
activeProcess = null;
|
|
@@ -37083,6 +37228,11 @@ class GodotServer {
|
|
|
37083
37228
|
launchedFrom = null;
|
|
37084
37229
|
launchedDropped = null;
|
|
37085
37230
|
closedHere = null;
|
|
37231
|
+
inFlight = new Map;
|
|
37232
|
+
callsTaken = 0;
|
|
37233
|
+
clientNoted = null;
|
|
37234
|
+
parentCommand = null;
|
|
37235
|
+
endNoted = false;
|
|
37086
37236
|
ownProject;
|
|
37087
37237
|
announcedAt = null;
|
|
37088
37238
|
handingOverFrom = null;
|
|
@@ -37119,6 +37269,7 @@ class GodotServer {
|
|
|
37119
37269
|
return { content: [{ type: "text", text: answerJson(payload) }] };
|
|
37120
37270
|
}
|
|
37121
37271
|
async run() {
|
|
37272
|
+
this.noteTheStart();
|
|
37122
37273
|
const located = await this.locator.find();
|
|
37123
37274
|
if (!located.ok) {
|
|
37124
37275
|
console.error(`[SERVER] ${GodotLocator.refusal(located).message} Until that is fixed every tool that runs the engine will say so.`);
|
|
@@ -37235,6 +37386,7 @@ class GodotServer {
|
|
|
37235
37386
|
return;
|
|
37236
37387
|
}
|
|
37237
37388
|
console.error(`[SERVER] pid ${successor} now serves ${this.ownProject}; this server is finished and is stopping.`);
|
|
37389
|
+
this.noteTheEnd("superseded", { successor });
|
|
37238
37390
|
this.handleShutdown(`superseded by ${successor}`, 0);
|
|
37239
37391
|
}, SUCCESSOR_CHECK_MS);
|
|
37240
37392
|
watch.unref();
|
|
@@ -37306,10 +37458,67 @@ class GodotServer {
|
|
|
37306
37458
|
}
|
|
37307
37459
|
await this.stdio?.close();
|
|
37308
37460
|
}
|
|
37461
|
+
noteTheStart() {
|
|
37462
|
+
const cwd = process.cwd();
|
|
37463
|
+
const mcpJson = join23(cwd, ".mcp.json");
|
|
37464
|
+
const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
|
|
37465
|
+
const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
|
|
37466
|
+
writeLifecycle("started", {
|
|
37467
|
+
argv: process.argv.slice(1),
|
|
37468
|
+
cwd,
|
|
37469
|
+
project: this.ownProject,
|
|
37470
|
+
mcpJson: existsSync17(mcpJson) ? mcpJson : null
|
|
37471
|
+
});
|
|
37472
|
+
if (predecessor !== null) {
|
|
37473
|
+
writeLifecycle("replaced", { project: this.ownProject, predecessor });
|
|
37474
|
+
}
|
|
37475
|
+
const ask = setTimeout(() => {
|
|
37476
|
+
processTree().then((tree) => {
|
|
37477
|
+
this.parentCommand = tree?.get(process.ppid)?.command ?? null;
|
|
37478
|
+
writeLifecycle("parent", {
|
|
37479
|
+
command: this.parentCommand,
|
|
37480
|
+
...tree === undefined ? { why: whyTheProcessTreeFailed() } : {}
|
|
37481
|
+
});
|
|
37482
|
+
});
|
|
37483
|
+
}, PARENT_ASKED_AFTER_MS);
|
|
37484
|
+
ask.unref();
|
|
37485
|
+
}
|
|
37486
|
+
noteTheEnd(cause, more = {}) {
|
|
37487
|
+
if (this.endNoted) {
|
|
37488
|
+
return;
|
|
37489
|
+
}
|
|
37490
|
+
this.endNoted = true;
|
|
37491
|
+
writeLifecycle("ended", {
|
|
37492
|
+
cause,
|
|
37493
|
+
project: this.ownProject,
|
|
37494
|
+
...this.parentCommand === null ? {} : { parentCommand: this.parentCommand },
|
|
37495
|
+
...this.clientNoted === null ? {} : { client: this.clientNoted },
|
|
37496
|
+
inFlight: callsInFlight(this.inFlight.values()),
|
|
37497
|
+
...more
|
|
37498
|
+
});
|
|
37499
|
+
}
|
|
37500
|
+
noteTheClient(client) {
|
|
37501
|
+
if (client === undefined) {
|
|
37502
|
+
return;
|
|
37503
|
+
}
|
|
37504
|
+
const said = `${client.name} ${client.version}`;
|
|
37505
|
+
if (said === this.clientNoted) {
|
|
37506
|
+
return;
|
|
37507
|
+
}
|
|
37508
|
+
this.clientNoted = said;
|
|
37509
|
+
writeLifecycle("client", { client: { name: client.name, version: client.version } });
|
|
37510
|
+
}
|
|
37309
37511
|
setupShutdownHandlers() {
|
|
37310
37512
|
const requestShutdown = (source, exitCode) => {
|
|
37311
37513
|
this.handleShutdown(source, exitCode);
|
|
37312
37514
|
};
|
|
37515
|
+
process.on("uncaughtException", (error, origin) => {
|
|
37516
|
+
this.noteTheEnd(origin === "unhandledRejection" ? "unhandledRejection" : "uncaughtException", {
|
|
37517
|
+
error: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
37518
|
+
});
|
|
37519
|
+
console.error(error);
|
|
37520
|
+
process.exit(1);
|
|
37521
|
+
});
|
|
37313
37522
|
process.once("SIGINT", () => {
|
|
37314
37523
|
requestShutdown("SIGINT", 0);
|
|
37315
37524
|
});
|
|
@@ -37322,7 +37531,8 @@ class GodotServer {
|
|
|
37322
37531
|
process.once("beforeExit", (code) => {
|
|
37323
37532
|
requestShutdown(`beforeExit:${code}`);
|
|
37324
37533
|
});
|
|
37325
|
-
process.once("exit", () => {
|
|
37534
|
+
process.once("exit", (code) => {
|
|
37535
|
+
this.noteTheEnd(`exit:${code}`);
|
|
37326
37536
|
this.forceCleanupOnExit();
|
|
37327
37537
|
});
|
|
37328
37538
|
}
|
|
@@ -37331,6 +37541,7 @@ class GodotServer {
|
|
|
37331
37541
|
return;
|
|
37332
37542
|
}
|
|
37333
37543
|
this.shutdownInitiated = true;
|
|
37544
|
+
this.noteTheEnd(source);
|
|
37334
37545
|
this.logDebug(`Shutting down server via ${source}`);
|
|
37335
37546
|
try {
|
|
37336
37547
|
await this.cleanup();
|
|
@@ -37370,9 +37581,23 @@ class GodotServer {
|
|
|
37370
37581
|
return mcp;
|
|
37371
37582
|
}
|
|
37372
37583
|
setupToolHandlers(protocol) {
|
|
37373
|
-
|
|
37584
|
+
const clientOf = (envelope) => {
|
|
37585
|
+
const named = envelope?.["io.modelcontextprotocol/clientInfo"];
|
|
37586
|
+
if (typeof named === "object" && named !== null) {
|
|
37587
|
+
const { name, version } = named;
|
|
37588
|
+
if (typeof name === "string" && typeof version === "string") {
|
|
37589
|
+
return { name, version };
|
|
37590
|
+
}
|
|
37591
|
+
}
|
|
37592
|
+
return protocol.getClientVersion();
|
|
37593
|
+
};
|
|
37594
|
+
protocol.setRequestHandler("tools/list", (_request, ctx) => {
|
|
37595
|
+
this.noteTheClient(clientOf(ctx.mcpReq.envelope));
|
|
37596
|
+
return { tools: this.tools };
|
|
37597
|
+
});
|
|
37374
37598
|
protocol.setRequestHandler("tools/call", async (request, ctx) => {
|
|
37375
37599
|
this.logDebug(`Handling tool request: ${request.params.name}`);
|
|
37600
|
+
this.noteTheClient(clientOf(ctx.mcpReq.envelope));
|
|
37376
37601
|
const spec = toolSpec(request.params.name);
|
|
37377
37602
|
if (!spec) {
|
|
37378
37603
|
throw new ProtocolError(ProtocolErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}. Tools: ${TOOL_SPECS.map((tool) => tool.name).join(", ")}`);
|
|
@@ -37383,7 +37608,7 @@ class GodotServer {
|
|
|
37383
37608
|
return checked.response;
|
|
37384
37609
|
}
|
|
37385
37610
|
const named = args["projectPath"];
|
|
37386
|
-
if (typeof named === "string" && existsSync17(
|
|
37611
|
+
if (typeof named === "string" && existsSync17(join23(named, "project.godot"))) {
|
|
37387
37612
|
this.lastProjectPath = named;
|
|
37388
37613
|
}
|
|
37389
37614
|
this.updates.refresh();
|
|
@@ -37396,8 +37621,14 @@ class GodotServer {
|
|
|
37396
37621
|
this.logDebug(`Progress for ${spec.name} was not sent: ${errorMessage(error)}`);
|
|
37397
37622
|
});
|
|
37398
37623
|
};
|
|
37399
|
-
const
|
|
37400
|
-
|
|
37624
|
+
const call = this.callsTaken++;
|
|
37625
|
+
this.inFlight.set(call, { tool: spec.name, op: checked.op ?? "", startedAt: Date.now() });
|
|
37626
|
+
try {
|
|
37627
|
+
const answer = await withCallSignal(ctx.mcpReq.signal, () => this.answered(spec.name, checked.op ?? "", args), progress);
|
|
37628
|
+
return this.withFeedbackNotice(this.withUpdateNotice(answer));
|
|
37629
|
+
} finally {
|
|
37630
|
+
this.inFlight.delete(call);
|
|
37631
|
+
}
|
|
37401
37632
|
});
|
|
37402
37633
|
}
|
|
37403
37634
|
async answered(tool, op, args) {
|
|
@@ -37616,10 +37847,12 @@ class GodotServer {
|
|
|
37616
37847
|
]);
|
|
37617
37848
|
}
|
|
37618
37849
|
if (!this.godotBridge.isConnected()) {
|
|
37619
|
-
|
|
37620
|
-
|
|
37621
|
-
'Open the project with editor_launch, or pass from: "disk" to read the file instead'
|
|
37850
|
+
const refused = await this.noEditorYet(answerable, [
|
|
37851
|
+
`${tool} ${op} was asked of the editor; from: "disk" reads the file instead`
|
|
37622
37852
|
]);
|
|
37853
|
+
if (refused !== null) {
|
|
37854
|
+
return refused;
|
|
37855
|
+
}
|
|
37623
37856
|
}
|
|
37624
37857
|
const { op: _dropped, ...asks } = answerable;
|
|
37625
37858
|
const contained = this.containProjectFiles(asks);
|
|
@@ -37704,11 +37937,11 @@ class GodotServer {
|
|
|
37704
37937
|
case "script_info":
|
|
37705
37938
|
switch (op) {
|
|
37706
37939
|
case "symbols":
|
|
37707
|
-
return await this.
|
|
37940
|
+
return await this.askScriptInfo("lsp_get_symbols", "Symbols", args);
|
|
37708
37941
|
case "completion":
|
|
37709
|
-
return await this.
|
|
37942
|
+
return await this.askScriptInfo("lsp_get_completions", "Completion", args);
|
|
37710
37943
|
default:
|
|
37711
|
-
return await this.
|
|
37944
|
+
return await this.askScriptInfo("lsp_get_hover", "Hover", args);
|
|
37712
37945
|
}
|
|
37713
37946
|
case "script_diagnostics":
|
|
37714
37947
|
return await this.handleScriptDiagnostics(args);
|
|
@@ -37885,7 +38118,7 @@ class GodotServer {
|
|
|
37885
38118
|
if (path === undefined) {
|
|
37886
38119
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
37887
38120
|
}
|
|
37888
|
-
const file =
|
|
38121
|
+
const file = join23(path, "project.godot");
|
|
37889
38122
|
if (!existsSync17(file)) {
|
|
37890
38123
|
return {
|
|
37891
38124
|
ok: false,
|
|
@@ -38572,18 +38805,18 @@ class GodotServer {
|
|
|
38572
38805
|
if (!engine.ok) {
|
|
38573
38806
|
return engine.response;
|
|
38574
38807
|
}
|
|
38575
|
-
const folder =
|
|
38808
|
+
const folder = dirname16(output.absolutePath);
|
|
38576
38809
|
try {
|
|
38577
|
-
|
|
38810
|
+
mkdirSync14(folder, { recursive: true });
|
|
38578
38811
|
} catch (error) {
|
|
38579
|
-
return this.createErrorResponse(`Cannot create ${
|
|
38812
|
+
return this.createErrorResponse(`Cannot create ${dirname16(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
38580
38813
|
}
|
|
38581
38814
|
const debug = readBoolean(args, "debug") ?? false;
|
|
38582
38815
|
const exportLogs = scratchDirectory("export");
|
|
38583
38816
|
const exportArgs = [
|
|
38584
38817
|
"--headless",
|
|
38585
38818
|
"--log-file",
|
|
38586
|
-
|
|
38819
|
+
join23(exportLogs, "engine.log"),
|
|
38587
38820
|
"--path",
|
|
38588
38821
|
project.value.path,
|
|
38589
38822
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -38591,7 +38824,7 @@ class GodotServer {
|
|
|
38591
38824
|
output.absolutePath
|
|
38592
38825
|
];
|
|
38593
38826
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
38594
|
-
const writtenAt = () => existsSync17(output.absolutePath) ?
|
|
38827
|
+
const writtenAt = () => existsSync17(output.absolutePath) ? statSync9(output.absolutePath).mtimeMs : null;
|
|
38595
38828
|
const before = writtenAt();
|
|
38596
38829
|
let ending;
|
|
38597
38830
|
try {
|
|
@@ -38647,10 +38880,18 @@ class GodotServer {
|
|
|
38647
38880
|
if (!paths.ok) {
|
|
38648
38881
|
return this.createErrorResponse(paths.reason, PATH_SOLUTIONS);
|
|
38649
38882
|
}
|
|
38650
|
-
const
|
|
38883
|
+
const flags = this.engineArguments(args);
|
|
38884
|
+
if (!flags.ok) {
|
|
38885
|
+
return flags.response;
|
|
38886
|
+
}
|
|
38887
|
+
const movie = movieRefusal(flags.value, [project.value.path], true);
|
|
38888
|
+
if (movie !== null) {
|
|
38889
|
+
return this.createErrorResponse(movie);
|
|
38890
|
+
}
|
|
38891
|
+
const missing = paths.value.filter((path) => !existsSync17(join23(project.value.path, path.slice("res://".length))));
|
|
38651
38892
|
const asked = paths.value.filter((path) => !missing.includes(path));
|
|
38652
38893
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
38653
|
-
const noRunner = !existsSync17(
|
|
38894
|
+
const noRunner = !existsSync17(join23(project.value.path, runner));
|
|
38654
38895
|
if (asked.length === 0) {
|
|
38655
38896
|
const verdict = `nothing at ${missing.join(" and ")}`;
|
|
38656
38897
|
const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
|
|
@@ -38694,6 +38935,7 @@ class GodotServer {
|
|
|
38694
38935
|
"--headless",
|
|
38695
38936
|
"--path",
|
|
38696
38937
|
project.value.path,
|
|
38938
|
+
...flags.value,
|
|
38697
38939
|
"-s",
|
|
38698
38940
|
`res://${runner}`,
|
|
38699
38941
|
"--ignoreHeadlessMode",
|
|
@@ -38740,24 +38982,28 @@ class GodotServer {
|
|
|
38740
38982
|
await untilGone([run.process.pid]);
|
|
38741
38983
|
}
|
|
38742
38984
|
await run.settled;
|
|
38743
|
-
const reportsDir =
|
|
38985
|
+
const reportsDir = join23(project.value.path, ".godot", ...ours.split("/"));
|
|
38744
38986
|
let report = null;
|
|
38745
38987
|
let reportProblem = null;
|
|
38746
38988
|
try {
|
|
38747
38989
|
const written = existsSync17(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
38748
38990
|
const newest = written.at(-1);
|
|
38749
38991
|
if (newest !== undefined) {
|
|
38750
|
-
report = parseJUnit(readFileSync21(
|
|
38992
|
+
report = parseJUnit(readFileSync21(join23(reportsDir, newest, "results.xml"), "utf8"));
|
|
38751
38993
|
}
|
|
38752
38994
|
} catch (error) {
|
|
38753
38995
|
reportProblem = errorMessage(error);
|
|
38754
38996
|
} finally {
|
|
38755
38997
|
discard(reportsDir, userData);
|
|
38756
38998
|
try {
|
|
38757
|
-
rmdirSync2(
|
|
38999
|
+
rmdirSync2(dirname16(reportsDir));
|
|
38758
39000
|
} catch {}
|
|
38759
39001
|
}
|
|
38760
|
-
const engineEntries = foldedRepeats(run.log.select({
|
|
39002
|
+
const engineEntries = foldedRepeats(run.log.select({
|
|
39003
|
+
severity: flags.value.includes("--verbose") ? "info" : "warning",
|
|
39004
|
+
sinceLastCall: false,
|
|
39005
|
+
limit: Number.POSITIVE_INFINITY
|
|
39006
|
+
}).entries.map(aboveTheRunner));
|
|
38761
39007
|
const engineEntriesShown = engineEntries.slice(-MOST_ENGINE_ENTRIES);
|
|
38762
39008
|
const verdicts = {
|
|
38763
39009
|
0: "passed",
|
|
@@ -38868,6 +39114,7 @@ class GodotServer {
|
|
|
38868
39114
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
38869
39115
|
const uncaughtFolded = foldedRepeats(uncaught.map(aboveTheRunner));
|
|
38870
39116
|
const listedAsUncaught = new Set(uncaughtFolded.map((entry) => entry.index));
|
|
39117
|
+
const notApplied = ignoredYetRan(report, ignored);
|
|
38871
39118
|
return this.jsonTextResponse({
|
|
38872
39119
|
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0,
|
|
38873
39120
|
verdict,
|
|
@@ -38886,11 +39133,13 @@ class GodotServer {
|
|
|
38886
39133
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
38887
39134
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
38888
39135
|
notRun: notRun > 0 ? notRun : undefined,
|
|
39136
|
+
ignoreNotApplied: notApplied.length > 0 ? notApplied : undefined,
|
|
38889
39137
|
...printedLines,
|
|
38890
39138
|
savesNote,
|
|
38891
39139
|
...scanned,
|
|
38892
39140
|
note: [
|
|
38893
39141
|
missingNote,
|
|
39142
|
+
notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
|
|
38894
39143
|
uncaughtNote(uncaught),
|
|
38895
39144
|
ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
|
|
38896
39145
|
].filter((part) => part !== undefined).join(" ") || undefined,
|
|
@@ -38979,31 +39228,9 @@ class GodotServer {
|
|
|
38979
39228
|
async diagnosticsOf(args) {
|
|
38980
39229
|
const answer = await this.handleLSP("lsp_get_diagnostics", args);
|
|
38981
39230
|
const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
38982
|
-
|
|
38983
|
-
|
|
38984
|
-
|
|
38985
|
-
return { refusal: this.createErrorResponse(reason), perScript: true };
|
|
38986
|
-
}
|
|
38987
|
-
if (payload["stillReading"] === true) {
|
|
38988
|
-
return { refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`), perScript: false };
|
|
38989
|
-
}
|
|
38990
|
-
if (typeof payload["serves"] === "string") {
|
|
38991
|
-
return {
|
|
38992
|
-
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
38993
|
-
"editor_status names the editor on the bridge and the port it says it serves",
|
|
38994
|
-
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
38995
|
-
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
38996
|
-
]),
|
|
38997
|
-
perScript: false
|
|
38998
|
-
};
|
|
38999
|
-
}
|
|
39000
|
-
return {
|
|
39001
|
-
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
39002
|
-
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
39003
|
-
"GDHARNESS_LSP_PORT points this server at another one"
|
|
39004
|
-
]),
|
|
39005
|
-
perScript: payload["aboutThisFile"] === true
|
|
39006
|
-
};
|
|
39231
|
+
const refused = this.languageServerRefusal(payload, "Diagnostics");
|
|
39232
|
+
if (refused !== null) {
|
|
39233
|
+
return refused;
|
|
39007
39234
|
}
|
|
39008
39235
|
const diagnostics = readArray(payload, "diagnostics") ?? [];
|
|
39009
39236
|
const errors = diagnostics.filter((entry) => asParams(entry)["severity"] === "error").length;
|
|
@@ -39110,6 +39337,40 @@ class GodotServer {
|
|
|
39110
39337
|
async handleLSP(toolName, args) {
|
|
39111
39338
|
return handleLSPTool(await this.lsp(), toolName, args);
|
|
39112
39339
|
}
|
|
39340
|
+
async askScriptInfo(toolName, asked, args) {
|
|
39341
|
+
const answer = await this.handleLSP(toolName, args);
|
|
39342
|
+
const refused = this.languageServerRefusal(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), asked);
|
|
39343
|
+
return refused === null ? answer : refused.refusal;
|
|
39344
|
+
}
|
|
39345
|
+
languageServerRefusal(payload, asked) {
|
|
39346
|
+
if (payload["error"] === undefined) {
|
|
39347
|
+
return null;
|
|
39348
|
+
}
|
|
39349
|
+
const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
|
|
39350
|
+
if (payload["refusedArguments"] === true) {
|
|
39351
|
+
return { refusal: this.createErrorResponse(reason), perScript: true };
|
|
39352
|
+
}
|
|
39353
|
+
if (payload["stillReading"] === true) {
|
|
39354
|
+
return { refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`), perScript: false };
|
|
39355
|
+
}
|
|
39356
|
+
if (typeof payload["serves"] === "string") {
|
|
39357
|
+
return {
|
|
39358
|
+
refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
|
|
39359
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
39360
|
+
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
39361
|
+
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
39362
|
+
]),
|
|
39363
|
+
perScript: false
|
|
39364
|
+
};
|
|
39365
|
+
}
|
|
39366
|
+
return {
|
|
39367
|
+
refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
|
|
39368
|
+
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
39369
|
+
"GDHARNESS_LSP_PORT points this server at another one"
|
|
39370
|
+
]),
|
|
39371
|
+
perScript: payload["aboutThisFile"] === true
|
|
39372
|
+
};
|
|
39373
|
+
}
|
|
39113
39374
|
async lsp() {
|
|
39114
39375
|
const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
|
|
39115
39376
|
if (this.lspClient !== null && this.lspClient.port !== port) {
|
|
@@ -39210,7 +39471,7 @@ class GodotServer {
|
|
|
39210
39471
|
return;
|
|
39211
39472
|
}
|
|
39212
39473
|
try {
|
|
39213
|
-
|
|
39474
|
+
appendFileSync2(run.transcript, line.endsWith(`
|
|
39214
39475
|
`) ? line : `${line}
|
|
39215
39476
|
`);
|
|
39216
39477
|
} catch (error) {
|
|
@@ -39421,7 +39682,8 @@ class GodotServer {
|
|
|
39421
39682
|
serverSpeaks: RUNTIME_PROTOCOL,
|
|
39422
39683
|
behind: game.protocol > RUNTIME_PROTOCOL ? "server" : "addon"
|
|
39423
39684
|
}))
|
|
39424
|
-
}
|
|
39685
|
+
},
|
|
39686
|
+
server: { pid: process.pid, version: SERVER_VERSION, lifecycleLog: lifecycleLogPath() }
|
|
39425
39687
|
});
|
|
39426
39688
|
}
|
|
39427
39689
|
async editorServersListening() {
|
|
@@ -39453,7 +39715,7 @@ class GodotServer {
|
|
|
39453
39715
|
return null;
|
|
39454
39716
|
}
|
|
39455
39717
|
try {
|
|
39456
|
-
return settingKeys(readFileSync21(
|
|
39718
|
+
return settingKeys(readFileSync21(join23(projectPath, "project.godot"), "utf8"));
|
|
39457
39719
|
} catch {
|
|
39458
39720
|
return null;
|
|
39459
39721
|
}
|
|
@@ -39509,6 +39771,10 @@ class GodotServer {
|
|
|
39509
39771
|
async handleCloseEditor() {
|
|
39510
39772
|
const before = this.godotBridge.getStatus();
|
|
39511
39773
|
if (!before.connected) {
|
|
39774
|
+
const launched = this.launchedEditor;
|
|
39775
|
+
if (launched !== null && await this.launchedEditorIsUp() && !this.godotBridge.isConnected()) {
|
|
39776
|
+
return await this.stopTheLaunchedEditor(launched);
|
|
39777
|
+
}
|
|
39512
39778
|
return this.createErrorResponse("No editor is connected, so there is nothing to close.", [
|
|
39513
39779
|
"editor_status says whether the bridge is up and what has reached it"
|
|
39514
39780
|
]);
|
|
@@ -39542,6 +39808,31 @@ class GodotServer {
|
|
|
39542
39808
|
tookMs: Date.now() - began
|
|
39543
39809
|
});
|
|
39544
39810
|
}
|
|
39811
|
+
async stopTheLaunchedEditor(launched) {
|
|
39812
|
+
const began = Date.now();
|
|
39813
|
+
try {
|
|
39814
|
+
process.kill(launched.pid);
|
|
39815
|
+
} catch (error) {
|
|
39816
|
+
if (alive(launched.pid)) {
|
|
39817
|
+
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"]);
|
|
39818
|
+
}
|
|
39819
|
+
}
|
|
39820
|
+
const stopWaitMs = 1e4;
|
|
39821
|
+
if (!await this.waitForBridge(() => !alive(launched.pid), began + stopWaitMs)) {
|
|
39822
|
+
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"]);
|
|
39823
|
+
}
|
|
39824
|
+
this.closedHere = { editorPid: launched.pid, at: new Date().toISOString() };
|
|
39825
|
+
this.launchedEditor = null;
|
|
39826
|
+
this.launchedFrom = null;
|
|
39827
|
+
return this.jsonTextResponse({
|
|
39828
|
+
closed: true,
|
|
39829
|
+
editorPid: launched.pid,
|
|
39830
|
+
projectPath: launched.projectPath,
|
|
39831
|
+
beforeConnecting: true,
|
|
39832
|
+
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.",
|
|
39833
|
+
tookMs: Date.now() - began
|
|
39834
|
+
});
|
|
39835
|
+
}
|
|
39545
39836
|
settleTheLaunchReading() {
|
|
39546
39837
|
const watched = this.launchedFrom;
|
|
39547
39838
|
const status = this.godotBridge.getStatus();
|
|
@@ -39677,13 +39968,9 @@ class GodotServer {
|
|
|
39677
39968
|
`editor_launch with projectPath ${this.ownProject} opens the editor this server serves`
|
|
39678
39969
|
]);
|
|
39679
39970
|
}
|
|
39680
|
-
const
|
|
39681
|
-
|
|
39682
|
-
|
|
39683
|
-
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 ? [
|
|
39684
|
-
"gdharness setup in the project enables it",
|
|
39685
|
-
"Or enable it under Project Settings, Plugins, in an editor opened by hand"
|
|
39686
|
-
] : ["gdharness setup in the project installs and enables it"]);
|
|
39971
|
+
const missing = editorAddonMissing(projectPath);
|
|
39972
|
+
if (missing !== null) {
|
|
39973
|
+
return this.createErrorResponse(`${missing.why}, so an editor opened on it would never reach this server.`, missing.remedies);
|
|
39687
39974
|
}
|
|
39688
39975
|
const owed = restartOwed(projectPath);
|
|
39689
39976
|
if (owed !== null && restartUnderway(owed)) {
|
|
@@ -39711,7 +39998,8 @@ class GodotServer {
|
|
|
39711
39998
|
writeEditorLogNote(projectPath, launched.pid);
|
|
39712
39999
|
this.launchedEditor = {
|
|
39713
40000
|
pid: launched.pid,
|
|
39714
|
-
startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null
|
|
40001
|
+
startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null,
|
|
40002
|
+
projectPath
|
|
39715
40003
|
};
|
|
39716
40004
|
this.closedHere = null;
|
|
39717
40005
|
return { pid: launched.pid, error: null };
|
|
@@ -39750,6 +40038,10 @@ class GodotServer {
|
|
|
39750
40038
|
if (!flags.ok) {
|
|
39751
40039
|
return flags.response;
|
|
39752
40040
|
}
|
|
40041
|
+
const movie = movieRefusal(flags.value, [project.value.path], op === "check" || resolveHeadless(args["headless"], { platform: process.platform, variables: process.env }));
|
|
40042
|
+
if (movie !== null) {
|
|
40043
|
+
return this.createErrorResponse(movie);
|
|
40044
|
+
}
|
|
39753
40045
|
const asked = this.runEnvironment(args);
|
|
39754
40046
|
if (!asked.ok) {
|
|
39755
40047
|
return asked.response;
|
|
@@ -39844,7 +40136,7 @@ class GodotServer {
|
|
|
39844
40136
|
}
|
|
39845
40137
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
39846
40138
|
const budgetMs = budget.ms;
|
|
39847
|
-
if (!existsSync17(
|
|
40139
|
+
if (!existsSync17(join23(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
39848
40140
|
return runtimeVerdict(null, {
|
|
39849
40141
|
addon: false,
|
|
39850
40142
|
budgetMs,
|
|
@@ -39929,7 +40221,7 @@ class GodotServer {
|
|
|
39929
40221
|
}
|
|
39930
40222
|
let announcedAt;
|
|
39931
40223
|
try {
|
|
39932
|
-
announcedAt =
|
|
40224
|
+
announcedAt = statSync9(endpoint.file).mtimeMs;
|
|
39933
40225
|
} catch {
|
|
39934
40226
|
return;
|
|
39935
40227
|
}
|
|
@@ -40098,7 +40390,7 @@ class GodotServer {
|
|
|
40098
40390
|
return;
|
|
40099
40391
|
}
|
|
40100
40392
|
if (game.transcript !== null) {
|
|
40101
|
-
|
|
40393
|
+
appendFileSync2(game.transcript, reported);
|
|
40102
40394
|
} else {
|
|
40103
40395
|
game.log.append("stderr", reported);
|
|
40104
40396
|
}
|
|
@@ -40802,7 +41094,7 @@ class GodotServer {
|
|
|
40802
41094
|
}
|
|
40803
41095
|
const going = await this.runStillGoing(run);
|
|
40804
41096
|
const projectPath = run.projectPath;
|
|
40805
|
-
const projectGone = projectPath !== null && !existsSync17(
|
|
41097
|
+
const projectGone = projectPath !== null && !existsSync17(join23(projectPath, "project.godot"));
|
|
40806
41098
|
if (projectGone && callersName !== undefined) {
|
|
40807
41099
|
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"]);
|
|
40808
41100
|
}
|
|
@@ -40973,7 +41265,11 @@ class GodotServer {
|
|
|
40973
41265
|
if (!greeted && launched === null && status.connected) {
|
|
40974
41266
|
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"]);
|
|
40975
41267
|
}
|
|
40976
|
-
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
41268
|
+
const projectPath = status.projectPath ?? this.launchedEditor?.projectPath ?? this.ownProject ?? undefined;
|
|
41269
|
+
const elsewhere = this.anotherProjectNamed(args, projectPath);
|
|
41270
|
+
if (elsewhere !== null) {
|
|
41271
|
+
return elsewhere;
|
|
41272
|
+
}
|
|
40977
41273
|
if (projectPath === undefined || !greeted && launched === null) {
|
|
40978
41274
|
return this.createErrorResponse("No editor is connected, so there is no console to read.", [
|
|
40979
41275
|
"editor_status says whether one is connected and whether it may yet be",
|
|
@@ -41024,6 +41320,19 @@ class GodotServer {
|
|
|
41024
41320
|
omitted: selected.omitted === 0 ? undefined : selected.omitted
|
|
41025
41321
|
});
|
|
41026
41322
|
}
|
|
41323
|
+
anotherProjectNamed(args, ofWhatIsRead) {
|
|
41324
|
+
const named = readNonEmptyString(args, "projectPath");
|
|
41325
|
+
if (named === undefined || ofWhatIsRead === null || ofWhatIsRead === undefined || ofWhatIsRead === "") {
|
|
41326
|
+
return null;
|
|
41327
|
+
}
|
|
41328
|
+
if (isSameDirectory(named, ofWhatIsRead)) {
|
|
41329
|
+
return null;
|
|
41330
|
+
}
|
|
41331
|
+
return this.createErrorResponse(`editor_output reads what this server holds, which is of ${ofWhatIsRead}, and this call names ${named}.`, [
|
|
41332
|
+
"Another project's run is read through that project's own gdharness server",
|
|
41333
|
+
"editor_status names every game running on the machine under runtimes"
|
|
41334
|
+
]);
|
|
41335
|
+
}
|
|
41027
41336
|
async handleGetDebugOutput(args, waitedMs) {
|
|
41028
41337
|
if (readString(args, "op") === "editor") {
|
|
41029
41338
|
return this.handleEditorConsole(args);
|
|
@@ -41033,6 +41342,10 @@ class GodotServer {
|
|
|
41033
41342
|
if (!run) {
|
|
41034
41343
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
41035
41344
|
}
|
|
41345
|
+
const elsewhere = this.anotherProjectNamed(args, run.projectPath ?? this.godotBridge.getStatus().projectPath);
|
|
41346
|
+
if (elsewhere !== null) {
|
|
41347
|
+
return elsewhere;
|
|
41348
|
+
}
|
|
41036
41349
|
await this.tieThePlayedGame(run);
|
|
41037
41350
|
this.drainEditorOutput(run);
|
|
41038
41351
|
this.drainTranscript(run);
|
|
@@ -41069,7 +41382,7 @@ class GodotServer {
|
|
|
41069
41382
|
notes.push(endedWithoutACode({
|
|
41070
41383
|
throughEditor: run.throughEditor,
|
|
41071
41384
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
41072
|
-
couldAnnounce: run.projectPath !== null && existsSync17(
|
|
41385
|
+
couldAnnounce: run.projectPath !== null && existsSync17(join23(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
41073
41386
|
endedHere: typeof run.endedHere === "string",
|
|
41074
41387
|
pickedUp: run.pickedUp === true
|
|
41075
41388
|
}));
|
|
@@ -41849,15 +42162,47 @@ class GodotServer {
|
|
|
41849
42162
|
const holds = held["classes"].map(String);
|
|
41850
42163
|
return { unseen: unseenByEditor(projectPath, holds), stillHeld: heldButGone(projectPath, holds) };
|
|
41851
42164
|
}
|
|
42165
|
+
async noEditorYet(args, instead) {
|
|
42166
|
+
const project = readNonEmptyString(args, "projectPath") ?? this.ownProject;
|
|
42167
|
+
const missing = project === null ? null : editorAddonMissing(project);
|
|
42168
|
+
if (missing !== null) {
|
|
42169
|
+
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"]);
|
|
42170
|
+
}
|
|
42171
|
+
const launched = this.launchedEditor;
|
|
42172
|
+
if (launched !== null && await this.launchedEditorIsUp()) {
|
|
42173
|
+
const began = Date.now();
|
|
42174
|
+
await this.waitForBridge(() => hasSaidWhoItIs(this.godotBridge.getStatus()) || !alive(launched.pid), began + editorRestartTimeoutMs());
|
|
42175
|
+
if (this.godotBridge.isConnected()) {
|
|
42176
|
+
return null;
|
|
42177
|
+
}
|
|
42178
|
+
const opened = `the editor this server opened on ${launched.projectPath}, pid ${launched.pid}`;
|
|
42179
|
+
if (alive(launched.pid)) {
|
|
42180
|
+
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.`, [
|
|
42181
|
+
...instead,
|
|
42182
|
+
"editor_status names it under awaitingLaunchedEditor until it has connected",
|
|
42183
|
+
"editor_output op editor shows what it is printing meanwhile"
|
|
42184
|
+
]);
|
|
42185
|
+
}
|
|
42186
|
+
return this.createErrorResponse(`Editor not connected: ${opened}, exited without reaching the bridge.`, [
|
|
42187
|
+
...instead,
|
|
42188
|
+
"editor_output op editor shows what it printed before it went",
|
|
42189
|
+
"editor_launch open starts another"
|
|
42190
|
+
]);
|
|
42191
|
+
}
|
|
42192
|
+
return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
|
|
42193
|
+
...instead,
|
|
42194
|
+
"Open the project in the editor with editor_launch, or by hand",
|
|
42195
|
+
"Enable the addon under Project > Project Settings > Plugins",
|
|
42196
|
+
"editor_status says whether the bridge is up and what it is listening on"
|
|
42197
|
+
]);
|
|
42198
|
+
}
|
|
41852
42199
|
async handleViaBridge(toolName, args, askedAs) {
|
|
41853
42200
|
if (!this.godotBridge.isConnected()) {
|
|
41854
|
-
const
|
|
41855
|
-
|
|
41856
|
-
|
|
41857
|
-
|
|
41858
|
-
|
|
41859
|
-
"editor_status says whether the bridge is up and what it is listening on"
|
|
41860
|
-
]);
|
|
42201
|
+
const alternative = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
|
|
42202
|
+
const refused = await this.noEditorYet(args, alternative === undefined ? [] : [alternative]);
|
|
42203
|
+
if (refused !== null) {
|
|
42204
|
+
return refused;
|
|
42205
|
+
}
|
|
41861
42206
|
}
|
|
41862
42207
|
const open = this.godotBridge.getStatus().projectPath;
|
|
41863
42208
|
const meant = readNonEmptyString(args, "projectPath");
|
|
@@ -41930,7 +42275,7 @@ class GodotServer {
|
|
|
41930
42275
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
41931
42276
|
const run = await this.currentRun();
|
|
41932
42277
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
41933
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync17(
|
|
42278
|
+
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)) {
|
|
41934
42279
|
return null;
|
|
41935
42280
|
}
|
|
41936
42281
|
const already = runtimesAnnounced().running;
|
|
@@ -41939,7 +42284,7 @@ class GodotServer {
|
|
|
41939
42284
|
return !run.announcedBefore.has(one.pid);
|
|
41940
42285
|
}
|
|
41941
42286
|
try {
|
|
41942
|
-
return
|
|
42287
|
+
return statSync9(one.file).mtimeMs >= run.startedAt;
|
|
41943
42288
|
} catch {
|
|
41944
42289
|
return false;
|
|
41945
42290
|
}
|
|
@@ -42015,7 +42360,7 @@ class GodotServer {
|
|
|
42015
42360
|
}
|
|
42016
42361
|
}
|
|
42017
42362
|
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
42018
|
-
const screenshotPath = screenshotDir ?
|
|
42363
|
+
const screenshotPath = screenshotDir ? join23(screenshotDir, "capture.png") : null;
|
|
42019
42364
|
const staleNote = this.runtimeStaleNote(choice.endpoint);
|
|
42020
42365
|
const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
|
|
42021
42366
|
const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
|