gdharness 1.2.24 → 1.2.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +508 -174
- package/build/godot/addons/gdharness_editor/serialisation.gd +29 -0
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +290 -0
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +7 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +24 -4
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +150 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +5 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +29 -0
- package/build/godot/operations/godot_operations.gd +7 -0
- package/build/godot/operations/import_pipeline.gd +92 -0
- package/build/godot/operations/input_actions.gd +3 -2
- package/build/godot/operations/plugins.gd +3 -2
- package/build/godot/operations/project_config.gd +17 -9
- package/build/godot/operations/project_file.gd +202 -0
- package/build/godot/operations/serialisation.gd +29 -0
- package/build/index.js +520 -190
- package/build/keeper.js +16 -9
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -70,6 +70,7 @@ var init_errors = __esm(() => {
|
|
|
70
70
|
|
|
71
71
|
// src/windows-ask.ts
|
|
72
72
|
import { spawn } from "node:child_process";
|
|
73
|
+
import { tmpdir } from "node:os";
|
|
73
74
|
import process2 from "node:process";
|
|
74
75
|
function holding(child, wanted) {
|
|
75
76
|
if (held === wanted) {
|
|
@@ -94,7 +95,7 @@ function running() {
|
|
|
94
95
|
"Bypass",
|
|
95
96
|
"-EncodedCommand",
|
|
96
97
|
Buffer.from(SCRIPT, "utf16le").toString("base64")
|
|
97
|
-
], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
98
|
+
], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, cwd: tmpdir() });
|
|
98
99
|
buffered = "";
|
|
99
100
|
child.stdout.setEncoding("utf8");
|
|
100
101
|
child.stdout.on("data", (chunk) => {
|
|
@@ -651,7 +652,7 @@ function engineFlags(given) {
|
|
|
651
652
|
const setBy = Object.hasOwn(SET_BY_ARGUMENT, flag) ? SET_BY_ARGUMENT[flag] : undefined;
|
|
652
653
|
return {
|
|
653
654
|
ok: false,
|
|
654
|
-
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
|
|
655
|
+
reason: setBy !== undefined ? `${flag} is set by this tool's own argument, ${setBy}, rather than through engineArgs.` : `${flag} is not one of the engine flags engineArgs takes, which are those that change what the engine logs, draws for debugging, checks on the GPU, which renderer draws, how it times frames or whether it builds the tree a screen reader reads, and Movie Maker's --write-movie: ${offered}. A value goes as the next entry, as in ["--fixed-fps", "60"].`
|
|
655
656
|
};
|
|
656
657
|
}
|
|
657
658
|
if (takes === "none") {
|
|
@@ -747,12 +748,27 @@ var init_launch = __esm(() => {
|
|
|
747
748
|
"--fixed-fps": "number",
|
|
748
749
|
"--max-fps": "number",
|
|
749
750
|
"--resolution": "size",
|
|
750
|
-
"--write-movie": "file"
|
|
751
|
+
"--write-movie": "file",
|
|
752
|
+
"--rendering-driver": "driver",
|
|
753
|
+
"--rendering-method": "method",
|
|
754
|
+
"--accessibility": "accessibility"
|
|
751
755
|
});
|
|
752
756
|
VALUE_KIND = {
|
|
753
757
|
number: { pattern: /^\d+(?:\.\d+)?$/, named: "a number" },
|
|
754
758
|
size: { pattern: /^\d+x\d+$/, named: "a size such as 1280x720" },
|
|
755
|
-
file: { pattern: /^[^-]/, named: "a file path" }
|
|
759
|
+
file: { pattern: /^[^-]/, named: "a file path" },
|
|
760
|
+
driver: {
|
|
761
|
+
pattern: /^(?:vulkan|d3d12|metal|opengl3|opengl3_es|opengl3_angle|dummy)$/,
|
|
762
|
+
named: "a rendering driver: vulkan, d3d12, metal, opengl3, opengl3_es, opengl3_angle or dummy"
|
|
763
|
+
},
|
|
764
|
+
method: {
|
|
765
|
+
pattern: /^(?:forward_plus|mobile|gl_compatibility)$/,
|
|
766
|
+
named: "a rendering method: forward_plus, mobile or gl_compatibility"
|
|
767
|
+
},
|
|
768
|
+
accessibility: {
|
|
769
|
+
pattern: /^(?:auto|always|disabled)$/,
|
|
770
|
+
named: "an accessibility mode: auto, always or disabled"
|
|
771
|
+
}
|
|
756
772
|
};
|
|
757
773
|
SET_BY_ARGUMENT = Object.freeze({
|
|
758
774
|
"--headless": "headless",
|
|
@@ -8220,13 +8236,13 @@ var init_alive = () => {};
|
|
|
8220
8236
|
|
|
8221
8237
|
// src/scratch.ts
|
|
8222
8238
|
import { existsSync as existsSync6, mkdtempSync, readdirSync as readdirSync3, rmSync as rmSync4 } from "node:fs";
|
|
8223
|
-
import { tmpdir } from "node:os";
|
|
8239
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
8224
8240
|
import { join as join6 } from "node:path";
|
|
8225
8241
|
import process6 from "node:process";
|
|
8226
8242
|
function scratchDirectory(kind) {
|
|
8227
|
-
return mkdtempSync(join6(
|
|
8243
|
+
return mkdtempSync(join6(tmpdir2(), `gdharness-${kind}-${process6.pid}-`));
|
|
8228
8244
|
}
|
|
8229
|
-
function sweepAbandonedScratch(directory =
|
|
8245
|
+
function sweepAbandonedScratch(directory = tmpdir2()) {
|
|
8230
8246
|
let entries;
|
|
8231
8247
|
try {
|
|
8232
8248
|
entries = readdirSync3(directory);
|
|
@@ -8336,14 +8352,20 @@ function snakeCased(params) {
|
|
|
8336
8352
|
return result;
|
|
8337
8353
|
}
|
|
8338
8354
|
function engineExtras(outcome) {
|
|
8355
|
+
const after = outcome.afterAnswerMessages ?? [];
|
|
8339
8356
|
return {
|
|
8340
8357
|
...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
|
|
8341
8358
|
...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
|
|
8359
|
+
...after.length > 0 ? { engine_messages_after_answer: after, engine_messages_after_answer_note: AFTER_ANSWER_NOTE } : {},
|
|
8360
|
+
...outcome.afterAnswerOmitted === undefined ? {} : { engine_messages_after_answer_omitted: outcome.afterAnswerOmitted },
|
|
8342
8361
|
...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
|
|
8343
8362
|
};
|
|
8344
8363
|
}
|
|
8345
8364
|
function problemsIn(log) {
|
|
8346
|
-
return log.everything()
|
|
8365
|
+
return problemsAmong(log.everything());
|
|
8366
|
+
}
|
|
8367
|
+
function problemsAmong(entries) {
|
|
8368
|
+
return entries.flatMap((entry) => {
|
|
8347
8369
|
if (entry.severity !== "info") {
|
|
8348
8370
|
return [entry];
|
|
8349
8371
|
}
|
|
@@ -8358,7 +8380,7 @@ function reported2(problems) {
|
|
|
8358
8380
|
return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
|
|
8359
8381
|
}
|
|
8360
8382
|
function reason(log, problems) {
|
|
8361
|
-
const own = log
|
|
8383
|
+
const own = refusalsIn(log);
|
|
8362
8384
|
if (own.length > 0) {
|
|
8363
8385
|
return own.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; ");
|
|
8364
8386
|
}
|
|
@@ -8370,6 +8392,9 @@ function reason(log, problems) {
|
|
|
8370
8392
|
const printed = log.everything().filter((entry) => entry.source === "stdout");
|
|
8371
8393
|
return printed.at(-1)?.text ?? "no output at all";
|
|
8372
8394
|
}
|
|
8395
|
+
function refusalsIn(log) {
|
|
8396
|
+
return log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
|
|
8397
|
+
}
|
|
8373
8398
|
function scriptFault(problems, script) {
|
|
8374
8399
|
const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
|
|
8375
8400
|
const directories = [...new Set([spelled(dirname4(script)), spelled(resolvedOr(dirname4(script)))])];
|
|
@@ -8472,15 +8497,32 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
8472
8497
|
} finally {
|
|
8473
8498
|
discard(paramsDir);
|
|
8474
8499
|
}
|
|
8475
|
-
const
|
|
8500
|
+
const everything = ran.log.everything();
|
|
8501
|
+
const answered = everything.findIndex((entry) => entry.source === "stderr" && entry.text === ANSWERED);
|
|
8502
|
+
const problems = problemsAmong(answered === -1 ? everything : everything.slice(0, answered));
|
|
8503
|
+
const after = answered === -1 ? [] : problemsAmong(everything.slice(answered + 1));
|
|
8504
|
+
const afterAnswered = {
|
|
8505
|
+
...after.length === 0 ? {} : { afterAnswerMessages: after.slice(-MESSAGES_KEPT) },
|
|
8506
|
+
...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {}
|
|
8507
|
+
};
|
|
8476
8508
|
if (answer.kind === "unreadable") {
|
|
8477
8509
|
return {
|
|
8478
8510
|
ok: false,
|
|
8479
8511
|
message: `${operation} wrote an answer that could not be read (${answer.why})`,
|
|
8480
|
-
...reported2(problems)
|
|
8512
|
+
...reported2(problems),
|
|
8513
|
+
...afterAnswered
|
|
8481
8514
|
};
|
|
8482
8515
|
}
|
|
8483
8516
|
if (answer.kind === "none") {
|
|
8517
|
+
const refusals = refusalsIn(ran.log);
|
|
8518
|
+
if (refusals.length > 0) {
|
|
8519
|
+
const said = new Set(refusals.map((entry) => entry.index));
|
|
8520
|
+
return {
|
|
8521
|
+
ok: false,
|
|
8522
|
+
message: refusals.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; "),
|
|
8523
|
+
...reported2(problems.filter((entry) => !said.has(entry.index)))
|
|
8524
|
+
};
|
|
8525
|
+
}
|
|
8484
8526
|
const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
|
|
8485
8527
|
return {
|
|
8486
8528
|
ok: false,
|
|
@@ -8493,14 +8535,16 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
8493
8535
|
return {
|
|
8494
8536
|
ok: false,
|
|
8495
8537
|
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"})`,
|
|
8496
|
-
...reported2(problems)
|
|
8538
|
+
...reported2(problems),
|
|
8539
|
+
...afterAnswered
|
|
8497
8540
|
};
|
|
8498
8541
|
}
|
|
8499
|
-
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.`;
|
|
8542
|
+
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"}.`;
|
|
8500
8543
|
return {
|
|
8501
8544
|
ok: true,
|
|
8502
8545
|
payload: answer.payload,
|
|
8503
8546
|
...reported2(problems),
|
|
8547
|
+
...afterAnswered,
|
|
8504
8548
|
...afterAnswer === undefined ? {} : { afterAnswer }
|
|
8505
8549
|
};
|
|
8506
8550
|
}
|
|
@@ -8519,7 +8563,7 @@ function answerIn(file) {
|
|
|
8519
8563
|
};
|
|
8520
8564
|
}
|
|
8521
8565
|
}
|
|
8522
|
-
var OWN_ERROR = "[ERROR] ", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
|
|
8566
|
+
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.", OWN_ERROR = "[ERROR] ", ANSWERED = "[gdharness] answered", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
|
|
8523
8567
|
var init_headless = __esm(() => {
|
|
8524
8568
|
init_engine_run();
|
|
8525
8569
|
init_reimport();
|
|
@@ -8528,7 +8572,7 @@ var init_headless = __esm(() => {
|
|
|
8528
8572
|
|
|
8529
8573
|
// src/update-check.ts
|
|
8530
8574
|
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8531
|
-
import { homedir as homedir3, tmpdir as
|
|
8575
|
+
import { homedir as homedir3, tmpdir as tmpdir3 } from "node:os";
|
|
8532
8576
|
import { join as join8 } from "node:path";
|
|
8533
8577
|
function packageUrl(root) {
|
|
8534
8578
|
return `${root.replace(/\/+$/, "")}/gdharness/latest`;
|
|
@@ -8564,7 +8608,7 @@ function cacheFile(environment = process.env) {
|
|
|
8564
8608
|
mkdirSync3(directory, { recursive: true, mode: 448 });
|
|
8565
8609
|
return join8(directory, "update-check.json");
|
|
8566
8610
|
} catch {
|
|
8567
|
-
return join8(
|
|
8611
|
+
return join8(tmpdir3(), "gdharness-update-check.json");
|
|
8568
8612
|
}
|
|
8569
8613
|
}
|
|
8570
8614
|
function readCache(path) {
|
|
@@ -11110,7 +11154,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11110
11154
|
{
|
|
11111
11155
|
name: "project_settings",
|
|
11112
11156
|
title: "Project settings",
|
|
11113
|
-
description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
|
|
11157
|
+
description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change touches only its own lines. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
|
|
11114
11158
|
parameters: {
|
|
11115
11159
|
projectPath: PROJECT_PATH,
|
|
11116
11160
|
setting: {
|
|
@@ -11295,7 +11339,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11295
11339
|
},
|
|
11296
11340
|
options: {
|
|
11297
11341
|
type: "object",
|
|
11298
|
-
description:
|
|
11342
|
+
description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads.`,
|
|
11299
11343
|
additionalProperties: true
|
|
11300
11344
|
},
|
|
11301
11345
|
reimport: {
|
|
@@ -11383,7 +11427,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11383
11427
|
engineArgs: {
|
|
11384
11428
|
type: "array",
|
|
11385
11429
|
items: { type: "string" },
|
|
11386
|
-
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
|
|
11430
|
+
description: `The engine's own flags for the run, such as ["--verbose"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU, which renderer draws, how it times frames or whether it builds the tree a screen reader reads: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused. With --verbose, engineEntries holds the ordinary lines the run printed as well, which is where the engine names the instances leaked and the resources still in use at exit.`
|
|
11387
11431
|
},
|
|
11388
11432
|
failFast: {
|
|
11389
11433
|
type: "boolean",
|
|
@@ -11810,7 +11854,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11810
11854
|
{
|
|
11811
11855
|
name: "editor_launch",
|
|
11812
11856
|
title: "Open, restart or close the editor",
|
|
11813
|
-
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.",
|
|
11857
|
+
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.",
|
|
11814
11858
|
parameters: {
|
|
11815
11859
|
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
11816
11860
|
hidden: {
|
|
@@ -11860,7 +11904,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11860
11904
|
type: "array",
|
|
11861
11905
|
items: { type: "string" },
|
|
11862
11906
|
ops: ["start", "check"],
|
|
11863
|
-
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
|
|
11907
|
+
description: `The engine's own flags, before the separator, such as ["--verbose"] or ["--fixed-fps", "60"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU, which renderer draws, how it times frames or whether it builds the tree a screen reader reads, and Movie Maker's --write-movie: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused, since the engine's command line can also run scripts, open other projects and write files. --rendering-driver takes vulkan, d3d12, metal, opengl3, opengl3_es, opengl3_angle or dummy, and --rendering-method forward_plus, mobile or gl_compatibility, so the same scene can be measured on another driver and a project's OpenGL fallback exercised on a machine that has Vulkan; they choose what draws a run with a window. --accessibility always has the game build the tree a screen reader reads, as the project's accessibility/general/accessibility_support at Always Active does, so runtime_inspect accessibility can read it; otherwise the engine builds it only when a screen reader is running. --write-movie takes an absolute path outside the project, in a directory that exists, ending in .avi (one file, video and sound) or .png (one numbered PNG per frame and a .wav beside them), where nothing it would write is there already; it needs a start with headless: false, since the engine crashes writing a movie headless, and it records at the project's editor/movie_writer/fps unless --fixed-fps says otherwise. A run with any is started by this server, as one with args is. A check given --verbose answers with everything printed rather than only errors and warnings, since what verbose adds, the instances leaked at exit among it, is printed as ordinary lines.`
|
|
11864
11908
|
},
|
|
11865
11909
|
headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
|
|
11866
11910
|
visible: {
|
|
@@ -11965,6 +12009,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
11965
12009
|
title: "Game and editor output",
|
|
11966
12010
|
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.",
|
|
11967
12011
|
parameters: {
|
|
12012
|
+
projectPath: {
|
|
12013
|
+
type: "string",
|
|
12014
|
+
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."
|
|
12015
|
+
},
|
|
11968
12016
|
severity: {
|
|
11969
12017
|
type: "string",
|
|
11970
12018
|
enum: ["error", "warning", "info"],
|
|
@@ -12016,7 +12064,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12016
12064
|
title: "Editor status",
|
|
12017
12065
|
effect: "read",
|
|
12018
12066
|
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.",
|
|
12019
|
-
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.",
|
|
12067
|
+
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.",
|
|
12020
12068
|
parameters: {},
|
|
12021
12069
|
requires: []
|
|
12022
12070
|
},
|
|
@@ -12096,7 +12144,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12096
12144
|
{
|
|
12097
12145
|
name: "runtime_inspect",
|
|
12098
12146
|
title: "Inspect the running game",
|
|
12099
|
-
description: "Questions about the running game: what is written on the screen, the scene tree, the nodes matching a query, where one node is on screen, what one property reads,
|
|
12147
|
+
description: "Questions about the running game: what is written on the screen, the scene tree, the nodes matching a query, where one node is on screen, what one property reads, the performance metrics, or what it tells a screen reader. Needs the game running with the runtime addon.",
|
|
12100
12148
|
parameters: {
|
|
12101
12149
|
projectPath: RUNNING_PROJECT_PATH,
|
|
12102
12150
|
pid: RUNNING_PID,
|
|
@@ -12165,7 +12213,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12165
12213
|
timeoutMs: {
|
|
12166
12214
|
type: "integer",
|
|
12167
12215
|
minimum: 1,
|
|
12168
|
-
description: "How long to wait for the answer before answering pending with a requestId, which runtime_invoke op result collects the reply by. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS."
|
|
12216
|
+
description: "How long to wait for the answer before answering pending with a requestId, which runtime_invoke op result collects the reply by. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS; accessibility defaults to 30000 or that, whichever is longer."
|
|
12169
12217
|
}
|
|
12170
12218
|
},
|
|
12171
12219
|
requires: [],
|
|
@@ -12193,7 +12241,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
12193
12241
|
},
|
|
12194
12242
|
metrics: {
|
|
12195
12243
|
effect: "read",
|
|
12196
|
-
summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked",
|
|
12244
|
+
summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked. memory_static is what the engine allocated on the CPU side, in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are what the renderer has on the GPU, in bytes, which memory_static does not count",
|
|
12245
|
+
requires: []
|
|
12246
|
+
},
|
|
12247
|
+
accessibility: {
|
|
12248
|
+
effect: "read",
|
|
12249
|
+
summary: `what the game tells a screen reader, read through UI Automation as a Windows screen reader reads it, under windows: one tree per window, each element with its role as UI Automation names it (text for static text, button, check_box, group for a control holding others), its name, its value where it has one, its bounds in the window's pixels, the actions a screen reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the one holding the focus. The name is what is read aloud, so an empty one is read as nothing; static text takes it from DisplayServer.accessibility_update_set_value, not from a name set on it, and the answer says under _note how many text elements on screen have none. With no control focused, the engine reports the first node under the root as focused, which in a run with the runtime is GdharnessRuntime. Needs a windowed run on Windows started with engineArgs ["--accessibility", "always"] or with the project's accessibility/general/accessibility_support at Always Active, since the engine builds the tree only when asked; reading it takes a second or two, starting PowerShell inside the game so it reaches the game's own desktop`,
|
|
12197
12250
|
requires: []
|
|
12198
12251
|
}
|
|
12199
12252
|
},
|
|
@@ -12219,7 +12272,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12219
12272
|
value: {
|
|
12220
12273
|
ops: ["set"],
|
|
12221
12274
|
blank: true,
|
|
12222
|
-
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.`
|
|
12275
|
+
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.`
|
|
12223
12276
|
},
|
|
12224
12277
|
method: {
|
|
12225
12278
|
type: "string",
|
|
@@ -12229,7 +12282,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12229
12282
|
args: {
|
|
12230
12283
|
type: "array",
|
|
12231
12284
|
ops: ["call"],
|
|
12232
|
-
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.`
|
|
12285
|
+
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.`
|
|
12233
12286
|
},
|
|
12234
12287
|
properties: {
|
|
12235
12288
|
type: ["boolean", "array"],
|
|
@@ -12323,7 +12376,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12323
12376
|
name: "runtime_input",
|
|
12324
12377
|
title: "Send input to the game",
|
|
12325
12378
|
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.",
|
|
12326
|
-
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.',
|
|
12379
|
+
answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again, for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
|
|
12327
12380
|
parameters: {
|
|
12328
12381
|
projectPath: RUNNING_PROJECT_PATH,
|
|
12329
12382
|
pid: RUNNING_PID,
|
|
@@ -12344,8 +12397,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
12344
12397
|
},
|
|
12345
12398
|
pressed: {
|
|
12346
12399
|
type: "boolean",
|
|
12347
|
-
ops: ["action", "key", "mouse_click"],
|
|
12348
|
-
description: "action, key, mouse_click: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. A wheel button held down is still a whole step, since a wheel is never held."
|
|
12400
|
+
ops: ["action", "key", "mouse_click", "joypad_button"],
|
|
12401
|
+
description: "action, key, mouse_click, joypad_button: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. A wheel button held down is still a whole step, since a wheel is never held."
|
|
12349
12402
|
},
|
|
12350
12403
|
strength: {
|
|
12351
12404
|
type: "number",
|
|
@@ -12390,8 +12443,26 @@ var init_tool_definitions = __esm(() => {
|
|
|
12390
12443
|
},
|
|
12391
12444
|
button: {
|
|
12392
12445
|
type: ["string", "number"],
|
|
12393
|
-
ops: ["click", "mouse_click"],
|
|
12394
|
-
description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left."
|
|
12446
|
+
ops: ["click", "mouse_click", "joypad_button"],
|
|
12447
|
+
description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left. joypad_button: the controller button, by name (a, b, x, y, back, guide, start, left_stick, right_stick, left_shoulder, right_shoulder, dpad_up, dpad_down, dpad_left, dpad_right, misc1, paddle1 to paddle4, touchpad) or by its JoyButton number; a, b, x and y are the bottom, right, left and top face buttons, as Godot names them, whatever the controller prints on them."
|
|
12448
|
+
},
|
|
12449
|
+
axis: {
|
|
12450
|
+
type: ["string", "number"],
|
|
12451
|
+
ops: ["joypad_motion"],
|
|
12452
|
+
description: "joypad_motion: the stick or trigger axis, by name (left_x, left_y, right_x, right_y, trigger_left, trigger_right) or by its JoyAxis number."
|
|
12453
|
+
},
|
|
12454
|
+
value: {
|
|
12455
|
+
type: "number",
|
|
12456
|
+
minimum: -1,
|
|
12457
|
+
maximum: 1,
|
|
12458
|
+
ops: ["joypad_motion"],
|
|
12459
|
+
description: "joypad_motion: where the axis is, -1 to 1: down and right are positive on a stick, and a trigger runs 0 to 1. The axis stays there until another motion moves it, as a stick held over does; send 0 to let it go back."
|
|
12460
|
+
},
|
|
12461
|
+
device: {
|
|
12462
|
+
type: "number",
|
|
12463
|
+
minimum: 0,
|
|
12464
|
+
ops: ["joypad_button", "joypad_motion"],
|
|
12465
|
+
description: "joypad_button, joypad_motion: which controller, counting from 0. Default 0."
|
|
12395
12466
|
},
|
|
12396
12467
|
doubleClick: {
|
|
12397
12468
|
type: "boolean",
|
|
@@ -12441,6 +12512,16 @@ var init_tool_definitions = __esm(() => {
|
|
|
12441
12512
|
effect: "additive",
|
|
12442
12513
|
summary: "move the mouse to a position, carrying the distance from where the last injected event put it unless relativeX and relativeY say otherwise, and the buttons held, so a control that drags moves under a run of these between a held mouse_click and its release",
|
|
12443
12514
|
requires: ["x", "y"]
|
|
12515
|
+
},
|
|
12516
|
+
joypad_button: {
|
|
12517
|
+
effect: "additive",
|
|
12518
|
+
summary: "press a controller button, or hold it, as an InputEventJoypadButton, so a game reading the pad in _input or _unhandled_input gets what a controller sends; an action bound to the button fires with it, as it would for a real one. Pressed and released a frame apart unless pressed says otherwise",
|
|
12519
|
+
requires: ["button"]
|
|
12520
|
+
},
|
|
12521
|
+
joypad_motion: {
|
|
12522
|
+
effect: "additive",
|
|
12523
|
+
summary: "move a stick or trigger axis to a value, as an InputEventJoypadMotion, which is what a game reading the sticks gets; the axis stays there until another motion moves it",
|
|
12524
|
+
requires: ["axis", "value"]
|
|
12444
12525
|
}
|
|
12445
12526
|
}
|
|
12446
12527
|
},
|
|
@@ -34333,7 +34414,7 @@ var init_stdio = __esm(() => {
|
|
|
34333
34414
|
// src/runtime-client.ts
|
|
34334
34415
|
import { existsSync as existsSync13, readdirSync as readdirSync8, readFileSync as readFileSync12, statSync as statSync6, unlinkSync } from "node:fs";
|
|
34335
34416
|
import { createConnection } from "node:net";
|
|
34336
|
-
import { tmpdir as
|
|
34417
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
34337
34418
|
import { dirname as dirname8, join as join14, resolve as resolve4 } from "node:path";
|
|
34338
34419
|
import { setTimeout as delay } from "node:timers/promises";
|
|
34339
34420
|
function runtimeDirectory(variables = process.env) {
|
|
@@ -34342,7 +34423,7 @@ function runtimeDirectory(variables = process.env) {
|
|
|
34342
34423
|
return explicit;
|
|
34343
34424
|
}
|
|
34344
34425
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
34345
|
-
return join14(perUser ??
|
|
34426
|
+
return join14(perUser ?? tmpdir4(), "gdharness");
|
|
34346
34427
|
}
|
|
34347
34428
|
function runtimeDirectories(variables = process.env) {
|
|
34348
34429
|
const candidates = [runtimeDirectory(variables)];
|
|
@@ -40126,11 +40207,58 @@ function askTheKeeperToStop(address, withinMs) {
|
|
|
40126
40207
|
}
|
|
40127
40208
|
var init_keeper_channel = () => {};
|
|
40128
40209
|
|
|
40210
|
+
// src/lifecycle-log.ts
|
|
40211
|
+
import { appendFileSync, mkdirSync as mkdirSync11, renameSync, statSync as statSync8 } from "node:fs";
|
|
40212
|
+
import { dirname as dirname14, join as join21 } from "node:path";
|
|
40213
|
+
import process10 from "node:process";
|
|
40214
|
+
function lifecycleLogPath() {
|
|
40215
|
+
return join21(runtimeDirectory(), "servers.log");
|
|
40216
|
+
}
|
|
40217
|
+
function writeLifecycle(event, fields = {}) {
|
|
40218
|
+
const path = lifecycleLogPath();
|
|
40219
|
+
try {
|
|
40220
|
+
mkdirSync11(dirname14(path), { recursive: true });
|
|
40221
|
+
if ((statSync8(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
|
|
40222
|
+
try {
|
|
40223
|
+
renameSync(path, `${path}.1`);
|
|
40224
|
+
} catch {}
|
|
40225
|
+
}
|
|
40226
|
+
const line = {
|
|
40227
|
+
at: new Date().toISOString(),
|
|
40228
|
+
pid: process10.pid,
|
|
40229
|
+
ppid: process10.ppid,
|
|
40230
|
+
version: SERVER_VERSION,
|
|
40231
|
+
event
|
|
40232
|
+
};
|
|
40233
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
40234
|
+
if (!Object.hasOwn(line, name)) {
|
|
40235
|
+
line[name] = value;
|
|
40236
|
+
}
|
|
40237
|
+
}
|
|
40238
|
+
appendFileSync(path, `${JSON.stringify(line)}
|
|
40239
|
+
`, "utf8");
|
|
40240
|
+
} catch {
|
|
40241
|
+
console.error(`[SERVER] Could not write the lifecycle log at ${path}.`);
|
|
40242
|
+
}
|
|
40243
|
+
}
|
|
40244
|
+
function callsInFlight(calls, now = Date.now()) {
|
|
40245
|
+
return [...calls].map((call) => ({
|
|
40246
|
+
tool: call.tool,
|
|
40247
|
+
...call.op === "" ? {} : { op: call.op },
|
|
40248
|
+
runningMs: now - call.startedAt
|
|
40249
|
+
}));
|
|
40250
|
+
}
|
|
40251
|
+
var LIFECYCLE_LOG_LIMIT = 1e6;
|
|
40252
|
+
var init_lifecycle_log = __esm(() => {
|
|
40253
|
+
init_runtime_client();
|
|
40254
|
+
init_server_version();
|
|
40255
|
+
});
|
|
40256
|
+
|
|
40129
40257
|
// src/lsp_client.ts
|
|
40130
40258
|
import { existsSync as existsSync17, realpathSync as realpathSync4 } from "node:fs";
|
|
40131
40259
|
import { readFile, realpath } from "node:fs/promises";
|
|
40132
40260
|
import { createConnection as createConnection3 } from "node:net";
|
|
40133
|
-
import { dirname as
|
|
40261
|
+
import { dirname as dirname15, join as join22, resolve as resolve5 } from "node:path";
|
|
40134
40262
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
40135
40263
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
40136
40264
|
function initializeTimeoutMs() {
|
|
@@ -40292,7 +40420,7 @@ class GodotLSPClient {
|
|
|
40292
40420
|
if (this.initialized) {
|
|
40293
40421
|
return;
|
|
40294
40422
|
}
|
|
40295
|
-
const rootPath = this.rootPath ??
|
|
40423
|
+
const rootPath = this.rootPath ?? dirname15(resolve5(filePath));
|
|
40296
40424
|
await this.initialize(rootPath);
|
|
40297
40425
|
}
|
|
40298
40426
|
async sendRequest(method, params, timeoutMs = this.timeouts.requestMs) {
|
|
@@ -40793,7 +40921,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
40793
40921
|
} catch {
|
|
40794
40922
|
throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
|
|
40795
40923
|
}
|
|
40796
|
-
if (!existsSync17(
|
|
40924
|
+
if (!existsSync17(join22(projectPath, "project.godot"))) {
|
|
40797
40925
|
throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
|
|
40798
40926
|
}
|
|
40799
40927
|
const contained = resolveWithinProject(projectPath, scriptPathValue);
|
|
@@ -41233,8 +41361,8 @@ var init_prompts = __esm(() => {
|
|
|
41233
41361
|
});
|
|
41234
41362
|
|
|
41235
41363
|
// src/rename.ts
|
|
41236
|
-
import { existsSync as existsSync18, mkdirSync as
|
|
41237
|
-
import { dirname as
|
|
41364
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync19, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync13 } from "node:fs";
|
|
41365
|
+
import { dirname as dirname16, posix as posix2 } from "node:path";
|
|
41238
41366
|
|
|
41239
41367
|
class Script {
|
|
41240
41368
|
path;
|
|
@@ -41946,15 +42074,15 @@ function applyRewrites(rewrites) {
|
|
|
41946
42074
|
const done = [];
|
|
41947
42075
|
try {
|
|
41948
42076
|
for (const rewrite of rewrites) {
|
|
41949
|
-
|
|
42077
|
+
mkdirSync12(dirname16(rewrite.writeTo), { recursive: true });
|
|
41950
42078
|
const staging = `${rewrite.writeTo}.gdharness-rename`;
|
|
41951
42079
|
writeFileSync13(staging, rewrite.after, "utf8");
|
|
41952
|
-
|
|
42080
|
+
renameSync2(staging, rewrite.writeTo);
|
|
41953
42081
|
let movedUid = false;
|
|
41954
42082
|
if (rewrite.writeTo !== rewrite.absolute) {
|
|
41955
42083
|
unlinkSync2(rewrite.absolute);
|
|
41956
42084
|
if (existsSync18(`${rewrite.absolute}.uid`)) {
|
|
41957
|
-
|
|
42085
|
+
renameSync2(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
|
|
41958
42086
|
movedUid = true;
|
|
41959
42087
|
}
|
|
41960
42088
|
}
|
|
@@ -41967,7 +42095,7 @@ function applyRewrites(rewrites) {
|
|
|
41967
42095
|
if (rewrite.writeTo !== rewrite.absolute) {
|
|
41968
42096
|
unlinkSync2(rewrite.writeTo);
|
|
41969
42097
|
if (movedUid) {
|
|
41970
|
-
|
|
42098
|
+
renameSync2(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
|
|
41971
42099
|
}
|
|
41972
42100
|
}
|
|
41973
42101
|
} catch {}
|
|
@@ -41999,8 +42127,8 @@ var init_rename = __esm(() => {
|
|
|
41999
42127
|
});
|
|
42000
42128
|
|
|
42001
42129
|
// src/rename-symbol.ts
|
|
42002
|
-
import { existsSync as existsSync19, mkdirSync as
|
|
42003
|
-
import { basename as basename5, dirname as
|
|
42130
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync13, readdirSync as readdirSync9, realpathSync as realpathSync5, rmSync as rmSync10, writeFileSync as writeFileSync14 } from "node:fs";
|
|
42131
|
+
import { basename as basename5, dirname as dirname17, join as join23, relative as relative4, resolve as resolve6 } from "node:path";
|
|
42004
42132
|
function resourcePathOf(projectPath, absolute) {
|
|
42005
42133
|
const inside = relative4(onDiskSpelling(projectPath), onDiskSpelling(absolute));
|
|
42006
42134
|
if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
|
|
@@ -42013,9 +42141,9 @@ function onDiskSpelling(path) {
|
|
|
42013
42141
|
let at = resolve6(path);
|
|
42014
42142
|
for (;; ) {
|
|
42015
42143
|
try {
|
|
42016
|
-
return
|
|
42144
|
+
return join23(realpathSync5.native(at), ...missing.reverse());
|
|
42017
42145
|
} catch {
|
|
42018
|
-
const parent =
|
|
42146
|
+
const parent = dirname17(at);
|
|
42019
42147
|
if (parent === at) {
|
|
42020
42148
|
return resolve6(path);
|
|
42021
42149
|
}
|
|
@@ -42441,15 +42569,15 @@ ${mention.kind}`;
|
|
|
42441
42569
|
};
|
|
42442
42570
|
}
|
|
42443
42571
|
function writeLeftAloneDetail(projectPath, symbol, detail) {
|
|
42444
|
-
const directory =
|
|
42445
|
-
|
|
42572
|
+
const directory = join23(projectPath, ".godot", "gdharness-renames");
|
|
42573
|
+
mkdirSync13(directory, { recursive: true });
|
|
42446
42574
|
detailWrites += 1;
|
|
42447
|
-
const path =
|
|
42575
|
+
const path = join23(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
|
|
42448
42576
|
writeFileSync14(path, `${JSON.stringify(detail, null, 2)}
|
|
42449
42577
|
`, "utf8");
|
|
42450
42578
|
const kept = readdirSync9(directory).filter((name) => name.endsWith(".json")).sort().reverse();
|
|
42451
42579
|
for (const old of kept.slice(10)) {
|
|
42452
|
-
rmSync10(
|
|
42580
|
+
rmSync10(join23(directory, old), { force: true });
|
|
42453
42581
|
}
|
|
42454
42582
|
return path;
|
|
42455
42583
|
}
|
|
@@ -42489,15 +42617,15 @@ var init_rename_symbol = __esm(() => {
|
|
|
42489
42617
|
});
|
|
42490
42618
|
|
|
42491
42619
|
// src/restart-note.ts
|
|
42492
|
-
import { mkdirSync as
|
|
42493
|
-
import { dirname as
|
|
42620
|
+
import { mkdirSync as mkdirSync14, readFileSync as readFileSync20, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
|
|
42621
|
+
import { dirname as dirname18, join as join24 } from "node:path";
|
|
42494
42622
|
function restartNotePath(projectPath) {
|
|
42495
|
-
return
|
|
42623
|
+
return join24(projectPath, ".godot", "gdharness-restart.json");
|
|
42496
42624
|
}
|
|
42497
42625
|
function noteRestartBegun(note) {
|
|
42498
42626
|
const path = restartNotePath(note.projectPath);
|
|
42499
42627
|
try {
|
|
42500
|
-
|
|
42628
|
+
mkdirSync14(dirname18(path), { recursive: true });
|
|
42501
42629
|
writeFileSync15(path, `${JSON.stringify(note, null, 2)}
|
|
42502
42630
|
`, "utf8");
|
|
42503
42631
|
} catch (error) {
|
|
@@ -42531,7 +42659,7 @@ var init_restart_note = __esm(() => {
|
|
|
42531
42659
|
});
|
|
42532
42660
|
|
|
42533
42661
|
// src/whole-file.ts
|
|
42534
|
-
import { renameSync as
|
|
42662
|
+
import { renameSync as renameSync3, rmSync as rmSync12, writeFileSync as writeFileSync16 } from "node:fs";
|
|
42535
42663
|
function writeWhole(path, text) {
|
|
42536
42664
|
const partial = `${path}.${process.pid}.partial`;
|
|
42537
42665
|
try {
|
|
@@ -42542,7 +42670,7 @@ function writeWhole(path, text) {
|
|
|
42542
42670
|
const until = Date.now() + HELD_FILE_MS;
|
|
42543
42671
|
for (;; ) {
|
|
42544
42672
|
try {
|
|
42545
|
-
|
|
42673
|
+
renameSync3(partial, path);
|
|
42546
42674
|
return true;
|
|
42547
42675
|
} catch (error) {
|
|
42548
42676
|
const code = error.code ?? "";
|
|
@@ -42564,20 +42692,20 @@ var init_whole_file = __esm(() => {
|
|
|
42564
42692
|
// src/run-record.ts
|
|
42565
42693
|
import { execFile as execFile4 } from "node:child_process";
|
|
42566
42694
|
import { createHash as createHash4 } from "node:crypto";
|
|
42567
|
-
import { mkdirSync as
|
|
42568
|
-
import { basename as basename6, dirname as
|
|
42695
|
+
import { mkdirSync as mkdirSync15, openSync as openSync2, readdirSync as readdirSync10, readFileSync as readFileSync21, realpathSync as realpathSync6, rmSync as rmSync13, statSync as statSync9 } from "node:fs";
|
|
42696
|
+
import { basename as basename6, dirname as dirname19, join as join25, resolve as resolve7 } from "node:path";
|
|
42569
42697
|
import { promisify as promisify4 } from "node:util";
|
|
42570
42698
|
function runsDirectory() {
|
|
42571
|
-
return
|
|
42699
|
+
return join25(runtimeDirectory(), "runs");
|
|
42572
42700
|
}
|
|
42573
42701
|
function realAsFarAsItExists(path) {
|
|
42574
42702
|
const missing = [];
|
|
42575
42703
|
let at = path;
|
|
42576
42704
|
for (;; ) {
|
|
42577
42705
|
try {
|
|
42578
|
-
return
|
|
42706
|
+
return join25(realpathSync6.native(at), ...missing);
|
|
42579
42707
|
} catch {
|
|
42580
|
-
const parent =
|
|
42708
|
+
const parent = dirname19(at);
|
|
42581
42709
|
if (parent === at) {
|
|
42582
42710
|
return path;
|
|
42583
42711
|
}
|
|
@@ -42592,14 +42720,14 @@ function projectKey(projectPath) {
|
|
|
42592
42720
|
return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
42593
42721
|
}
|
|
42594
42722
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
42595
|
-
return
|
|
42723
|
+
return join25(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
42596
42724
|
}
|
|
42597
42725
|
function latestRunPath(servedBy, directory = runtimeDirectory()) {
|
|
42598
|
-
return
|
|
42726
|
+
return join25(directory, "runs", `latest-${projectKey(servedBy)}.json`);
|
|
42599
42727
|
}
|
|
42600
42728
|
function writeLatestRun(servedBy, latest) {
|
|
42601
42729
|
try {
|
|
42602
|
-
|
|
42730
|
+
mkdirSync15(runsDirectory(), { recursive: true });
|
|
42603
42731
|
writeWhole(latestRunPath(servedBy), JSON.stringify(latest, null, 2));
|
|
42604
42732
|
} catch {}
|
|
42605
42733
|
}
|
|
@@ -42619,21 +42747,21 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
|
|
|
42619
42747
|
}
|
|
42620
42748
|
}
|
|
42621
42749
|
function openTranscript(startedAt) {
|
|
42622
|
-
|
|
42623
|
-
const path =
|
|
42750
|
+
mkdirSync15(runsDirectory(), { recursive: true });
|
|
42751
|
+
const path = join25(runsDirectory(), `run-${startedAt}.log`);
|
|
42624
42752
|
return { path, fd: openSync2(path, "a") };
|
|
42625
42753
|
}
|
|
42626
42754
|
function profilePath(startedAt) {
|
|
42627
|
-
return
|
|
42755
|
+
return join25(runsDirectory(), `run-${startedAt}.profile.json`);
|
|
42628
42756
|
}
|
|
42629
42757
|
function noteIsAbout(note, playing) {
|
|
42630
42758
|
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
42631
42759
|
}
|
|
42632
42760
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
42633
|
-
return
|
|
42761
|
+
return join25(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
42634
42762
|
}
|
|
42635
42763
|
function writeEditorRunNote(note) {
|
|
42636
|
-
|
|
42764
|
+
mkdirSync15(runsDirectory(), { recursive: true });
|
|
42637
42765
|
const path = editorNotePath(note.projectPath);
|
|
42638
42766
|
if (!writeWhole(path, JSON.stringify(note, null, 2))) {
|
|
42639
42767
|
throw new Error(`the editor run note could not be written at ${path}`);
|
|
@@ -42641,7 +42769,7 @@ function writeEditorRunNote(note) {
|
|
|
42641
42769
|
}
|
|
42642
42770
|
function readEditorRunNote(projectPath) {
|
|
42643
42771
|
for (const directory of runtimeDirectories()) {
|
|
42644
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
42772
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join25(directory, "runs", "editor-run.json"));
|
|
42645
42773
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
42646
42774
|
return note;
|
|
42647
42775
|
}
|
|
@@ -42675,7 +42803,7 @@ function readRunRecord(projectPath) {
|
|
|
42675
42803
|
if (own !== null) {
|
|
42676
42804
|
return own;
|
|
42677
42805
|
}
|
|
42678
|
-
const shared = recordAt(
|
|
42806
|
+
const shared = recordAt(join25(directory, "runs", SHARED_NOTE));
|
|
42679
42807
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
42680
42808
|
return shared;
|
|
42681
42809
|
}
|
|
@@ -42687,13 +42815,13 @@ function everyRunRecord() {
|
|
|
42687
42815
|
for (const directory of runtimeDirectories()) {
|
|
42688
42816
|
let entries;
|
|
42689
42817
|
try {
|
|
42690
|
-
entries = readdirSync10(
|
|
42818
|
+
entries = readdirSync10(join25(directory, "runs"));
|
|
42691
42819
|
} catch {
|
|
42692
42820
|
continue;
|
|
42693
42821
|
}
|
|
42694
42822
|
for (const entry of entries) {
|
|
42695
42823
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
42696
|
-
const record = recordAt(
|
|
42824
|
+
const record = recordAt(join25(directory, "runs", entry));
|
|
42697
42825
|
if (record !== null) {
|
|
42698
42826
|
found.push(record);
|
|
42699
42827
|
}
|
|
@@ -42903,7 +43031,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
42903
43031
|
}
|
|
42904
43032
|
function clearRunRecord(projectPath) {
|
|
42905
43033
|
for (const directory of runtimeDirectories()) {
|
|
42906
|
-
const shared =
|
|
43034
|
+
const shared = join25(directory, "runs", SHARED_NOTE);
|
|
42907
43035
|
const sharedRecord = recordAt(shared);
|
|
42908
43036
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
42909
43037
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
@@ -42915,7 +43043,7 @@ function clearRunRecord(projectPath) {
|
|
|
42915
43043
|
}
|
|
42916
43044
|
function ageMs(path, now) {
|
|
42917
43045
|
try {
|
|
42918
|
-
return now -
|
|
43046
|
+
return now - statSync9(path).mtimeMs;
|
|
42919
43047
|
} catch {
|
|
42920
43048
|
return 0;
|
|
42921
43049
|
}
|
|
@@ -42937,7 +43065,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
42937
43065
|
if (!transcript && !entry.endsWith(".partial")) {
|
|
42938
43066
|
continue;
|
|
42939
43067
|
}
|
|
42940
|
-
const path =
|
|
43068
|
+
const path = join25(directory, entry);
|
|
42941
43069
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
42942
43070
|
continue;
|
|
42943
43071
|
}
|
|
@@ -42958,7 +43086,7 @@ var init_run_record = __esm(() => {
|
|
|
42958
43086
|
});
|
|
42959
43087
|
|
|
42960
43088
|
// src/script-origins.ts
|
|
42961
|
-
import { join as
|
|
43089
|
+
import { join as join26 } from "node:path";
|
|
42962
43090
|
function unresolvedClassIn(message) {
|
|
42963
43091
|
return UNRESOLVED.exec(message)?.[1] ?? null;
|
|
42964
43092
|
}
|
|
@@ -43011,7 +43139,7 @@ function checkWithEngine(godotPath, projectPath) {
|
|
|
43011
43139
|
const ran = await runEngine(godotPath, [
|
|
43012
43140
|
"--headless",
|
|
43013
43141
|
"--log-file",
|
|
43014
|
-
|
|
43142
|
+
join26(logDir, "engine.log"),
|
|
43015
43143
|
"--path",
|
|
43016
43144
|
projectPath,
|
|
43017
43145
|
"--check-only",
|
|
@@ -43240,21 +43368,21 @@ var init_server_instructions = __esm(() => {
|
|
|
43240
43368
|
import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
|
|
43241
43369
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
43242
43370
|
import {
|
|
43243
|
-
appendFileSync,
|
|
43371
|
+
appendFileSync as appendFileSync2,
|
|
43244
43372
|
closeSync as closeSync2,
|
|
43245
43373
|
constants,
|
|
43246
43374
|
copyFileSync,
|
|
43247
43375
|
existsSync as existsSync20,
|
|
43248
43376
|
fstatSync,
|
|
43249
|
-
mkdirSync as
|
|
43377
|
+
mkdirSync as mkdirSync16,
|
|
43250
43378
|
openSync as openSync3,
|
|
43251
43379
|
readdirSync as readdirSync11,
|
|
43252
43380
|
readFileSync as readFileSync23,
|
|
43253
43381
|
readSync as readSync2,
|
|
43254
43382
|
rmdirSync as rmdirSync2,
|
|
43255
|
-
statSync as
|
|
43383
|
+
statSync as statSync10
|
|
43256
43384
|
} from "node:fs";
|
|
43257
|
-
import { basename as basename7, dirname as
|
|
43385
|
+
import { basename as basename7, dirname as dirname20, extname as extname3, isAbsolute as isAbsolute3, join as join27, normalize as normalize2, relative as relative5 } from "node:path";
|
|
43258
43386
|
import { setTimeout as delay4 } from "node:timers/promises";
|
|
43259
43387
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
43260
43388
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -43291,11 +43419,11 @@ function writtenFileRefusal(path, rule) {
|
|
|
43291
43419
|
if (!rule.extensions.includes(extname3(path).toLowerCase())) {
|
|
43292
43420
|
return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
|
|
43293
43421
|
}
|
|
43294
|
-
const directory = realPathOr(
|
|
43422
|
+
const directory = realPathOr(dirname20(path));
|
|
43295
43423
|
if (!existsSync20(directory)) {
|
|
43296
|
-
return `${named}'s directory ${
|
|
43424
|
+
return `${named}'s directory ${dirname20(path)} does not exist; ${what} creates no directories.`;
|
|
43297
43425
|
}
|
|
43298
|
-
const where =
|
|
43426
|
+
const where = join27(directory, basename7(path));
|
|
43299
43427
|
const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
43300
43428
|
if (inside !== undefined) {
|
|
43301
43429
|
return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
|
|
@@ -43816,6 +43944,23 @@ function engineFlagsNote(args) {
|
|
|
43816
43944
|
const named = among.join(", ");
|
|
43817
43945
|
return `${named} ${among.length === 1 ? "is a flag" : "are flags"} the engine reads, and args goes to the game after --, where the engine does not read ${among.length === 1 ? "it" : "them"}. engineArgs hands the engine its own flags; ${among.length === 1 ? "leave it" : "leave them"} in args only for a game that reads ${among.length === 1 ? "that flag" : "those flags"} itself.`;
|
|
43818
43946
|
}
|
|
43947
|
+
function editorAddonMissing(projectPath) {
|
|
43948
|
+
if (!existsSync20(join27(projectPath, "project.godot"))) {
|
|
43949
|
+
return null;
|
|
43950
|
+
}
|
|
43951
|
+
const report = inspectProject(projectPath);
|
|
43952
|
+
const installed = report.addons.some((addon) => addon.name === "gdharness_editor" && addon.installed);
|
|
43953
|
+
if (installed && report.pluginsEnabled.includes("gdharness_editor")) {
|
|
43954
|
+
return null;
|
|
43955
|
+
}
|
|
43956
|
+
return {
|
|
43957
|
+
why: `the gdharness editor addon is not ${installed ? "enabled" : "installed"} in ${projectPath}`,
|
|
43958
|
+
remedies: installed ? [
|
|
43959
|
+
"gdharness setup in the project enables it",
|
|
43960
|
+
"Or enable it under Project Settings, Plugins, in an editor opened by hand"
|
|
43961
|
+
] : ["gdharness setup in the project installs and enables it"]
|
|
43962
|
+
};
|
|
43963
|
+
}
|
|
43819
43964
|
function opElsewhere(tool, requested) {
|
|
43820
43965
|
const ops = Object.hasOwn(OPS_ELSEWHERE, tool) ? OPS_ELSEWHERE[tool] : undefined;
|
|
43821
43966
|
const where = ops !== undefined && typeof requested === "string" && Object.hasOwn(ops, requested) ? ops[requested] : undefined;
|
|
@@ -44036,7 +44181,7 @@ function profileFrameFunctions(projectPath) {
|
|
|
44036
44181
|
let projectGodot = null;
|
|
44037
44182
|
if (projectPath !== null) {
|
|
44038
44183
|
try {
|
|
44039
|
-
projectGodot = readFileSync23(
|
|
44184
|
+
projectGodot = readFileSync23(join27(projectPath, "project.godot"), "utf8");
|
|
44040
44185
|
} catch {}
|
|
44041
44186
|
}
|
|
44042
44187
|
return frameFunctionsFor(queuedMessagesOf(projectGodot));
|
|
@@ -44210,7 +44355,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
|
|
|
44210
44355
|
}
|
|
44211
44356
|
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
44212
44357
|
try {
|
|
44213
|
-
return settingKeys(readFileSync23(
|
|
44358
|
+
return settingKeys(readFileSync23(join27(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
44214
44359
|
} catch {
|
|
44215
44360
|
return true;
|
|
44216
44361
|
}
|
|
@@ -44225,7 +44370,7 @@ function uncaughtNote(uncaught) {
|
|
|
44225
44370
|
}
|
|
44226
44371
|
function elsewhereIn(projectPath, asked) {
|
|
44227
44372
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
44228
|
-
if (wanted === "" || existsSync20(
|
|
44373
|
+
if (wanted === "" || existsSync20(join27(projectPath, wanted))) {
|
|
44229
44374
|
return "";
|
|
44230
44375
|
}
|
|
44231
44376
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -44315,13 +44460,13 @@ function scriptsToDiagnose(projectPath, given) {
|
|
|
44315
44460
|
}
|
|
44316
44461
|
function isDirectory(path) {
|
|
44317
44462
|
try {
|
|
44318
|
-
return
|
|
44463
|
+
return statSync10(path).isDirectory();
|
|
44319
44464
|
} catch {
|
|
44320
44465
|
return false;
|
|
44321
44466
|
}
|
|
44322
44467
|
}
|
|
44323
44468
|
function scriptsUnder(directory, prefix) {
|
|
44324
|
-
if (existsSync20(
|
|
44469
|
+
if (existsSync20(join27(directory, ".gdignore"))) {
|
|
44325
44470
|
return [];
|
|
44326
44471
|
}
|
|
44327
44472
|
const found = [];
|
|
@@ -44331,7 +44476,7 @@ function scriptsUnder(directory, prefix) {
|
|
|
44331
44476
|
}
|
|
44332
44477
|
const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
44333
44478
|
if (entry.isDirectory()) {
|
|
44334
|
-
found.push(...scriptsUnder(
|
|
44479
|
+
found.push(...scriptsUnder(join27(directory, entry.name), path));
|
|
44335
44480
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
44336
44481
|
found.push(path);
|
|
44337
44482
|
}
|
|
@@ -44347,7 +44492,7 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
44347
44492
|
return false;
|
|
44348
44493
|
}
|
|
44349
44494
|
try {
|
|
44350
|
-
return readFileSync23(
|
|
44495
|
+
return readFileSync23(join27(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
44351
44496
|
} catch {
|
|
44352
44497
|
return false;
|
|
44353
44498
|
}
|
|
@@ -44372,7 +44517,7 @@ function needsItsOwnProcess(asked) {
|
|
|
44372
44517
|
class GodotServer {
|
|
44373
44518
|
stdio = null;
|
|
44374
44519
|
locator = new GodotLocator;
|
|
44375
|
-
operationsScript =
|
|
44520
|
+
operationsScript = join27(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
44376
44521
|
godotBridge;
|
|
44377
44522
|
tools = buildToolDefinitions();
|
|
44378
44523
|
activeProcess = null;
|
|
@@ -44396,6 +44541,11 @@ class GodotServer {
|
|
|
44396
44541
|
launchedFrom = null;
|
|
44397
44542
|
launchedDropped = null;
|
|
44398
44543
|
closedHere = null;
|
|
44544
|
+
inFlight = new Map;
|
|
44545
|
+
callsTaken = 0;
|
|
44546
|
+
clientNoted = null;
|
|
44547
|
+
parentCommand = null;
|
|
44548
|
+
endNoted = false;
|
|
44399
44549
|
ownProject;
|
|
44400
44550
|
announcedAt = null;
|
|
44401
44551
|
handingOverFrom = null;
|
|
@@ -44432,6 +44582,7 @@ class GodotServer {
|
|
|
44432
44582
|
return { content: [{ type: "text", text: answerJson(payload) }] };
|
|
44433
44583
|
}
|
|
44434
44584
|
async run() {
|
|
44585
|
+
this.noteTheStart();
|
|
44435
44586
|
const located = await this.locator.find();
|
|
44436
44587
|
if (!located.ok) {
|
|
44437
44588
|
console.error(`[SERVER] ${GodotLocator.refusal(located).message} Until that is fixed every tool that runs the engine will say so.`);
|
|
@@ -44548,6 +44699,7 @@ class GodotServer {
|
|
|
44548
44699
|
return;
|
|
44549
44700
|
}
|
|
44550
44701
|
console.error(`[SERVER] pid ${successor} now serves ${this.ownProject}; this server is finished and is stopping.`);
|
|
44702
|
+
this.noteTheEnd("superseded", { successor });
|
|
44551
44703
|
this.handleShutdown(`superseded by ${successor}`, 0);
|
|
44552
44704
|
}, SUCCESSOR_CHECK_MS);
|
|
44553
44705
|
watch.unref();
|
|
@@ -44619,10 +44771,67 @@ class GodotServer {
|
|
|
44619
44771
|
}
|
|
44620
44772
|
await this.stdio?.close();
|
|
44621
44773
|
}
|
|
44774
|
+
noteTheStart() {
|
|
44775
|
+
const cwd = process.cwd();
|
|
44776
|
+
const mcpJson = join27(cwd, ".mcp.json");
|
|
44777
|
+
const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
|
|
44778
|
+
const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
|
|
44779
|
+
writeLifecycle("started", {
|
|
44780
|
+
argv: process.argv.slice(1),
|
|
44781
|
+
cwd,
|
|
44782
|
+
project: this.ownProject,
|
|
44783
|
+
mcpJson: existsSync20(mcpJson) ? mcpJson : null
|
|
44784
|
+
});
|
|
44785
|
+
if (predecessor !== null) {
|
|
44786
|
+
writeLifecycle("replaced", { project: this.ownProject, predecessor });
|
|
44787
|
+
}
|
|
44788
|
+
const ask = setTimeout(() => {
|
|
44789
|
+
processTree().then((tree) => {
|
|
44790
|
+
this.parentCommand = tree?.get(process.ppid)?.command ?? null;
|
|
44791
|
+
writeLifecycle("parent", {
|
|
44792
|
+
command: this.parentCommand,
|
|
44793
|
+
...tree === undefined ? { why: whyTheProcessTreeFailed() } : {}
|
|
44794
|
+
});
|
|
44795
|
+
});
|
|
44796
|
+
}, PARENT_ASKED_AFTER_MS);
|
|
44797
|
+
ask.unref();
|
|
44798
|
+
}
|
|
44799
|
+
noteTheEnd(cause, more = {}) {
|
|
44800
|
+
if (this.endNoted) {
|
|
44801
|
+
return;
|
|
44802
|
+
}
|
|
44803
|
+
this.endNoted = true;
|
|
44804
|
+
writeLifecycle("ended", {
|
|
44805
|
+
cause,
|
|
44806
|
+
project: this.ownProject,
|
|
44807
|
+
...this.parentCommand === null ? {} : { parentCommand: this.parentCommand },
|
|
44808
|
+
...this.clientNoted === null ? {} : { client: this.clientNoted },
|
|
44809
|
+
inFlight: callsInFlight(this.inFlight.values()),
|
|
44810
|
+
...more
|
|
44811
|
+
});
|
|
44812
|
+
}
|
|
44813
|
+
noteTheClient(client) {
|
|
44814
|
+
if (client === undefined) {
|
|
44815
|
+
return;
|
|
44816
|
+
}
|
|
44817
|
+
const said = `${client.name} ${client.version}`;
|
|
44818
|
+
if (said === this.clientNoted) {
|
|
44819
|
+
return;
|
|
44820
|
+
}
|
|
44821
|
+
this.clientNoted = said;
|
|
44822
|
+
writeLifecycle("client", { client: { name: client.name, version: client.version } });
|
|
44823
|
+
}
|
|
44622
44824
|
setupShutdownHandlers() {
|
|
44623
44825
|
const requestShutdown = (source, exitCode) => {
|
|
44624
44826
|
this.handleShutdown(source, exitCode);
|
|
44625
44827
|
};
|
|
44828
|
+
process.on("uncaughtException", (error, origin) => {
|
|
44829
|
+
this.noteTheEnd(origin === "unhandledRejection" ? "unhandledRejection" : "uncaughtException", {
|
|
44830
|
+
error: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
44831
|
+
});
|
|
44832
|
+
console.error(error);
|
|
44833
|
+
process.exit(1);
|
|
44834
|
+
});
|
|
44626
44835
|
process.once("SIGINT", () => {
|
|
44627
44836
|
requestShutdown("SIGINT", 0);
|
|
44628
44837
|
});
|
|
@@ -44635,7 +44844,8 @@ class GodotServer {
|
|
|
44635
44844
|
process.once("beforeExit", (code) => {
|
|
44636
44845
|
requestShutdown(`beforeExit:${code}`);
|
|
44637
44846
|
});
|
|
44638
|
-
process.once("exit", () => {
|
|
44847
|
+
process.once("exit", (code) => {
|
|
44848
|
+
this.noteTheEnd(`exit:${code}`);
|
|
44639
44849
|
this.forceCleanupOnExit();
|
|
44640
44850
|
});
|
|
44641
44851
|
}
|
|
@@ -44644,6 +44854,7 @@ class GodotServer {
|
|
|
44644
44854
|
return;
|
|
44645
44855
|
}
|
|
44646
44856
|
this.shutdownInitiated = true;
|
|
44857
|
+
this.noteTheEnd(source);
|
|
44647
44858
|
this.logDebug(`Shutting down server via ${source}`);
|
|
44648
44859
|
try {
|
|
44649
44860
|
await this.cleanup();
|
|
@@ -44683,9 +44894,23 @@ class GodotServer {
|
|
|
44683
44894
|
return mcp;
|
|
44684
44895
|
}
|
|
44685
44896
|
setupToolHandlers(protocol) {
|
|
44686
|
-
|
|
44897
|
+
const clientOf = (envelope) => {
|
|
44898
|
+
const named = envelope?.["io.modelcontextprotocol/clientInfo"];
|
|
44899
|
+
if (typeof named === "object" && named !== null) {
|
|
44900
|
+
const { name, version } = named;
|
|
44901
|
+
if (typeof name === "string" && typeof version === "string") {
|
|
44902
|
+
return { name, version };
|
|
44903
|
+
}
|
|
44904
|
+
}
|
|
44905
|
+
return protocol.getClientVersion();
|
|
44906
|
+
};
|
|
44907
|
+
protocol.setRequestHandler("tools/list", (_request, ctx) => {
|
|
44908
|
+
this.noteTheClient(clientOf(ctx.mcpReq.envelope));
|
|
44909
|
+
return { tools: this.tools };
|
|
44910
|
+
});
|
|
44687
44911
|
protocol.setRequestHandler("tools/call", async (request, ctx) => {
|
|
44688
44912
|
this.logDebug(`Handling tool request: ${request.params.name}`);
|
|
44913
|
+
this.noteTheClient(clientOf(ctx.mcpReq.envelope));
|
|
44689
44914
|
const spec = toolSpec(request.params.name);
|
|
44690
44915
|
if (!spec) {
|
|
44691
44916
|
throw new ProtocolError(ProtocolErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}. Tools: ${TOOL_SPECS.map((tool) => tool.name).join(", ")}`);
|
|
@@ -44696,7 +44921,7 @@ class GodotServer {
|
|
|
44696
44921
|
return checked.response;
|
|
44697
44922
|
}
|
|
44698
44923
|
const named = args["projectPath"];
|
|
44699
|
-
if (typeof named === "string" && existsSync20(
|
|
44924
|
+
if (typeof named === "string" && existsSync20(join27(named, "project.godot"))) {
|
|
44700
44925
|
this.lastProjectPath = named;
|
|
44701
44926
|
}
|
|
44702
44927
|
this.updates.refresh();
|
|
@@ -44709,8 +44934,14 @@ class GodotServer {
|
|
|
44709
44934
|
this.logDebug(`Progress for ${spec.name} was not sent: ${errorMessage(error)}`);
|
|
44710
44935
|
});
|
|
44711
44936
|
};
|
|
44712
|
-
const
|
|
44713
|
-
|
|
44937
|
+
const call = this.callsTaken++;
|
|
44938
|
+
this.inFlight.set(call, { tool: spec.name, op: checked.op ?? "", startedAt: Date.now() });
|
|
44939
|
+
try {
|
|
44940
|
+
const answer = await withCallSignal(ctx.mcpReq.signal, () => this.answered(spec.name, checked.op ?? "", args), progress);
|
|
44941
|
+
return this.withFeedbackNotice(this.withUpdateNotice(answer));
|
|
44942
|
+
} finally {
|
|
44943
|
+
this.inFlight.delete(call);
|
|
44944
|
+
}
|
|
44714
44945
|
});
|
|
44715
44946
|
}
|
|
44716
44947
|
async answered(tool, op, args) {
|
|
@@ -44929,10 +45160,12 @@ class GodotServer {
|
|
|
44929
45160
|
]);
|
|
44930
45161
|
}
|
|
44931
45162
|
if (!this.godotBridge.isConnected()) {
|
|
44932
|
-
|
|
44933
|
-
|
|
44934
|
-
'Open the project with editor_launch, or pass from: "disk" to read the file instead'
|
|
45163
|
+
const refused = await this.noEditorYet(answerable, [
|
|
45164
|
+
`${tool} ${op} was asked of the editor; from: "disk" reads the file instead`
|
|
44935
45165
|
]);
|
|
45166
|
+
if (refused !== null) {
|
|
45167
|
+
return refused;
|
|
45168
|
+
}
|
|
44936
45169
|
}
|
|
44937
45170
|
const { op: _dropped, ...asks } = answerable;
|
|
44938
45171
|
const contained = this.containProjectFiles(asks);
|
|
@@ -45017,11 +45250,11 @@ class GodotServer {
|
|
|
45017
45250
|
case "script_info":
|
|
45018
45251
|
switch (op) {
|
|
45019
45252
|
case "symbols":
|
|
45020
|
-
return await this.
|
|
45253
|
+
return await this.askScriptInfo("lsp_get_symbols", "Symbols", args);
|
|
45021
45254
|
case "completion":
|
|
45022
|
-
return await this.
|
|
45255
|
+
return await this.askScriptInfo("lsp_get_completions", "Completion", args);
|
|
45023
45256
|
default:
|
|
45024
|
-
return await this.
|
|
45257
|
+
return await this.askScriptInfo("lsp_get_hover", "Hover", args);
|
|
45025
45258
|
}
|
|
45026
45259
|
case "script_diagnostics":
|
|
45027
45260
|
return await this.handleScriptDiagnostics(args);
|
|
@@ -45094,6 +45327,12 @@ class GodotServer {
|
|
|
45094
45327
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
45095
45328
|
property: readNonEmptyString(args, "property") ?? ""
|
|
45096
45329
|
}, { tool: "runtime_inspect", op, patience });
|
|
45330
|
+
case "accessibility":
|
|
45331
|
+
return await this.handleRuntimeCommand("get_accessibility", whichGame(args), {
|
|
45332
|
+
tool: "runtime_inspect",
|
|
45333
|
+
op,
|
|
45334
|
+
patience: readPositiveNumber(args, "timeoutMs") ?? Math.max(30000, this.runtimeTimeoutMs())
|
|
45335
|
+
});
|
|
45097
45336
|
default:
|
|
45098
45337
|
return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, { tool: "runtime_inspect", op, patience });
|
|
45099
45338
|
}
|
|
@@ -45198,7 +45437,7 @@ class GodotServer {
|
|
|
45198
45437
|
if (path === undefined) {
|
|
45199
45438
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
45200
45439
|
}
|
|
45201
|
-
const file =
|
|
45440
|
+
const file = join27(path, "project.godot");
|
|
45202
45441
|
if (!existsSync20(file)) {
|
|
45203
45442
|
return {
|
|
45204
45443
|
ok: false,
|
|
@@ -45885,18 +46124,18 @@ class GodotServer {
|
|
|
45885
46124
|
if (!engine.ok) {
|
|
45886
46125
|
return engine.response;
|
|
45887
46126
|
}
|
|
45888
|
-
const folder =
|
|
46127
|
+
const folder = dirname20(output.absolutePath);
|
|
45889
46128
|
try {
|
|
45890
|
-
|
|
46129
|
+
mkdirSync16(folder, { recursive: true });
|
|
45891
46130
|
} catch (error) {
|
|
45892
|
-
return this.createErrorResponse(`Cannot create ${
|
|
46131
|
+
return this.createErrorResponse(`Cannot create ${dirname20(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
45893
46132
|
}
|
|
45894
46133
|
const debug = readBoolean(args, "debug") ?? false;
|
|
45895
46134
|
const exportLogs = scratchDirectory("export");
|
|
45896
46135
|
const exportArgs = [
|
|
45897
46136
|
"--headless",
|
|
45898
46137
|
"--log-file",
|
|
45899
|
-
|
|
46138
|
+
join27(exportLogs, "engine.log"),
|
|
45900
46139
|
"--path",
|
|
45901
46140
|
project.value.path,
|
|
45902
46141
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -45904,7 +46143,7 @@ class GodotServer {
|
|
|
45904
46143
|
output.absolutePath
|
|
45905
46144
|
];
|
|
45906
46145
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
45907
|
-
const writtenAt = () => existsSync20(output.absolutePath) ?
|
|
46146
|
+
const writtenAt = () => existsSync20(output.absolutePath) ? statSync10(output.absolutePath).mtimeMs : null;
|
|
45908
46147
|
const before = writtenAt();
|
|
45909
46148
|
let ending;
|
|
45910
46149
|
try {
|
|
@@ -45968,10 +46207,10 @@ class GodotServer {
|
|
|
45968
46207
|
if (movie !== null) {
|
|
45969
46208
|
return this.createErrorResponse(movie);
|
|
45970
46209
|
}
|
|
45971
|
-
const missing = paths.value.filter((path) => !existsSync20(
|
|
46210
|
+
const missing = paths.value.filter((path) => !existsSync20(join27(project.value.path, path.slice("res://".length))));
|
|
45972
46211
|
const asked = paths.value.filter((path) => !missing.includes(path));
|
|
45973
46212
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
45974
|
-
const noRunner = !existsSync20(
|
|
46213
|
+
const noRunner = !existsSync20(join27(project.value.path, runner));
|
|
45975
46214
|
if (asked.length === 0) {
|
|
45976
46215
|
const verdict = `nothing at ${missing.join(" and ")}`;
|
|
45977
46216
|
const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
|
|
@@ -46062,21 +46301,21 @@ class GodotServer {
|
|
|
46062
46301
|
await untilGone([run.process.pid]);
|
|
46063
46302
|
}
|
|
46064
46303
|
await run.settled;
|
|
46065
|
-
const reportsDir =
|
|
46304
|
+
const reportsDir = join27(project.value.path, ".godot", ...ours.split("/"));
|
|
46066
46305
|
let report = null;
|
|
46067
46306
|
let reportProblem = null;
|
|
46068
46307
|
try {
|
|
46069
46308
|
const written = existsSync20(reportsDir) ? readdirSync11(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
46070
46309
|
const newest = written.at(-1);
|
|
46071
46310
|
if (newest !== undefined) {
|
|
46072
|
-
report = parseJUnit(readFileSync23(
|
|
46311
|
+
report = parseJUnit(readFileSync23(join27(reportsDir, newest, "results.xml"), "utf8"));
|
|
46073
46312
|
}
|
|
46074
46313
|
} catch (error) {
|
|
46075
46314
|
reportProblem = errorMessage(error);
|
|
46076
46315
|
} finally {
|
|
46077
46316
|
discard(reportsDir, userData);
|
|
46078
46317
|
try {
|
|
46079
|
-
rmdirSync2(
|
|
46318
|
+
rmdirSync2(dirname20(reportsDir));
|
|
46080
46319
|
} catch {}
|
|
46081
46320
|
}
|
|
46082
46321
|
const engineEntries = foldedRepeats(run.log.select({
|
|
@@ -46308,31 +46547,9 @@ class GodotServer {
|
|
|
46308
46547
|
async diagnosticsOf(args) {
|
|
46309
46548
|
const answer = await this.handleLSP("lsp_get_diagnostics", args);
|
|
46310
46549
|
const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
46311
|
-
|
|
46312
|
-
|
|
46313
|
-
|
|
46314
|
-
return { refusal: this.createErrorResponse(reason), perScript: true };
|
|
46315
|
-
}
|
|
46316
|
-
if (payload["stillReading"] === true) {
|
|
46317
|
-
return { refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`), perScript: false };
|
|
46318
|
-
}
|
|
46319
|
-
if (typeof payload["serves"] === "string") {
|
|
46320
|
-
return {
|
|
46321
|
-
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
46322
|
-
"editor_status names the editor on the bridge and the port it says it serves",
|
|
46323
|
-
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
46324
|
-
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
46325
|
-
]),
|
|
46326
|
-
perScript: false
|
|
46327
|
-
};
|
|
46328
|
-
}
|
|
46329
|
-
return {
|
|
46330
|
-
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
46331
|
-
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
46332
|
-
"GDHARNESS_LSP_PORT points this server at another one"
|
|
46333
|
-
]),
|
|
46334
|
-
perScript: payload["aboutThisFile"] === true
|
|
46335
|
-
};
|
|
46550
|
+
const refused = this.languageServerRefusal(payload, "Diagnostics");
|
|
46551
|
+
if (refused !== null) {
|
|
46552
|
+
return refused;
|
|
46336
46553
|
}
|
|
46337
46554
|
const diagnostics = readArray(payload, "diagnostics") ?? [];
|
|
46338
46555
|
const errors = diagnostics.filter((entry) => asParams(entry)["severity"] === "error").length;
|
|
@@ -46439,6 +46656,40 @@ class GodotServer {
|
|
|
46439
46656
|
async handleLSP(toolName, args) {
|
|
46440
46657
|
return handleLSPTool(await this.lsp(), toolName, args);
|
|
46441
46658
|
}
|
|
46659
|
+
async askScriptInfo(toolName, asked, args) {
|
|
46660
|
+
const answer = await this.handleLSP(toolName, args);
|
|
46661
|
+
const refused = this.languageServerRefusal(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), asked);
|
|
46662
|
+
return refused === null ? answer : refused.refusal;
|
|
46663
|
+
}
|
|
46664
|
+
languageServerRefusal(payload, asked) {
|
|
46665
|
+
if (payload["error"] === undefined) {
|
|
46666
|
+
return null;
|
|
46667
|
+
}
|
|
46668
|
+
const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
|
|
46669
|
+
if (payload["refusedArguments"] === true) {
|
|
46670
|
+
return { refusal: this.createErrorResponse(reason), perScript: true };
|
|
46671
|
+
}
|
|
46672
|
+
if (payload["stillReading"] === true) {
|
|
46673
|
+
return { refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`), perScript: false };
|
|
46674
|
+
}
|
|
46675
|
+
if (typeof payload["serves"] === "string") {
|
|
46676
|
+
return {
|
|
46677
|
+
refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
|
|
46678
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
46679
|
+
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
46680
|
+
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
46681
|
+
]),
|
|
46682
|
+
perScript: false
|
|
46683
|
+
};
|
|
46684
|
+
}
|
|
46685
|
+
return {
|
|
46686
|
+
refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
|
|
46687
|
+
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
46688
|
+
"GDHARNESS_LSP_PORT points this server at another one"
|
|
46689
|
+
]),
|
|
46690
|
+
perScript: payload["aboutThisFile"] === true
|
|
46691
|
+
};
|
|
46692
|
+
}
|
|
46442
46693
|
async lsp() {
|
|
46443
46694
|
const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
|
|
46444
46695
|
if (this.lspClient !== null && this.lspClient.port !== port) {
|
|
@@ -46539,7 +46790,7 @@ class GodotServer {
|
|
|
46539
46790
|
return;
|
|
46540
46791
|
}
|
|
46541
46792
|
try {
|
|
46542
|
-
|
|
46793
|
+
appendFileSync2(run.transcript, line.endsWith(`
|
|
46543
46794
|
`) ? line : `${line}
|
|
46544
46795
|
`);
|
|
46545
46796
|
} catch (error) {
|
|
@@ -46750,7 +47001,8 @@ class GodotServer {
|
|
|
46750
47001
|
serverSpeaks: RUNTIME_PROTOCOL,
|
|
46751
47002
|
behind: game.protocol > RUNTIME_PROTOCOL ? "server" : "addon"
|
|
46752
47003
|
}))
|
|
46753
|
-
}
|
|
47004
|
+
},
|
|
47005
|
+
server: { pid: process.pid, version: SERVER_VERSION, lifecycleLog: lifecycleLogPath() }
|
|
46754
47006
|
});
|
|
46755
47007
|
}
|
|
46756
47008
|
async editorServersListening() {
|
|
@@ -46782,7 +47034,7 @@ class GodotServer {
|
|
|
46782
47034
|
return null;
|
|
46783
47035
|
}
|
|
46784
47036
|
try {
|
|
46785
|
-
return settingKeys(readFileSync23(
|
|
47037
|
+
return settingKeys(readFileSync23(join27(projectPath, "project.godot"), "utf8"));
|
|
46786
47038
|
} catch {
|
|
46787
47039
|
return null;
|
|
46788
47040
|
}
|
|
@@ -46838,6 +47090,10 @@ class GodotServer {
|
|
|
46838
47090
|
async handleCloseEditor() {
|
|
46839
47091
|
const before = this.godotBridge.getStatus();
|
|
46840
47092
|
if (!before.connected) {
|
|
47093
|
+
const launched = this.launchedEditor;
|
|
47094
|
+
if (launched !== null && await this.launchedEditorIsUp() && !this.godotBridge.isConnected()) {
|
|
47095
|
+
return await this.stopTheLaunchedEditor(launched);
|
|
47096
|
+
}
|
|
46841
47097
|
return this.createErrorResponse("No editor is connected, so there is nothing to close.", [
|
|
46842
47098
|
"editor_status says whether the bridge is up and what has reached it"
|
|
46843
47099
|
]);
|
|
@@ -46871,6 +47127,31 @@ class GodotServer {
|
|
|
46871
47127
|
tookMs: Date.now() - began
|
|
46872
47128
|
});
|
|
46873
47129
|
}
|
|
47130
|
+
async stopTheLaunchedEditor(launched) {
|
|
47131
|
+
const began = Date.now();
|
|
47132
|
+
try {
|
|
47133
|
+
process.kill(launched.pid);
|
|
47134
|
+
} catch (error) {
|
|
47135
|
+
if (alive(launched.pid)) {
|
|
47136
|
+
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"]);
|
|
47137
|
+
}
|
|
47138
|
+
}
|
|
47139
|
+
const stopWaitMs = 1e4;
|
|
47140
|
+
if (!await this.waitForBridge(() => !alive(launched.pid), began + stopWaitMs)) {
|
|
47141
|
+
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"]);
|
|
47142
|
+
}
|
|
47143
|
+
this.closedHere = { editorPid: launched.pid, at: new Date().toISOString() };
|
|
47144
|
+
this.launchedEditor = null;
|
|
47145
|
+
this.launchedFrom = null;
|
|
47146
|
+
return this.jsonTextResponse({
|
|
47147
|
+
closed: true,
|
|
47148
|
+
editorPid: launched.pid,
|
|
47149
|
+
projectPath: launched.projectPath,
|
|
47150
|
+
beforeConnecting: true,
|
|
47151
|
+
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.",
|
|
47152
|
+
tookMs: Date.now() - began
|
|
47153
|
+
});
|
|
47154
|
+
}
|
|
46874
47155
|
settleTheLaunchReading() {
|
|
46875
47156
|
const watched = this.launchedFrom;
|
|
46876
47157
|
const status = this.godotBridge.getStatus();
|
|
@@ -47006,13 +47287,9 @@ class GodotServer {
|
|
|
47006
47287
|
`editor_launch with projectPath ${this.ownProject} opens the editor this server serves`
|
|
47007
47288
|
]);
|
|
47008
47289
|
}
|
|
47009
|
-
const
|
|
47010
|
-
|
|
47011
|
-
|
|
47012
|
-
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 ? [
|
|
47013
|
-
"gdharness setup in the project enables it",
|
|
47014
|
-
"Or enable it under Project Settings, Plugins, in an editor opened by hand"
|
|
47015
|
-
] : ["gdharness setup in the project installs and enables it"]);
|
|
47290
|
+
const missing = editorAddonMissing(projectPath);
|
|
47291
|
+
if (missing !== null) {
|
|
47292
|
+
return this.createErrorResponse(`${missing.why}, so an editor opened on it would never reach this server.`, missing.remedies);
|
|
47016
47293
|
}
|
|
47017
47294
|
const owed = restartOwed(projectPath);
|
|
47018
47295
|
if (owed !== null && restartUnderway(owed)) {
|
|
@@ -47040,7 +47317,8 @@ class GodotServer {
|
|
|
47040
47317
|
writeEditorLogNote(projectPath, launched.pid);
|
|
47041
47318
|
this.launchedEditor = {
|
|
47042
47319
|
pid: launched.pid,
|
|
47043
|
-
startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null
|
|
47320
|
+
startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null,
|
|
47321
|
+
projectPath
|
|
47044
47322
|
};
|
|
47045
47323
|
this.closedHere = null;
|
|
47046
47324
|
return { pid: launched.pid, error: null };
|
|
@@ -47177,7 +47455,7 @@ class GodotServer {
|
|
|
47177
47455
|
}
|
|
47178
47456
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
47179
47457
|
const budgetMs = budget.ms;
|
|
47180
|
-
if (!existsSync20(
|
|
47458
|
+
if (!existsSync20(join27(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
47181
47459
|
return runtimeVerdict(null, {
|
|
47182
47460
|
addon: false,
|
|
47183
47461
|
budgetMs,
|
|
@@ -47262,7 +47540,7 @@ class GodotServer {
|
|
|
47262
47540
|
}
|
|
47263
47541
|
let announcedAt;
|
|
47264
47542
|
try {
|
|
47265
|
-
announcedAt =
|
|
47543
|
+
announcedAt = statSync10(endpoint.file).mtimeMs;
|
|
47266
47544
|
} catch {
|
|
47267
47545
|
return;
|
|
47268
47546
|
}
|
|
@@ -47431,7 +47709,7 @@ class GodotServer {
|
|
|
47431
47709
|
return;
|
|
47432
47710
|
}
|
|
47433
47711
|
if (game.transcript !== null) {
|
|
47434
|
-
|
|
47712
|
+
appendFileSync2(game.transcript, reported);
|
|
47435
47713
|
} else {
|
|
47436
47714
|
game.log.append("stderr", reported);
|
|
47437
47715
|
}
|
|
@@ -48135,7 +48413,7 @@ class GodotServer {
|
|
|
48135
48413
|
}
|
|
48136
48414
|
const going = await this.runStillGoing(run);
|
|
48137
48415
|
const projectPath = run.projectPath;
|
|
48138
|
-
const projectGone = projectPath !== null && !existsSync20(
|
|
48416
|
+
const projectGone = projectPath !== null && !existsSync20(join27(projectPath, "project.godot"));
|
|
48139
48417
|
if (projectGone && callersName !== undefined) {
|
|
48140
48418
|
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"]);
|
|
48141
48419
|
}
|
|
@@ -48306,7 +48584,11 @@ class GodotServer {
|
|
|
48306
48584
|
if (!greeted && launched === null && status.connected) {
|
|
48307
48585
|
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"]);
|
|
48308
48586
|
}
|
|
48309
|
-
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
48587
|
+
const projectPath = status.projectPath ?? this.launchedEditor?.projectPath ?? this.ownProject ?? undefined;
|
|
48588
|
+
const elsewhere = this.anotherProjectNamed(args, projectPath);
|
|
48589
|
+
if (elsewhere !== null) {
|
|
48590
|
+
return elsewhere;
|
|
48591
|
+
}
|
|
48310
48592
|
if (projectPath === undefined || !greeted && launched === null) {
|
|
48311
48593
|
return this.createErrorResponse("No editor is connected, so there is no console to read.", [
|
|
48312
48594
|
"editor_status says whether one is connected and whether it may yet be",
|
|
@@ -48357,6 +48639,19 @@ class GodotServer {
|
|
|
48357
48639
|
omitted: selected.omitted === 0 ? undefined : selected.omitted
|
|
48358
48640
|
});
|
|
48359
48641
|
}
|
|
48642
|
+
anotherProjectNamed(args, ofWhatIsRead) {
|
|
48643
|
+
const named = readNonEmptyString(args, "projectPath");
|
|
48644
|
+
if (named === undefined || ofWhatIsRead === null || ofWhatIsRead === undefined || ofWhatIsRead === "") {
|
|
48645
|
+
return null;
|
|
48646
|
+
}
|
|
48647
|
+
if (isSameDirectory(named, ofWhatIsRead)) {
|
|
48648
|
+
return null;
|
|
48649
|
+
}
|
|
48650
|
+
return this.createErrorResponse(`editor_output reads what this server holds, which is of ${ofWhatIsRead}, and this call names ${named}.`, [
|
|
48651
|
+
"Another project's run is read through that project's own gdharness server",
|
|
48652
|
+
"editor_status names every game running on the machine under runtimes"
|
|
48653
|
+
]);
|
|
48654
|
+
}
|
|
48360
48655
|
async handleGetDebugOutput(args, waitedMs) {
|
|
48361
48656
|
if (readString(args, "op") === "editor") {
|
|
48362
48657
|
return this.handleEditorConsole(args);
|
|
@@ -48366,6 +48661,10 @@ class GodotServer {
|
|
|
48366
48661
|
if (!run) {
|
|
48367
48662
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
48368
48663
|
}
|
|
48664
|
+
const elsewhere = this.anotherProjectNamed(args, run.projectPath ?? this.godotBridge.getStatus().projectPath);
|
|
48665
|
+
if (elsewhere !== null) {
|
|
48666
|
+
return elsewhere;
|
|
48667
|
+
}
|
|
48369
48668
|
await this.tieThePlayedGame(run);
|
|
48370
48669
|
this.drainEditorOutput(run);
|
|
48371
48670
|
this.drainTranscript(run);
|
|
@@ -48402,7 +48701,7 @@ class GodotServer {
|
|
|
48402
48701
|
notes.push(endedWithoutACode({
|
|
48403
48702
|
throughEditor: run.throughEditor,
|
|
48404
48703
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
48405
|
-
couldAnnounce: run.projectPath !== null && existsSync20(
|
|
48704
|
+
couldAnnounce: run.projectPath !== null && existsSync20(join27(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
48406
48705
|
endedHere: typeof run.endedHere === "string",
|
|
48407
48706
|
pickedUp: run.pickedUp === true
|
|
48408
48707
|
}));
|
|
@@ -49182,15 +49481,47 @@ class GodotServer {
|
|
|
49182
49481
|
const holds = held["classes"].map(String);
|
|
49183
49482
|
return { unseen: unseenByEditor(projectPath, holds), stillHeld: heldButGone(projectPath, holds) };
|
|
49184
49483
|
}
|
|
49484
|
+
async noEditorYet(args, instead) {
|
|
49485
|
+
const project = readNonEmptyString(args, "projectPath") ?? this.ownProject;
|
|
49486
|
+
const missing = project === null ? null : editorAddonMissing(project);
|
|
49487
|
+
if (missing !== null) {
|
|
49488
|
+
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"]);
|
|
49489
|
+
}
|
|
49490
|
+
const launched = this.launchedEditor;
|
|
49491
|
+
if (launched !== null && await this.launchedEditorIsUp()) {
|
|
49492
|
+
const began = Date.now();
|
|
49493
|
+
await this.waitForBridge(() => hasSaidWhoItIs(this.godotBridge.getStatus()) || !alive(launched.pid), began + editorRestartTimeoutMs());
|
|
49494
|
+
if (this.godotBridge.isConnected()) {
|
|
49495
|
+
return null;
|
|
49496
|
+
}
|
|
49497
|
+
const opened = `the editor this server opened on ${launched.projectPath}, pid ${launched.pid}`;
|
|
49498
|
+
if (alive(launched.pid)) {
|
|
49499
|
+
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.`, [
|
|
49500
|
+
...instead,
|
|
49501
|
+
"editor_status names it under awaitingLaunchedEditor until it has connected",
|
|
49502
|
+
"editor_output op editor shows what it is printing meanwhile"
|
|
49503
|
+
]);
|
|
49504
|
+
}
|
|
49505
|
+
return this.createErrorResponse(`Editor not connected: ${opened}, exited without reaching the bridge.`, [
|
|
49506
|
+
...instead,
|
|
49507
|
+
"editor_output op editor shows what it printed before it went",
|
|
49508
|
+
"editor_launch open starts another"
|
|
49509
|
+
]);
|
|
49510
|
+
}
|
|
49511
|
+
return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
|
|
49512
|
+
...instead,
|
|
49513
|
+
"Open the project in the editor with editor_launch, or by hand",
|
|
49514
|
+
"Enable the addon under Project > Project Settings > Plugins",
|
|
49515
|
+
"editor_status says whether the bridge is up and what it is listening on"
|
|
49516
|
+
]);
|
|
49517
|
+
}
|
|
49185
49518
|
async handleViaBridge(toolName, args, askedAs) {
|
|
49186
49519
|
if (!this.godotBridge.isConnected()) {
|
|
49187
|
-
const
|
|
49188
|
-
|
|
49189
|
-
|
|
49190
|
-
|
|
49191
|
-
|
|
49192
|
-
"editor_status says whether the bridge is up and what it is listening on"
|
|
49193
|
-
]);
|
|
49520
|
+
const alternative = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
|
|
49521
|
+
const refused = await this.noEditorYet(args, alternative === undefined ? [] : [alternative]);
|
|
49522
|
+
if (refused !== null) {
|
|
49523
|
+
return refused;
|
|
49524
|
+
}
|
|
49194
49525
|
}
|
|
49195
49526
|
const open = this.godotBridge.getStatus().projectPath;
|
|
49196
49527
|
const meant = readNonEmptyString(args, "projectPath");
|
|
@@ -49263,7 +49594,7 @@ class GodotServer {
|
|
|
49263
49594
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
49264
49595
|
const run = await this.currentRun();
|
|
49265
49596
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
49266
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync20(
|
|
49597
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync20(join27(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
|
|
49267
49598
|
return null;
|
|
49268
49599
|
}
|
|
49269
49600
|
const already = runtimesAnnounced().running;
|
|
@@ -49272,7 +49603,7 @@ class GodotServer {
|
|
|
49272
49603
|
return !run.announcedBefore.has(one.pid);
|
|
49273
49604
|
}
|
|
49274
49605
|
try {
|
|
49275
|
-
return
|
|
49606
|
+
return statSync10(one.file).mtimeMs >= run.startedAt;
|
|
49276
49607
|
} catch {
|
|
49277
49608
|
return false;
|
|
49278
49609
|
}
|
|
@@ -49348,7 +49679,7 @@ class GodotServer {
|
|
|
49348
49679
|
}
|
|
49349
49680
|
}
|
|
49350
49681
|
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
49351
|
-
const screenshotPath = screenshotDir ?
|
|
49682
|
+
const screenshotPath = screenshotDir ? join27(screenshotDir, "capture.png") : null;
|
|
49352
49683
|
const staleNote = this.runtimeStaleNote(choice.endpoint);
|
|
49353
49684
|
const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
|
|
49354
49685
|
const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
|
|
@@ -49499,7 +49830,7 @@ async function runGodotServer() {
|
|
|
49499
49830
|
const server = new GodotServer;
|
|
49500
49831
|
await server.run();
|
|
49501
49832
|
}
|
|
49502
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, EDITOR_FREE_INSTEAD, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
49833
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, EDITOR_FREE_INSTEAD, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
49503
49834
|
var init_server = __esm(() => {
|
|
49504
49835
|
init_dist();
|
|
49505
49836
|
init_stdio();
|
|
@@ -49528,6 +49859,7 @@ var init_server = __esm(() => {
|
|
|
49528
49859
|
init_junit();
|
|
49529
49860
|
init_keeper_channel();
|
|
49530
49861
|
init_launch();
|
|
49862
|
+
init_lifecycle_log();
|
|
49531
49863
|
init_lsp_client();
|
|
49532
49864
|
init_outside();
|
|
49533
49865
|
init_paths();
|
|
@@ -49553,7 +49885,7 @@ var init_server = __esm(() => {
|
|
|
49553
49885
|
init_tool_reference();
|
|
49554
49886
|
init_update_check();
|
|
49555
49887
|
run4 = promisify5(execFile5);
|
|
49556
|
-
__dirname2 =
|
|
49888
|
+
__dirname2 = dirname20(fileURLToPath5(import.meta.url));
|
|
49557
49889
|
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
49558
49890
|
LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
49559
49891
|
VALUE_NAMES = {
|
|
@@ -49612,7 +49944,7 @@ init_bridge_announce();
|
|
|
49612
49944
|
init_errors();
|
|
49613
49945
|
init_godot_path();
|
|
49614
49946
|
import { existsSync as existsSync21 } from "node:fs";
|
|
49615
|
-
import { join as
|
|
49947
|
+
import { join as join28, resolve as resolve8 } from "node:path";
|
|
49616
49948
|
|
|
49617
49949
|
// src/harnesses.ts
|
|
49618
49950
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -50773,7 +51105,9 @@ emptily: no game running, a game with no debugger behind it, a game running rath
|
|
|
50773
51105
|
control outside a headless viewport. Read the refusal instead of retrying the call.
|
|
50774
51106
|
|
|
50775
51107
|
Every answer is read back from the engine after the change rather than echoed from the request, and
|
|
50776
|
-
whatever the engine said on stderr comes back under \`engine_messages\`.
|
|
51108
|
+
whatever the engine said on stderr comes back under \`engine_messages\`. A headless engine runs the
|
|
51109
|
+
project's autoloads after the operation has answered, and what it reports from then until it exits
|
|
51110
|
+
comes back separately, under \`engine_messages_after_answer\`: none of it changes the answer.
|
|
50777
51111
|
|
|
50778
51112
|
## More
|
|
50779
51113
|
|
|
@@ -50875,7 +51209,7 @@ class UsageError extends Refusal {
|
|
|
50875
51209
|
function projectArgument(at) {
|
|
50876
51210
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
50877
51211
|
const projectPath = resolve8(named ?? ".");
|
|
50878
|
-
if (!existsSync21(
|
|
51212
|
+
if (!existsSync21(join28(projectPath, "project.godot"))) {
|
|
50879
51213
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
50880
51214
|
}
|
|
50881
51215
|
return projectPath;
|
|
@@ -50920,7 +51254,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
50920
51254
|
return;
|
|
50921
51255
|
}
|
|
50922
51256
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
50923
|
-
if (!existsSync21(
|
|
51257
|
+
if (!existsSync21(join28(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
50924
51258
|
console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
|
|
50925
51259
|
}
|
|
50926
51260
|
}
|