gdharness 1.2.41 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +643 -378
- package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -4
- package/build/godot/addons/gdharness_editor/bridge_client.gd +1 -1
- package/build/godot/addons/gdharness_editor/file_reads.gd +134 -10
- package/build/godot/addons/gdharness_editor/headless_check.gd +34 -4
- package/build/godot/addons/gdharness_editor/property_values.gd +4 -9
- package/build/godot/addons/gdharness_editor/reading.gd +25 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +209 -15
- package/build/godot/addons/gdharness_editor/serialisation.gd +5 -1
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +9 -9
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +346 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +438 -0
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +91 -49
- package/build/godot/addons/gdharness_runtime/reading.gd +25 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +29 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +2 -7
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +6 -14
- package/build/godot/addons/gdharness_runtime/serialisation.gd +5 -1
- package/build/godot/operations/animation_reads.gd +7 -4
- package/build/godot/operations/editor_tools.gd +19 -1
- package/build/godot/operations/file_reads.gd +134 -10
- package/build/godot/operations/property_values.gd +4 -9
- package/build/godot/operations/reading.gd +25 -0
- package/build/godot/operations/serialisation.gd +5 -1
- package/build/keeper.js +10 -4
- package/package.json +1 -2
- package/build/index.js +0 -45043
package/build/cli.js
CHANGED
|
@@ -504,16 +504,23 @@ function readAnnouncement(path) {
|
|
|
504
504
|
return null;
|
|
505
505
|
}
|
|
506
506
|
const said = parsed;
|
|
507
|
-
if (typeof said.port !== "number" || typeof said.pid !== "number") {
|
|
507
|
+
if (said.protocol !== BRIDGE_ANNOUNCE_PROTOCOL || typeof said.host !== "string" || typeof said.port !== "number" || typeof said.pid !== "number" || typeof said.version !== "string" || typeof said.startedAt !== "string") {
|
|
508
508
|
return null;
|
|
509
509
|
}
|
|
510
|
-
return
|
|
510
|
+
return {
|
|
511
|
+
protocol: said.protocol,
|
|
512
|
+
host: said.host,
|
|
513
|
+
port: said.port,
|
|
514
|
+
pid: said.pid,
|
|
515
|
+
version: said.version,
|
|
516
|
+
startedAt: said.startedAt
|
|
517
|
+
};
|
|
511
518
|
} catch {
|
|
512
519
|
return null;
|
|
513
520
|
}
|
|
514
521
|
}
|
|
515
522
|
function serverBehind(announcement, startedAt) {
|
|
516
|
-
if (announcement === null ||
|
|
523
|
+
if (announcement === null || startedAt === undefined) {
|
|
517
524
|
return null;
|
|
518
525
|
}
|
|
519
526
|
const announcedAt = Date.parse(announcement.startedAt);
|
|
@@ -668,12 +675,8 @@ function resolveHeadless(requested, host) {
|
|
|
668
675
|
}
|
|
669
676
|
return envValue("DISPLAY", host.variables) === undefined && envValue("WAYLAND_DISPLAY", host.variables) === undefined;
|
|
670
677
|
}
|
|
671
|
-
function resolveSilent(requested
|
|
672
|
-
|
|
673
|
-
return requested;
|
|
674
|
-
}
|
|
675
|
-
const said = envValue("GDHARNESS_SILENT", variables)?.toLowerCase();
|
|
676
|
-
return said !== undefined && said !== "0" && said !== "false";
|
|
678
|
+
function resolveSilent(requested) {
|
|
679
|
+
return requested !== false;
|
|
677
680
|
}
|
|
678
681
|
function engineFlags(given) {
|
|
679
682
|
const offered = Object.keys(ENGINE_FLAGS).join(", ");
|
|
@@ -727,10 +730,11 @@ function runArguments(options) {
|
|
|
727
730
|
}
|
|
728
731
|
return args;
|
|
729
732
|
}
|
|
730
|
-
function editorArguments(projectPath, ports, logFile, hidden) {
|
|
733
|
+
function editorArguments(projectPath, ports, logFile, hidden, silent) {
|
|
731
734
|
return [
|
|
732
735
|
"-e",
|
|
733
736
|
...hidden ? ["--headless"] : [],
|
|
737
|
+
...silent && !hidden ? ["--audio-driver", "Dummy"] : [],
|
|
734
738
|
"--path",
|
|
735
739
|
projectPath,
|
|
736
740
|
"--lsp-port",
|
|
@@ -9653,32 +9657,30 @@ var init_update_check = __esm(() => {
|
|
|
9653
9657
|
|
|
9654
9658
|
// src/server-version.ts
|
|
9655
9659
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
9656
|
-
function
|
|
9657
|
-
|
|
9658
|
-
return addonDigest !== shippedDigest;
|
|
9659
|
-
}
|
|
9660
|
-
return addonVersion !== serverVersion;
|
|
9660
|
+
function staleAddon(addonDigest, shippedDigest) {
|
|
9661
|
+
return addonDigest !== shippedDigest;
|
|
9661
9662
|
}
|
|
9662
|
-
function sameCodeNote(addonVersion, serverVersion
|
|
9663
|
-
if (addonVersion ===
|
|
9663
|
+
function sameCodeNote(addonVersion, serverVersion) {
|
|
9664
|
+
if (addonVersion === "" || addonVersion === serverVersion) {
|
|
9664
9665
|
return;
|
|
9665
9666
|
}
|
|
9666
9667
|
const same = `The editor loaded the ${addonVersion} addons, which are the same code this ${serverVersion} server ships, so nothing needs restarting`;
|
|
9667
9668
|
return isNewer(addonVersion, serverVersion) ? `${same}. This server is the older of the two, which a reconnect in your harness settles once the project pins ${addonVersion}.` : `${same}; addonVersion is the version the editor loaded when it started.`;
|
|
9668
9669
|
}
|
|
9669
9670
|
function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
|
|
9670
|
-
if (!
|
|
9671
|
+
if (!staleAddon(addonDigest, shippedDigest)) {
|
|
9671
9672
|
return;
|
|
9672
9673
|
}
|
|
9673
|
-
const reported = addonVersion
|
|
9674
|
+
const reported = addonVersion;
|
|
9674
9675
|
const restart = onDisk !== undefined && onDisk !== shippedDigest ? `The project's own addons are not this server's either, so a restart would load the same code again: run ${gdharnessCommand(serverVersion, "upgrade")} in a shell in the project directory, which installs the ${serverVersion} addons, and editor_launch restart then picks them up.` : "Restart it with editor_launch restart to pick this one up.";
|
|
9676
|
+
const asked = "so this server asks it nothing but to quit:";
|
|
9675
9677
|
if (reported === serverVersion) {
|
|
9676
|
-
return `The editor is running a different build of the ${reported} addon from the one this server ships
|
|
9678
|
+
return `The editor is running a different build of the ${reported} addon from the one this server ships, ${asked} ${restart}`;
|
|
9677
9679
|
}
|
|
9678
|
-
const editor = reported === "" ?
|
|
9679
|
-
const both = `The editor is running ${editor} while this server ships ${serverVersion}
|
|
9680
|
+
const editor = reported === "" ? "an addon with no version marker" : `the ${reported} addon`;
|
|
9681
|
+
const both = `The editor is running ${editor} while this server ships ${serverVersion}, ${asked}`;
|
|
9680
9682
|
if (reported !== "" && isNewer(reported, serverVersion)) {
|
|
9681
|
-
return `${both}
|
|
9683
|
+
return `${both} this server is the older half; reconnect it in your harness so it spawns ${reported}.`;
|
|
9682
9684
|
}
|
|
9683
9685
|
return `${both} ${restart}`;
|
|
9684
9686
|
}
|
|
@@ -9686,23 +9688,18 @@ function gdharnessCommand(version, args) {
|
|
|
9686
9688
|
return `npx -y gdharness@${version} ${args}`;
|
|
9687
9689
|
}
|
|
9688
9690
|
function runtimeAddonUnknown(addon) {
|
|
9689
|
-
return addon
|
|
9691
|
+
return addon.version === "" && addon.digest === "";
|
|
9690
9692
|
}
|
|
9691
9693
|
function runtimeMismatch(pid, addon, serverVersion, shippedDigest, onDisk, projectPath) {
|
|
9692
|
-
if (runtimeAddonUnknown(addon)) {
|
|
9693
|
-
return;
|
|
9694
|
-
}
|
|
9695
|
-
if (addon !== undefined && !editorIsStale(addon.version, serverVersion, addon.digest, shippedDigest)) {
|
|
9694
|
+
if (runtimeAddonUnknown(addon) || !staleAddon(addon.digest, shippedDigest)) {
|
|
9696
9695
|
return;
|
|
9697
9696
|
}
|
|
9698
9697
|
const game = `The game (pid ${pid})`;
|
|
9699
|
-
if (addon
|
|
9700
|
-
return `${game} is running the ${addon.version} runtime addon while this server ships ${serverVersion}. This server is the older half: reconnect it in your harness so it spawns ${addon.version}.`;
|
|
9698
|
+
if (addon.version !== "" && isNewer(addon.version, serverVersion)) {
|
|
9699
|
+
return `${game} is running the ${addon.version} runtime addon while this server ships ${serverVersion}, so this server asks it nothing. This server is the older half: reconnect it in your harness so it spawns ${addon.version}.`;
|
|
9701
9700
|
}
|
|
9702
9701
|
let loaded;
|
|
9703
|
-
if (addon ===
|
|
9704
|
-
loaded = `a runtime addon from before games reported theirs, while this server ships ${serverVersion}`;
|
|
9705
|
-
} else if (addon.version === serverVersion) {
|
|
9702
|
+
if (addon.version === serverVersion) {
|
|
9706
9703
|
loaded = `a different build of the ${serverVersion} runtime addon from the one this server ships`;
|
|
9707
9704
|
} else if (addon.version === "") {
|
|
9708
9705
|
loaded = `a runtime addon with no version marker, while this server ships ${serverVersion}`;
|
|
@@ -9710,20 +9707,12 @@ function runtimeMismatch(pid, addon, serverVersion, shippedDigest, onDisk, proje
|
|
|
9710
9707
|
loaded = `the ${addon.version} runtime addon, while this server ships ${serverVersion}`;
|
|
9711
9708
|
}
|
|
9712
9709
|
const remedy = onDisk !== undefined && onDisk !== shippedDigest ? `The project's own runtime addon is not this server's either, so starting the game again loads the same code: run ${gdharnessCommand(serverVersion, `upgrade "${projectPath}"`)} in a shell, which installs the ${serverVersion} addons, and the game picks them up when it next starts.` : `The project holds this server's runtime addon, so the game picks it up when it next starts.`;
|
|
9713
|
-
return `${game} is running ${loaded},
|
|
9714
|
-
}
|
|
9715
|
-
function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
|
|
9716
|
-
const staleNote = addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk);
|
|
9717
|
-
if (staleNote === undefined || typeof answer !== "object" || answer === null || Array.isArray(answer)) {
|
|
9718
|
-
return answer;
|
|
9719
|
-
}
|
|
9720
|
-
return { ...answer, addonIsStale: true, staleNote };
|
|
9710
|
+
return `${game} is running ${loaded}, so this server asks it nothing: its answers would come from that code. ${remedy}`;
|
|
9721
9711
|
}
|
|
9722
|
-
var DEBUG_MODE,
|
|
9712
|
+
var DEBUG_MODE, SERVER_VERSION;
|
|
9723
9713
|
var init_server_version = __esm(() => {
|
|
9724
9714
|
init_update_check();
|
|
9725
|
-
DEBUG_MODE = process.env["
|
|
9726
|
-
GODOT_DEBUG_MODE_DEFAULT = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE;
|
|
9715
|
+
DEBUG_MODE = process.env["GDHARNESS_DEBUG"] === "1";
|
|
9727
9716
|
SERVER_VERSION = (() => {
|
|
9728
9717
|
try {
|
|
9729
9718
|
const pkg = JSON.parse(readFileSync7(new URL("../package.json", import.meta.url), "utf8"));
|
|
@@ -10812,9 +10801,7 @@ function failedReloadNote(failed) {
|
|
|
10812
10801
|
const answered = failed.codeName === "" ? `error ${failed.code}` : `error ${failed.code}, ${failed.codeName}`;
|
|
10813
10802
|
const unusable = "A failed compile leaves the copy the editor holds unusable, measured on 4.7.2: the editor can make no instance of it, and one it already made has lost its methods, while its member lists read as before.";
|
|
10814
10803
|
let copy;
|
|
10815
|
-
if (failed.restored
|
|
10816
|
-
copy = `${unusable} This editor's addon does not put it back, so it stays that way until the script compiles: reload it then, or restart the editor with editor_launch restart.`;
|
|
10817
|
-
} else if (!failed.restored) {
|
|
10804
|
+
if (!failed.restored) {
|
|
10818
10805
|
copy = `${unusable} The editor compiled it again from the text it was built from, and that failed too${failed.restoredAs === "" ? "" : ` (${failed.restoredAs})`}, so it stays that way until the script compiles: reload it then, or restart the editor with editor_launch restart.`;
|
|
10819
10806
|
} else if (unchanged) {
|
|
10820
10807
|
copy = "The copy the editor holds was compiled again from the text it was built from, which puts it back as it was, with the members it held before, which listMembers lists.";
|
|
@@ -10969,6 +10956,34 @@ function scriptsWithoutUid(projectPath) {
|
|
|
10969
10956
|
visit(projectPath, "");
|
|
10970
10957
|
return found.sort();
|
|
10971
10958
|
}
|
|
10959
|
+
function pathsInUidCache(projectPath) {
|
|
10960
|
+
let data;
|
|
10961
|
+
try {
|
|
10962
|
+
data = readFileSync9(join13(projectPath, ".godot", "uid_cache.bin"));
|
|
10963
|
+
} catch {
|
|
10964
|
+
return null;
|
|
10965
|
+
}
|
|
10966
|
+
if (data.length < 4) {
|
|
10967
|
+
return null;
|
|
10968
|
+
}
|
|
10969
|
+
const count = data.readUInt32LE(0);
|
|
10970
|
+
const paths = new Map;
|
|
10971
|
+
let at = 4;
|
|
10972
|
+
for (let entry = 0;entry < count; entry++) {
|
|
10973
|
+
if (at + 12 > data.length) {
|
|
10974
|
+
return null;
|
|
10975
|
+
}
|
|
10976
|
+
const id = data.readBigUInt64LE(at);
|
|
10977
|
+
const length = data.readUInt32LE(at + 8);
|
|
10978
|
+
at += 12;
|
|
10979
|
+
if (at + length > data.length) {
|
|
10980
|
+
return null;
|
|
10981
|
+
}
|
|
10982
|
+
paths.set(id, data.toString("utf8", at, at + length));
|
|
10983
|
+
at += length;
|
|
10984
|
+
}
|
|
10985
|
+
return new Set(paths.values());
|
|
10986
|
+
}
|
|
10972
10987
|
function isBinary(bytes) {
|
|
10973
10988
|
return bytes.subarray(0, 8000).includes(0);
|
|
10974
10989
|
}
|
|
@@ -11540,24 +11555,12 @@ function addonsDigest(names, from) {
|
|
|
11540
11555
|
return hash.digest("hex");
|
|
11541
11556
|
}
|
|
11542
11557
|
function shippedEditorDigest() {
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
shippedDigest = editorAddonDigest();
|
|
11546
|
-
} catch {
|
|
11547
|
-
shippedDigest = null;
|
|
11548
|
-
}
|
|
11549
|
-
}
|
|
11550
|
-
return shippedDigest ?? undefined;
|
|
11558
|
+
shippedDigest ??= editorAddonDigest();
|
|
11559
|
+
return shippedDigest;
|
|
11551
11560
|
}
|
|
11552
11561
|
function shippedRuntimeDigest() {
|
|
11553
|
-
|
|
11554
|
-
|
|
11555
|
-
shippedRuntime = runtimeAddonDigest();
|
|
11556
|
-
} catch {
|
|
11557
|
-
shippedRuntime = null;
|
|
11558
|
-
}
|
|
11559
|
-
}
|
|
11560
|
-
return shippedRuntime ?? undefined;
|
|
11562
|
+
shippedRuntime ??= runtimeAddonDigest();
|
|
11563
|
+
return shippedRuntime;
|
|
11561
11564
|
}
|
|
11562
11565
|
function installedEditorDigest(projectPath) {
|
|
11563
11566
|
return digestMarker(projectPath, ADDONS[0]);
|
|
@@ -11871,7 +11874,7 @@ function annotationsOf(spec) {
|
|
|
11871
11874
|
openWorldHint: false
|
|
11872
11875
|
};
|
|
11873
11876
|
}
|
|
11874
|
-
var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "Through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR2, TOOL_SPECS, SPECS_BY_NAME;
|
|
11877
|
+
var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "Through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. Each file the written one refers to is named by its uid, as the editor names it, except a file the project's uid cache has no uid for yet (one made since the project was last scanned), which is named by path and listed under namedByPath with the call that scans it. An editor of the project that is running and has not reached this server is waited for rather than written around.", IMPORTED_FIRST = "importedFirst names the files whose import was behind them on disk, such as a picture changed since it was imported or one never imported, which were imported before the run through project_import reimport, so the run reads them as they are now; it is left out when every import was current. notImportedFirst lists, as project_import reimport does, any it could not import, and importNote says that the run reads those as they were. The import gives every script without a .uid sidecar one, as the editor's scan does, and uidsWrittenFirst names those scripts. importTookMs is how long the import took; it is outside the run's own time limit, so a call that imported can take that much longer than the limit it was given.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR2, TOOL_SPECS, SPECS_BY_NAME;
|
|
11875
11878
|
var init_tool_definitions = __esm(() => {
|
|
11876
11879
|
init_launch();
|
|
11877
11880
|
PROJECT_INFO_SECTIONS = [
|
|
@@ -12062,7 +12065,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12062
12065
|
{
|
|
12063
12066
|
name: "project_settings",
|
|
12064
12067
|
title: "Project settings",
|
|
12065
|
-
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
|
|
12068
|
+
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, or one on another release's addon that was not asked, 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. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events and deadzone, and answers was_new false with what it held under old_events and old_deadzone. 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 moves no other line; the save is the engine's, and like the editor's it writes Godot's header and config/features into a file that lacks them. 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.",
|
|
12066
12069
|
parameters: {
|
|
12067
12070
|
projectPath: PROJECT_PATH,
|
|
12068
12071
|
setting: {
|
|
@@ -12273,7 +12276,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12273
12276
|
operations: {
|
|
12274
12277
|
status: {
|
|
12275
12278
|
effect: "read",
|
|
12276
|
-
summary: "which resources are outdated or failed, or one resource with resourcePath, each with its reason. Judged by content with the checks the editor makes before it reimports: a sidecar changed since its import, as setting an option without a reimport leaves it; a sidecar with no uid or written for another file; an output under .godot/imported that is gone (missing_outputs) or changed; a source whose hash differs from the one its import recorded, or with no record. Beyond the editor, a glTF scene whose import does not depend on an image it names, because it was built before that image was imported (imported_without; a scene whose import runs an import script is not judged on images). failed is an import that failed, which the editor never tries again on its own. Without resourcePath, a sidecar whose source is gone is listed as missing_source",
|
|
12279
|
+
summary: "which resources are outdated or failed, or one resource with resourcePath, each with its reason and a status the summary counts: needs_reimport (out of date, or a file the engine imports that has never been imported), failed, up_to_date, missing_source, or not_imported (a file the engine does not import at all, with why). Judged by content with the checks the editor makes before it reimports: a sidecar changed since its import, as setting an option without a reimport leaves it; a sidecar with no uid or written for another file; an output under .godot/imported that is gone (missing_outputs) or changed; a source whose hash differs from the one its import recorded, or with no record. Beyond the editor, a glTF scene whose import does not depend on an image it names, because it was built before that image was imported (imported_without; a scene whose import runs an import script is not judged on images). failed is an import that failed, which the editor never tries again on its own. Without resourcePath, a sidecar whose source is gone is listed as missing_source",
|
|
12277
12280
|
requires: []
|
|
12278
12281
|
},
|
|
12279
12282
|
options: { effect: "read", summary: "the import options of one resource", requires: ["resourcePath"] },
|
|
@@ -12294,7 +12297,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12294
12297
|
},
|
|
12295
12298
|
refresh_uids: {
|
|
12296
12299
|
effect: "additive",
|
|
12297
|
-
summary: "import the project so every script and shader has its .uid sidecar, and name under uidsCreated the
|
|
12300
|
+
summary: "import the project so every script and shader has its .uid sidecar and the uid cache holds the uid of every file, scenes made with no editor among them, which a headless write names a reference by (see namedByPath), and name under uidsCreated the sidecars this made, under uidsCached the res:// files whose uid the cache holds now and did not before, and under stillWithoutUid the ones the engine would not import: it writes no scene and no script, so a project whose sidecars are all present is left untouched",
|
|
12298
12301
|
requires: []
|
|
12299
12302
|
},
|
|
12300
12303
|
refresh_classes: {
|
|
@@ -12344,7 +12347,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12344
12347
|
title: "Run tests",
|
|
12345
12348
|
effect: "additive",
|
|
12346
12349
|
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
12347
|
-
answer: "time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for.",
|
|
12350
|
+
answer: "time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for. " + IMPORTED_FIRST,
|
|
12348
12351
|
parameters: {
|
|
12349
12352
|
projectPath: PROJECT_PATH,
|
|
12350
12353
|
path: {
|
|
@@ -12393,7 +12396,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
12393
12396
|
rootNodeName: {
|
|
12394
12397
|
type: "string",
|
|
12395
12398
|
ops: ["create"],
|
|
12396
|
-
description: "create: the root node name. Default the class name, as the editor names a new root."
|
|
12399
|
+
description: "create: the root node name. Default the class name, as the editor names a new root, or with inherits the name of that scene's root."
|
|
12400
|
+
},
|
|
12401
|
+
inherits: {
|
|
12402
|
+
type: "string",
|
|
12403
|
+
ops: ["create"],
|
|
12404
|
+
description: `create: a scene for the new one to inherit, such as "parts/crate.tscn", as the editor's New Inherited Scene makes one. Its root is an instance of that scene, so changes there reach it, and scene_node set gives the nodes it inherits values of this scene's own. The root's class is that scene's, so rootNodeType is not given with it; the answer names it under rootNodeType and the scene under inherits.`
|
|
12397
12405
|
},
|
|
12398
12406
|
script: { type: "string", ops: ["create"], description: "create: a script to attach to the root." },
|
|
12399
12407
|
newPath: { type: "string", ops: ["save_as"], description: "save_as: where the copy goes." }
|
|
@@ -12414,11 +12422,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
12414
12422
|
name: "scene_tree",
|
|
12415
12423
|
title: "Scene tree",
|
|
12416
12424
|
effect: "read",
|
|
12417
|
-
description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. A node that instances another scene names that file under instanceOf, and the nodes listed under it come from that file rather than this one. Needs no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without making its nodes, so no script of theirs runs (autoloads still start once it has answered, as in every headless call). For the nodes of a game that is running, runtime_inspect tree.",
|
|
12425
|
+
description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. A node that instances another scene names that file under instanceOf, and the nodes listed under it come from that file rather than this one; its editableChildren says whether this scene, or a scene it inherits, keeps values of its own for them (scene_node set editableChildren). Needs no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without making its nodes, so no script of theirs runs (autoloads still start once it has answered, as in every headless call). For the nodes of a game that is running, runtime_inspect tree.",
|
|
12418
12426
|
parameters: {
|
|
12419
12427
|
projectPath: PROJECT_PATH,
|
|
12420
12428
|
scenePath: SCENE_PATH,
|
|
12421
|
-
depth: {
|
|
12429
|
+
depth: {
|
|
12430
|
+
type: "number",
|
|
12431
|
+
description: "How many levels to descend. Default: all. A node at the last level that has children of its own lists none and says how many under childrenNotListed."
|
|
12432
|
+
},
|
|
12422
12433
|
includeProperties: { type: "boolean", description: "Include each node's properties. Default false." }
|
|
12423
12434
|
},
|
|
12424
12435
|
requires: ["projectPath", "scenePath"]
|
|
@@ -12426,7 +12437,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12426
12437
|
{
|
|
12427
12438
|
name: "scene_node",
|
|
12428
12439
|
title: "Scene nodes",
|
|
12429
|
-
description: `One node in a scene file: add, read, set, duplicate, reparent or delete it, or paint TileMap cells. Any ClassDB node type or script class can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties, and another scene is instanced by an add with instancePath. A set with properties {"name": "Boss"} renames a node. A property holding a Resource takes the res:// path of one, so a texture, a material or a theme is a set like any other. A write is refused while the editor holds unsaved changes to the scene, and
|
|
12440
|
+
description: `One node in a scene file: add, read, set, duplicate, reparent or delete it, or paint TileMap cells. Any ClassDB node type or script class can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties, and another scene is instanced by an add with instancePath. A set with properties {"name": "Boss"} renames a node, and answers nodePath as its new path. A rename or a reparent carries the node paths naming the node, as the editor's rename does: NodePath values on nodes (in arrays, dictionaries, built-in resources, %unique paths) and the tracks its players' animations hold, saving a library kept in a .tres of its own. Each rewrite is listed under retargeted ({scenePath, nodePath, property or animation and track, from, to}), and under notRetargeted, with a reason, each left: a track kept in another scene or an imported file, or a value another scene gives that the move left naming nothing. The move is followed into every other .tscn that instances or inherits this scene at any depth: the editable children, values, added nodes and connections named by the old path are rewritten, since Godot finds a moved node's values by unique_id but editable children only by path and drops the values kept below them on the next save, and that scene's node paths and tracks are carried as above. A reparent also drops the node ids saved along each rewritten path, which ran through the old parent; the next save writes them again. The scenes rewritten are named under followed, and under notFollowed any left as they were, with why. A delete rewrites no path and lists under stillNaming each node path and track, here and in the scenes instancing or inheriting this one, that named the deleted node and now names nothing. A property holding a Resource takes the res:// path of one, so a texture, a material or a theme is a set like any other. A write is refused while the editor holds unsaved changes to the scene. A node this scene inherits takes values of this scene's own, and so does a node inside an instance of another scene once that instance's editableChildren is true; neither can be moved, renamed, copied or deleted, since the other scene makes it again on load. ${WITHOUT_AN_EDITOR}`,
|
|
12430
12441
|
parameters: {
|
|
12431
12442
|
projectPath: PROJECT_PATH,
|
|
12432
12443
|
scenePath: SCENE_PATH,
|
|
@@ -12439,7 +12450,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12439
12450
|
nodeType: {
|
|
12440
12451
|
type: "string",
|
|
12441
12452
|
ops: ["add"],
|
|
12442
|
-
description:
|
|
12453
|
+
description: `add: the node class, such as "CharacterBody2D", or a class_name a project script declares, which the node is made as with that script attached. Give this or instancePath. The answer's nodeType is the class of the node made, for an instance the class of its scene's root.`
|
|
12443
12454
|
},
|
|
12444
12455
|
instancePath: {
|
|
12445
12456
|
type: "string",
|
|
@@ -12448,6 +12459,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
12448
12459
|
},
|
|
12449
12460
|
nodeName: { type: "string", ops: ["add"], description: "add: the new node's name." },
|
|
12450
12461
|
properties: { ...PROPERTIES, ops: ["add", "set"] },
|
|
12462
|
+
editableChildren: {
|
|
12463
|
+
type: "boolean",
|
|
12464
|
+
ops: ["set"],
|
|
12465
|
+
description: "set: on an instance of another scene, whether its children are editable here, as the editor's Editable Children makes them. True lets scene_node set give the nodes inside that instance values of this scene's own, which no other instance shares. An instance nested inside another takes it once the one around it has editable children. False drops every value this scene keeps for them, and every node it added below one of them. The answer names the nodes whose values went under droppedValues (they stay, with the instance's own values) and the added nodes that went under droppedNodes; a node added straight under the instance stays. The answer says editableChildren as the file now holds it."
|
|
12466
|
+
},
|
|
12451
12467
|
newName: { type: "string", ops: ["duplicate"], description: "duplicate: the copy's name." },
|
|
12452
12468
|
newParentPath: {
|
|
12453
12469
|
type: "string",
|
|
@@ -12481,17 +12497,17 @@ var init_tool_definitions = __esm(() => {
|
|
|
12481
12497
|
},
|
|
12482
12498
|
get: {
|
|
12483
12499
|
effect: "read",
|
|
12484
|
-
summary: "read a node's properties, and a TileMapLayer's or TileMap's cells as set_tilemap_cells takes them, the first 1000 with cellCount for all",
|
|
12500
|
+
summary: "read a node's properties, and a TileMapLayer's or TileMap's cells as set_tilemap_cells takes them, the first 1000 with cellCount for all. An instance of another scene also answers instanceOf and editableChildren, as scene_tree does",
|
|
12485
12501
|
requires: ["nodePath"]
|
|
12486
12502
|
},
|
|
12487
12503
|
set: {
|
|
12488
12504
|
effect: "destructive",
|
|
12489
|
-
summary: "set properties on a node",
|
|
12490
|
-
requires: ["nodePath"
|
|
12505
|
+
summary: "set properties on a node, or make the children of an instance editable; give either or both",
|
|
12506
|
+
requires: ["nodePath"]
|
|
12491
12507
|
},
|
|
12492
12508
|
duplicate: {
|
|
12493
12509
|
effect: "additive",
|
|
12494
|
-
summary: "copy a node and its children",
|
|
12510
|
+
summary: "copy a node and its children, beside it or under parentNodePath, answering the copy's newNodePath, parentNodePath and index",
|
|
12495
12511
|
requires: ["nodePath", "newName"]
|
|
12496
12512
|
},
|
|
12497
12513
|
reparent: {
|
|
@@ -12766,7 +12782,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12766
12782
|
scriptPath: {
|
|
12767
12783
|
type: ["string", "array"],
|
|
12768
12784
|
items: { type: "string" },
|
|
12769
|
-
description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered
|
|
12785
|
+
description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered on its own. Several are answered together, with clean, errors and warnings for all of them: a script with nothing to report is named in nothingReported, and every other is answered under scripts in that same shape, including a script that could not be read, named with why while the rest are still asked about. Any number of scripts is one call, which reports progress as it goes.'
|
|
12770
12786
|
}
|
|
12771
12787
|
},
|
|
12772
12788
|
requires: ["projectPath", "scriptPath"]
|
|
@@ -12868,7 +12884,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12868
12884
|
},
|
|
12869
12885
|
move: {
|
|
12870
12886
|
effect: "destructive",
|
|
12871
|
-
summary: "move or rename a file or a directory, and every path in the project naming it",
|
|
12887
|
+
summary: "move or rename a file or a directory, and every path in the project naming it. With no editor an import pass then rebuilds the project's uid cache, so a reference by uid leads to the new path, and uidTable says whether it ran",
|
|
12872
12888
|
requires: ["newPath"]
|
|
12873
12889
|
}
|
|
12874
12890
|
}
|
|
@@ -12883,6 +12899,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
12883
12899
|
type: "boolean",
|
|
12884
12900
|
ops: ["open", "restart"],
|
|
12885
12901
|
description: "Open the editor headless and, on Windows, on a desktop of its own, so no window of it or of the games it plays appears. Default false: one visible editor, shown without taking the focus. On restart only for an editor a server opened; one opened by hand comes back as it was, so hidden is refused there."
|
|
12902
|
+
},
|
|
12903
|
+
silent: {
|
|
12904
|
+
type: "boolean",
|
|
12905
|
+
ops: ["open", "restart"],
|
|
12906
|
+
description: "Open the editor with no sound out of the machine, started with --audio-driver Dummy, so a tool script, a preview or a scene playing in its viewport is heard by nobody. Default true: pass false to hear it. The answer's arguments are what the editor was started with. The editor hands no audio driver to a game it plays, so editor_run's own silent decides that game's sound. A hidden editor is headless and silent either way. On restart only for an editor a server opened; one opened by hand comes back as it was."
|
|
12886
12907
|
}
|
|
12887
12908
|
},
|
|
12888
12909
|
requires: [],
|
|
@@ -12937,7 +12958,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12937
12958
|
silent: {
|
|
12938
12959
|
type: "boolean",
|
|
12939
12960
|
ops: ["start"],
|
|
12940
|
-
description: "start: run the game with no sound out of the machine, by starting the engine with --audio-driver Dummy: every stream still plays and every playback position still advances, so what is playing can still be asked. The editor takes no engine flags for a game it plays, so a silent windowed run is started by this server, whose keeper is its debugger rather than the editor. A headless run, check's included, is silent already. Default
|
|
12961
|
+
description: "start: run the game with no sound out of the machine, by starting the engine with --audio-driver Dummy: every stream still plays and every playback position still advances, so what is playing can still be asked. The editor takes no engine flags for a game it plays, so a silent windowed run is started by this server, whose keeper is its debugger rather than the editor. A headless run, check's included, is silent already, and so is a test run. Default true, for an exported build too: pass false to hear the game, which a windowed run with the editor connected then plays through the editor."
|
|
12941
12962
|
},
|
|
12942
12963
|
executable: {
|
|
12943
12964
|
type: "string",
|
|
@@ -13007,7 +13028,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13007
13028
|
graceful: {
|
|
13008
13029
|
type: "boolean",
|
|
13009
13030
|
ops: ["start", "stop"],
|
|
13010
|
-
description: "stop, and a start that ends the run going before it: ask the game to close first, the way a player's close asks it, so its own handling of a close runs, which a game keeping a crash mark needs to take the mark down. The runtime addon delivers the close request to every node and then the root window's close_requested, and a tree with auto_accept_quit on then quits. The game is given ten seconds to exit, GDHARNESS_CLOSE_WAIT_MS sets another, and is ended the ordinary way when it does not, or when it has no runtime listening,
|
|
13031
|
+
description: "stop, and a start that ends the run going before it: ask the game to close first, the way a player's close asks it, so its own handling of a close runs, which a game keeping a crash mark needs to take the mark down. The runtime addon delivers the close request to every node and then the root window's close_requested, and a tree with auto_accept_quit on then quits. The game is given ten seconds to exit, GDHARNESS_CLOSE_WAIT_MS sets another, and is ended the ordinary way when it does not, or when it has no runtime listening, refuses the close, or does not answer within three seconds, as a game held at a breakpoint does not; notClosedBecause (previousRunNotClosedBecause on a start) then says which. Default true; false ends the process at once."
|
|
13011
13032
|
},
|
|
13012
13033
|
pid: {
|
|
13013
13034
|
type: "integer",
|
|
@@ -13044,7 +13065,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13044
13065
|
requires: []
|
|
13045
13066
|
}
|
|
13046
13067
|
},
|
|
13047
|
-
answer: "start: through is gdharness for a game this server started itself, and editor for one the editor plays. scene is the scene the game started in, the project's main scene when none was given. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
|
|
13068
|
+
answer: "start: through is gdharness for a game this server started itself, and editor for one the editor plays. scene is the scene the game started in, the project's main scene when none was given. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. start and check: " + IMPORTED_FIRST + " An exported build carries its own imports and is not asked. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
|
|
13048
13069
|
defaultOperation: "start"
|
|
13049
13070
|
},
|
|
13050
13071
|
{
|
|
@@ -13106,8 +13127,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
13106
13127
|
name: "editor_status",
|
|
13107
13128
|
title: "Editor status",
|
|
13108
13129
|
effect: "read",
|
|
13109
|
-
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 editorCannotConnect, 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
|
|
13110
|
-
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. Its play says which play the editor is on: play.play counts the plays that editor has started since it opened, whoever started them, and play.editorPid is the editor, so two reads with different pairs are two plays even when both say playing. 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 the gdharness upgrade command, spelled out to run in a shell, 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.
|
|
13130
|
+
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 editorCannotConnect, 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 a connected editor and on each running game (absent with neither), says it runs addon code other than what this server ships, and staleNote says which half moves: the project, by a gdharness upgrade it spells out to run in a shell, the editor by a restart, or this server by a reconnect. 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.",
|
|
13131
|
+
answer: "projectIs, when present, is the gdharness version of the addons in the project, which differs from this server's (project_info says which of the two to move), and configIs, when present, the version the project's MCP config starts, which differs from this server's, so the next connection runs that one. 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. Its play says which play the editor is on: play.play counts the plays that editor has started since it opened, whoever started them, and play.editorPid is the editor, so two reads with different pairs are two plays even when both say playing. 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 the gdharness upgrade command, spelled out to run in a shell, 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. A runtime_* call to such a game is refused with the staleNote and not sent. 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 editorCannotConnect is a project without the gdharness editor addon installed or enabled, where no editor can ever dial in, however young the server or whichever editor is open; its remedies say how to install or enable it. 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. A stale editor is asked nothing but to quit: every other call to it is refused with staleNote, which names editor_launch restart, the upgrade command or a reconnect. 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. serverVersion is the gdharness version of this server. bridgeAvailable says the editor bridge is listening; when it failed to start, startupError gives the error, and retryingBridge is true while this server tries again. announcedAt is the file where this server told editors of its project which port to dial. connectedAt is when the editor connected, and lastPongAt when it last answered this server's keep-alive ping. addonDigest is a hash of the editor addon's code, which addonIsStale compares with what this server ships. debugPort is the port of the editor's remote debugger, the one games it plays connect to. openedByAServer says a gdharness server started this editor, and syncsBreakpoints that it keeps its breakpoints when a debug session opens, which Godot otherwise clears. pendingRequests counts the requests sent to the editor and not yet answered, and queuedResources the files with writes waiting behind another write to the same file.",
|
|
13111
13132
|
parameters: {},
|
|
13112
13133
|
requires: []
|
|
13113
13134
|
},
|
|
@@ -13388,7 +13409,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13388
13409
|
viewportPath: {
|
|
13389
13410
|
type: "string",
|
|
13390
13411
|
ops: ["viewport"],
|
|
13391
|
-
description: "viewport: the Viewport node. Default the root viewport."
|
|
13412
|
+
description: "viewport: the Viewport node. Default the root viewport. A Window of the game that is hidden or minimised has nothing drawn to it, and is refused naming which."
|
|
13392
13413
|
},
|
|
13393
13414
|
width: {
|
|
13394
13415
|
type: "integer",
|
|
@@ -13694,7 +13715,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13694
13715
|
{
|
|
13695
13716
|
name: "debug_breakpoint",
|
|
13696
13717
|
title: "Breakpoints",
|
|
13697
|
-
description: "Sets or removes a breakpoint. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it at once (runningGame says so). The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor
|
|
13718
|
+
description: "Sets or removes a breakpoint. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it at once (runningGame says so). The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor where it was turned off again is said so under breakpointsAtRisk, here and in editor_status.",
|
|
13698
13719
|
parameters: {
|
|
13699
13720
|
projectPath: PROJECT_PATH,
|
|
13700
13721
|
scriptPath: SCRIPT_PATH,
|
|
@@ -35642,6 +35663,10 @@ function parseAnnouncement(file, pid) {
|
|
|
35642
35663
|
}
|
|
35643
35664
|
const editorPid = readNumber(fields, "editor_pid");
|
|
35644
35665
|
const addonVersion = readString(fields, "addon_version");
|
|
35666
|
+
const addonDigest = readString(fields, "addon_digest");
|
|
35667
|
+
if (addonVersion === undefined || addonDigest === undefined) {
|
|
35668
|
+
return { kind: "rubbish" };
|
|
35669
|
+
}
|
|
35645
35670
|
return {
|
|
35646
35671
|
kind: "runtime",
|
|
35647
35672
|
endpoint: {
|
|
@@ -35652,7 +35677,7 @@ function parseAnnouncement(file, pid) {
|
|
|
35652
35677
|
file,
|
|
35653
35678
|
...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {},
|
|
35654
35679
|
...when,
|
|
35655
|
-
|
|
35680
|
+
addon: { version: addonVersion, digest: addonDigest }
|
|
35656
35681
|
}
|
|
35657
35682
|
};
|
|
35658
35683
|
}
|
|
@@ -35943,6 +35968,22 @@ function keepLate(id, answer) {
|
|
|
35943
35968
|
function lateAnswer(id) {
|
|
35944
35969
|
return lateAnswers.get(id);
|
|
35945
35970
|
}
|
|
35971
|
+
function notSent(game, command, timeoutMs, refusals, dialling) {
|
|
35972
|
+
if (refusals > 0) {
|
|
35973
|
+
const refused = dialling ? `it refused ${refusals === 1 ? "one connection" : `${refusals} connections`} and had not accepted the next when the wait ended, after ${timeoutMs}ms` : `it refused ${refusals === 1 ? "the connection" : `all ${refusals} connections tried`} within ${timeoutMs}ms`;
|
|
35974
|
+
return {
|
|
35975
|
+
ok: false,
|
|
35976
|
+
reason: "refused",
|
|
35977
|
+
message: `The game (${game}) is running but nothing answered on its port: ${refused}. Nothing listens there while a game is starting or shutting down, and the engine refuses connections while more are waiting for the game than the eight it queues, which several servers calling one game at once, or a long frame or a breakpoint holding it, can make it do. Nothing was sent, so nothing ran.`
|
|
35978
|
+
};
|
|
35979
|
+
}
|
|
35980
|
+
const why = dialling ? "its port had not accepted the connection, which is how Linux and macOS hold one while the game's queue is full, as it is when the game has not taken the connections before it during a long frame or at a breakpoint" : `${UNTAKEN_PER_GAME} connections from this server were already waiting for the game to take them, which it does between frames, so a long frame or a breakpoint holds them`;
|
|
35981
|
+
return {
|
|
35982
|
+
ok: false,
|
|
35983
|
+
reason: "busy",
|
|
35984
|
+
message: `'${command}' was not sent to the game (${game}) within ${timeoutMs}ms: ${why}. Nothing was sent, so nothing ran.`
|
|
35985
|
+
};
|
|
35986
|
+
}
|
|
35946
35987
|
function placeOf(endpoint) {
|
|
35947
35988
|
return `${dialAddress(endpoint.address)}:${endpoint.port}`;
|
|
35948
35989
|
}
|
|
@@ -35981,6 +36022,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, afterTheWait = "dr
|
|
|
35981
36022
|
let buffered = "";
|
|
35982
36023
|
let welcomed = false;
|
|
35983
36024
|
let socket = null;
|
|
36025
|
+
let connected = false;
|
|
35984
36026
|
let refusals = 0;
|
|
35985
36027
|
let redial = null;
|
|
35986
36028
|
let giveBackThePlace = null;
|
|
@@ -36017,16 +36059,8 @@ function runtimeRequest(endpoint, command, params, timeoutMs, afterTheWait = "dr
|
|
|
36017
36059
|
settle(reply);
|
|
36018
36060
|
};
|
|
36019
36061
|
const timer = setTimeout(() => {
|
|
36020
|
-
if (socket === null) {
|
|
36021
|
-
finish(refusals
|
|
36022
|
-
ok: false,
|
|
36023
|
-
reason: "refused",
|
|
36024
|
-
message: `The game (${describe2(endpoint)}) is running but nothing answered on its port: it refused ${refusals === 1 ? "the connection" : `all ${refusals} connections tried`} within ${timeoutMs}ms. Nothing listens there while a game is starting or shutting down, and the engine refuses connections while more are waiting for the game than the eight it queues, which several servers calling one game at once, or a long frame or a breakpoint holding it, can make it do. Nothing was sent, so nothing ran.`
|
|
36025
|
-
} : {
|
|
36026
|
-
ok: false,
|
|
36027
|
-
reason: "busy",
|
|
36028
|
-
message: `'${command}' was not sent to the game (${describe2(endpoint)}) within ${timeoutMs}ms: ${UNTAKEN_PER_GAME} connections from this server were already waiting for the game to take them, which it does between frames, so a long frame or a breakpoint holds them. Nothing was sent, so nothing ran.`
|
|
36029
|
-
});
|
|
36062
|
+
if (socket === null || !connected) {
|
|
36063
|
+
finish(notSent(describe2(endpoint), command, timeoutMs, refusals, socket !== null));
|
|
36030
36064
|
return;
|
|
36031
36065
|
}
|
|
36032
36066
|
const still = welcomed ? `The game (${describe2(endpoint)}) has the request and did not answer '${command}' within ${timeoutMs}ms.` : `The game (${describe2(endpoint)}) did not answer '${command}' within ${timeoutMs}ms and had not taken the request: its frame has not ended since it was sent, which a long frame or a breakpoint does, and the request runs when it does.`;
|
|
@@ -36073,6 +36107,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, afterTheWait = "dr
|
|
|
36073
36107
|
socket = opened;
|
|
36074
36108
|
opened.setEncoding("utf8");
|
|
36075
36109
|
opened.on("connect", () => {
|
|
36110
|
+
connected = true;
|
|
36076
36111
|
opened.write(`${JSON.stringify({ id, command, params })}
|
|
36077
36112
|
`);
|
|
36078
36113
|
});
|
|
@@ -36097,18 +36132,16 @@ function runtimeRequest(endpoint, command, params, timeoutMs, afterTheWait = "dr
|
|
|
36097
36132
|
if (!welcomed) {
|
|
36098
36133
|
welcomed = true;
|
|
36099
36134
|
taken();
|
|
36100
|
-
|
|
36101
|
-
|
|
36102
|
-
|
|
36103
|
-
|
|
36104
|
-
|
|
36105
|
-
|
|
36106
|
-
|
|
36107
|
-
|
|
36108
|
-
return;
|
|
36109
|
-
}
|
|
36110
|
-
continue;
|
|
36135
|
+
const spoken = readString(message, "type") === "welcome" ? readNumber(message, "protocol") : undefined;
|
|
36136
|
+
if (spoken !== RUNTIME_PROTOCOL) {
|
|
36137
|
+
finish({
|
|
36138
|
+
ok: false,
|
|
36139
|
+
reason: "protocol",
|
|
36140
|
+
message: `The game (${describe2(endpoint)}) speaks runtime protocol ${spoken ?? "unknown"} and this server speaks ${RUNTIME_PROTOCOL}. Reinstall the runtime addon into the project.`
|
|
36141
|
+
});
|
|
36142
|
+
return;
|
|
36111
36143
|
}
|
|
36144
|
+
continue;
|
|
36112
36145
|
}
|
|
36113
36146
|
if (message["id"] !== id) {
|
|
36114
36147
|
continue;
|
|
@@ -36173,7 +36206,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, afterTheWait = "dr
|
|
|
36173
36206
|
});
|
|
36174
36207
|
});
|
|
36175
36208
|
}
|
|
36176
|
-
var RUNTIME_PROTOCOL =
|
|
36209
|
+
var RUNTIME_PROTOCOL = 3, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, CONSOLE_BOUNDARY = "[gdharness] runtime listening on ", ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, judging = null, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1, LATE_ANSWER_MS, LATE_KEPT = 50, lateAnswers, UNTAKEN_PER_GAME = 4, REDIAL_MS = 50, untaken;
|
|
36177
36210
|
var init_runtime_client = __esm(() => {
|
|
36178
36211
|
init_alive();
|
|
36179
36212
|
init_launch();
|
|
@@ -36839,8 +36872,8 @@ class GodotDAPClient {
|
|
|
36839
36872
|
}
|
|
36840
36873
|
const response = await this.sendRequest("initialize", {
|
|
36841
36874
|
adapterID: "godot",
|
|
36842
|
-
clientID: "
|
|
36843
|
-
clientName: "
|
|
36875
|
+
clientID: "gdharness",
|
|
36876
|
+
clientName: "gdharness",
|
|
36844
36877
|
locale: "en",
|
|
36845
36878
|
linesStartAt1: true,
|
|
36846
36879
|
columnsStartAt1: true,
|
|
@@ -37547,8 +37580,8 @@ function theConsoleWasNotCaptured(why, projectPath) {
|
|
|
37547
37580
|
if (why.kind === "not ours") {
|
|
37548
37581
|
return "This editor was not opened by gdharness, so its console was never captured and nothing " + "here has it: an editor prints to a console no plugin can read, and the addon cannot get " + "at it from inside. This is not an editor that printed nothing.";
|
|
37549
37582
|
}
|
|
37550
|
-
if (why.kind === "
|
|
37551
|
-
return `This editor was opened by a gdharness server, but the console beside ${projectPath} belongs ` + `to editor ${why.notedPid ?? "none recorded"} rather than to this one, so it holds an ` + "earlier session and is not offered as this one.
|
|
37583
|
+
if (why.kind === "another editor") {
|
|
37584
|
+
return `This editor was opened by a gdharness server, but the console beside ${projectPath} belongs ` + `to editor ${why.notedPid ?? "none recorded"} rather than to this one, so it holds an ` + "earlier session and is not offered as this one.";
|
|
37552
37585
|
}
|
|
37553
37586
|
return "This editor was opened by gdharness and has written nothing to its console file yet, not " + "even the engine version it prints first, so the file is missing rather than empty. An " + "editor that could not open the file for writing leaves this.";
|
|
37554
37587
|
}
|
|
@@ -37558,7 +37591,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
|
|
|
37558
37591
|
}
|
|
37559
37592
|
const noted = readEditorLogNote(projectPath);
|
|
37560
37593
|
if (noted === null || editorPid === null || noted.pid !== editorPid) {
|
|
37561
|
-
return { kind: "
|
|
37594
|
+
return { kind: "another editor", notedPid: noted?.pid ?? null };
|
|
37562
37595
|
}
|
|
37563
37596
|
const path = editorLogPath(projectPath);
|
|
37564
37597
|
if (!existsSync20(path)) {
|
|
@@ -43121,8 +43154,11 @@ function resolveDefaultBridgeHost() {
|
|
|
43121
43154
|
const host = process.env["GDHARNESS_BRIDGE_HOST"]?.trim();
|
|
43122
43155
|
return host === undefined || host === "" ? DEFAULT_HOST : host;
|
|
43123
43156
|
}
|
|
43157
|
+
function greetingFaults(message) {
|
|
43158
|
+
return Object.entries(GREETING_FIELDS).filter(([field, type]) => typeof message[field] !== type).map(([field]) => field);
|
|
43159
|
+
}
|
|
43124
43160
|
function servedPort(said) {
|
|
43125
|
-
return
|
|
43161
|
+
return Number.isInteger(said) && said > 0 && said <= 65535 ? said : undefined;
|
|
43126
43162
|
}
|
|
43127
43163
|
function mayYetConnect(listeningSince, now = Date.now()) {
|
|
43128
43164
|
return listeningSince === undefined || now - listeningSince.getTime() < CONNECT_WINDOW_MS;
|
|
@@ -43150,13 +43186,27 @@ function getDefaultBridge(ownProject = null) {
|
|
|
43150
43186
|
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
|
|
43151
43187
|
return defaultBridge;
|
|
43152
43188
|
}
|
|
43153
|
-
var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, MOST_CLOSE_REASON_BYTES = 123, OTHER_PROJECT_CLOSE_CODE = 4001, ONE_SHOT_CLOSE_CODE = 4002, CONNECT_WINDOW_MS = 35000, BridgeTimeout, EditorBusy, BUSY_RETRY_MS = 100, GodotBridge, defaultBridge = null;
|
|
43189
|
+
var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, MOST_CLOSE_REASON_BYTES = 123, OTHER_PROJECT_CLOSE_CODE = 4001, ONE_SHOT_CLOSE_CODE = 4002, GREETING_FIELDS, CONNECT_WINDOW_MS = 35000, BridgeTimeout, AddonOfAnotherRelease, ASKED_OF_ANY_RELEASE, EditorBusy, BUSY_RETRY_MS = 100, GodotBridge, defaultBridge = null;
|
|
43154
43190
|
var init_godot_bridge = __esm(() => {
|
|
43155
43191
|
init_wrapper();
|
|
43156
43192
|
init_errors();
|
|
43157
43193
|
init_launch();
|
|
43158
43194
|
init_paths();
|
|
43159
43195
|
init_ports();
|
|
43196
|
+
init_server_version();
|
|
43197
|
+
init_setup();
|
|
43198
|
+
GREETING_FIELDS = {
|
|
43199
|
+
project_path: "string",
|
|
43200
|
+
addon_version: "string",
|
|
43201
|
+
addon_digest: "string",
|
|
43202
|
+
editor_pid: "number",
|
|
43203
|
+
lsp_port: "number",
|
|
43204
|
+
dap_port: "number",
|
|
43205
|
+
debug_port: "number",
|
|
43206
|
+
syncs_breakpoints: "boolean",
|
|
43207
|
+
opened_by_a_server: "boolean",
|
|
43208
|
+
started_by_an_editor: "boolean"
|
|
43209
|
+
};
|
|
43160
43210
|
BridgeTimeout = class BridgeTimeout extends Error {
|
|
43161
43211
|
toolName;
|
|
43162
43212
|
waitedMs;
|
|
@@ -43166,6 +43216,9 @@ var init_godot_bridge = __esm(() => {
|
|
|
43166
43216
|
this.waitedMs = waitedMs;
|
|
43167
43217
|
}
|
|
43168
43218
|
};
|
|
43219
|
+
AddonOfAnotherRelease = class AddonOfAnotherRelease extends Error {
|
|
43220
|
+
};
|
|
43221
|
+
ASKED_OF_ANY_RELEASE = new Set(["quit_editor", "restart_editor"]);
|
|
43169
43222
|
EditorBusy = class EditorBusy extends Error {
|
|
43170
43223
|
constructor(toolName) {
|
|
43171
43224
|
super(`The editor was scanning or importing its files, so ${toolName} was not run.`);
|
|
@@ -43340,6 +43393,14 @@ var init_godot_bridge = __esm(() => {
|
|
|
43340
43393
|
portWanted: this.wantedPort !== 0 && this.boundPort !== null && this.boundPort !== this.wantedPort ? this.wantedPort : undefined
|
|
43341
43394
|
};
|
|
43342
43395
|
}
|
|
43396
|
+
anotherRelease() {
|
|
43397
|
+
const info = this.connectionInfo;
|
|
43398
|
+
if (info?.greeted !== true || info.addonVersion === undefined || info.addonDigest === undefined) {
|
|
43399
|
+
return;
|
|
43400
|
+
}
|
|
43401
|
+
const project = info.projectPath;
|
|
43402
|
+
return addonMismatch(info.addonVersion, SERVER_VERSION, info.addonDigest, shippedEditorDigest(), project === undefined || project === "" ? undefined : installedEditorDigest(project));
|
|
43403
|
+
}
|
|
43343
43404
|
invokeTool(toolName, args, options = {}) {
|
|
43344
43405
|
const counted = options.counted ?? true;
|
|
43345
43406
|
const resourceKey = this.getResourceKey(args);
|
|
@@ -43487,24 +43548,33 @@ var init_godot_bridge = __esm(() => {
|
|
|
43487
43548
|
case "godot_ready":
|
|
43488
43549
|
if (this.connectionInfo) {
|
|
43489
43550
|
if (this.belongsElsewhere(message.project_path)) {
|
|
43490
|
-
this.elsewhereUnlessOneShot(message.project_path, message.editor_pid, message.opened_by_a_server
|
|
43551
|
+
this.elsewhereUnlessOneShot(message.project_path, message.editor_pid, message.opened_by_a_server);
|
|
43491
43552
|
return;
|
|
43492
43553
|
}
|
|
43493
43554
|
this.connectionInfo.projectPath = withoutTrailingSeparator(message.project_path);
|
|
43494
|
-
|
|
43555
|
+
const faults = greetingFaults(message);
|
|
43556
|
+
if (faults.length > 0) {
|
|
43557
|
+
this.log("info", `Godot ready from another release's addon, which sent no ${faults.join(", ")}`);
|
|
43558
|
+
this.connectionInfo.addonVersion = typeof message.addon_version === "string" ? message.addon_version : "";
|
|
43559
|
+
this.connectionInfo.addonDigest = "";
|
|
43560
|
+
this.connectionInfo.editorPid = typeof message.editor_pid === "number" ? message.editor_pid : undefined;
|
|
43561
|
+
this.connectionInfo.greeted = true;
|
|
43562
|
+
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
43563
|
+
return;
|
|
43564
|
+
}
|
|
43565
|
+
this.connectionInfo.addonVersion = message.addon_version;
|
|
43495
43566
|
this.connectionInfo.addonDigest = message.addon_digest;
|
|
43496
43567
|
this.connectionInfo.editorPid = message.editor_pid;
|
|
43497
43568
|
this.connectionInfo.lspPort = servedPort(message.lsp_port);
|
|
43498
43569
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
43499
43570
|
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
43500
|
-
this.connectionInfo.openedByAServer = message.opened_by_a_server
|
|
43501
|
-
this.connectionInfo.startedByAnEditor =
|
|
43502
|
-
this.connectionInfo.syncsBreakpoints =
|
|
43571
|
+
this.connectionInfo.openedByAServer = message.opened_by_a_server;
|
|
43572
|
+
this.connectionInfo.startedByAnEditor = message.started_by_an_editor;
|
|
43573
|
+
this.connectionInfo.syncsBreakpoints = message.syncs_breakpoints;
|
|
43503
43574
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
43504
43575
|
this.connectionInfo.greeted = true;
|
|
43505
|
-
|
|
43506
|
-
|
|
43507
|
-
this.identify(pid);
|
|
43576
|
+
if (!message.opened_by_a_server && this.oneShotReader !== null) {
|
|
43577
|
+
this.identify(message.editor_pid);
|
|
43508
43578
|
return;
|
|
43509
43579
|
}
|
|
43510
43580
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
@@ -43521,6 +43591,10 @@ var init_godot_bridge = __esm(() => {
|
|
|
43521
43591
|
if (!this.isConnected()) {
|
|
43522
43592
|
return Promise.reject(new Error("Godot is not connected"));
|
|
43523
43593
|
}
|
|
43594
|
+
const otherRelease = ASKED_OF_ANY_RELEASE.has(toolName) ? undefined : this.anotherRelease();
|
|
43595
|
+
if (otherRelease !== undefined) {
|
|
43596
|
+
return Promise.reject(new AddonOfAnotherRelease(otherRelease));
|
|
43597
|
+
}
|
|
43524
43598
|
const requestId = randomUUID();
|
|
43525
43599
|
const message = {
|
|
43526
43600
|
type: "tool_invoke",
|
|
@@ -43743,6 +43817,168 @@ var init_godot_bridge = __esm(() => {
|
|
|
43743
43817
|
};
|
|
43744
43818
|
});
|
|
43745
43819
|
|
|
43820
|
+
// src/import-freshness.ts
|
|
43821
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
43822
|
+
import { existsSync as existsSync25, readdirSync as readdirSync13, readFileSync as readFileSync24, statSync as statSync12 } from "node:fs";
|
|
43823
|
+
import { join as join29 } from "node:path";
|
|
43824
|
+
function md5Of(file) {
|
|
43825
|
+
let stats;
|
|
43826
|
+
try {
|
|
43827
|
+
stats = statSync12(file);
|
|
43828
|
+
} catch {
|
|
43829
|
+
return null;
|
|
43830
|
+
}
|
|
43831
|
+
const known = hashes.get(file);
|
|
43832
|
+
if (known !== undefined && known.size === stats.size && known.mtimeMs === stats.mtimeMs) {
|
|
43833
|
+
return known.md5;
|
|
43834
|
+
}
|
|
43835
|
+
let md5;
|
|
43836
|
+
try {
|
|
43837
|
+
md5 = createHash5("md5").update(readFileSync24(file)).digest("hex");
|
|
43838
|
+
} catch {
|
|
43839
|
+
return null;
|
|
43840
|
+
}
|
|
43841
|
+
hashes.set(file, { size: stats.size, mtimeMs: stats.mtimeMs, md5 });
|
|
43842
|
+
return md5;
|
|
43843
|
+
}
|
|
43844
|
+
function sectionOf(text, section) {
|
|
43845
|
+
const keys = new Map;
|
|
43846
|
+
let inside = false;
|
|
43847
|
+
for (const line of text.split(/\r?\n/)) {
|
|
43848
|
+
const header = /^\[([^\]]+)\]\s*$/.exec(line);
|
|
43849
|
+
if (header !== null) {
|
|
43850
|
+
inside = header[1] === section;
|
|
43851
|
+
continue;
|
|
43852
|
+
}
|
|
43853
|
+
const pair = inside ? /^([A-Za-z0-9_./]+)=(.*)$/.exec(line) : null;
|
|
43854
|
+
if (pair?.[1] !== undefined && pair[2] !== undefined) {
|
|
43855
|
+
keys.set(pair[1], pair[2]);
|
|
43856
|
+
}
|
|
43857
|
+
}
|
|
43858
|
+
return keys;
|
|
43859
|
+
}
|
|
43860
|
+
function unquoted(value) {
|
|
43861
|
+
return value === undefined ? "" : value.replace(/^"(.*)"$/, "$1");
|
|
43862
|
+
}
|
|
43863
|
+
function listed2(value) {
|
|
43864
|
+
return [...(value ?? "").matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1] ?? "");
|
|
43865
|
+
}
|
|
43866
|
+
function onDisk(projectPath, resPath) {
|
|
43867
|
+
return join29(projectPath, resPath.slice("res://".length));
|
|
43868
|
+
}
|
|
43869
|
+
function behind(projectPath, resPath, sidecar) {
|
|
43870
|
+
const remap = sectionOf(sidecar, "remap");
|
|
43871
|
+
const importer = unquoted(remap.get("importer"));
|
|
43872
|
+
if (importer === "keep" || importer === "skip") {
|
|
43873
|
+
return null;
|
|
43874
|
+
}
|
|
43875
|
+
const outputs = listed2(sectionOf(sidecar, "deps").get("dest_files"));
|
|
43876
|
+
if (outputs.some((output) => !existsSync25(onDisk(projectPath, output)))) {
|
|
43877
|
+
return "its imported output is not on disk";
|
|
43878
|
+
}
|
|
43879
|
+
const name = resPath.slice(resPath.lastIndexOf("/") + 1);
|
|
43880
|
+
const record = join29(projectPath, ".godot", "imported", `${name}-${createHash5("md5").update(resPath, "utf8").digest("hex")}.md5`);
|
|
43881
|
+
let recorded = "";
|
|
43882
|
+
try {
|
|
43883
|
+
recorded = /^source_md5="([0-9a-f]*)"/m.exec(readFileSync24(record, "utf8"))?.[1] ?? "";
|
|
43884
|
+
} catch {
|
|
43885
|
+
recorded = "";
|
|
43886
|
+
}
|
|
43887
|
+
if (recorded === "") {
|
|
43888
|
+
return "its import recorded no hash of its source";
|
|
43889
|
+
}
|
|
43890
|
+
const current = md5Of(onDisk(projectPath, resPath));
|
|
43891
|
+
return current === null || current === recorded ? null : "the source changed after it was imported";
|
|
43892
|
+
}
|
|
43893
|
+
function staleImports(projectPath) {
|
|
43894
|
+
const stale = [];
|
|
43895
|
+
const visit = (directory, resDirectory) => {
|
|
43896
|
+
if (existsSync25(join29(directory, ".gdignore")) || existsSync25(join29(directory, VERSION_MARKER))) {
|
|
43897
|
+
return;
|
|
43898
|
+
}
|
|
43899
|
+
let entries;
|
|
43900
|
+
try {
|
|
43901
|
+
entries = readdirSync13(directory, { withFileTypes: true });
|
|
43902
|
+
} catch {
|
|
43903
|
+
return;
|
|
43904
|
+
}
|
|
43905
|
+
const names = new Set(entries.map((entry) => entry.name));
|
|
43906
|
+
for (const entry of entries) {
|
|
43907
|
+
if (entry.name.startsWith(".")) {
|
|
43908
|
+
continue;
|
|
43909
|
+
}
|
|
43910
|
+
const resPath = `${resDirectory}${entry.name}`;
|
|
43911
|
+
if (entry.isDirectory()) {
|
|
43912
|
+
visit(join29(directory, entry.name), `${resPath}/`);
|
|
43913
|
+
continue;
|
|
43914
|
+
}
|
|
43915
|
+
if (!entry.isFile()) {
|
|
43916
|
+
continue;
|
|
43917
|
+
}
|
|
43918
|
+
if (entry.name.endsWith(".import")) {
|
|
43919
|
+
const source = resPath.slice(0, -".import".length);
|
|
43920
|
+
if (!names.has(entry.name.slice(0, -".import".length))) {
|
|
43921
|
+
continue;
|
|
43922
|
+
}
|
|
43923
|
+
let sidecar;
|
|
43924
|
+
try {
|
|
43925
|
+
sidecar = readFileSync24(join29(directory, entry.name), "utf8");
|
|
43926
|
+
} catch {
|
|
43927
|
+
continue;
|
|
43928
|
+
}
|
|
43929
|
+
const reason = behind(projectPath, source, sidecar);
|
|
43930
|
+
if (reason !== null) {
|
|
43931
|
+
stale.push({ path: source, reason });
|
|
43932
|
+
}
|
|
43933
|
+
continue;
|
|
43934
|
+
}
|
|
43935
|
+
const extension = entry.name.slice(entry.name.lastIndexOf(".") + 1).toLowerCase();
|
|
43936
|
+
if (IMPORTABLE.has(extension) && !names.has(`${entry.name}.import`)) {
|
|
43937
|
+
stale.push({ path: resPath, reason: "it has not been imported" });
|
|
43938
|
+
}
|
|
43939
|
+
}
|
|
43940
|
+
};
|
|
43941
|
+
visit(projectPath, "res://");
|
|
43942
|
+
return stale.sort((one, other) => one.path.localeCompare(other.path));
|
|
43943
|
+
}
|
|
43944
|
+
var IMPORTABLE_EXTENSIONS, IMPORTABLE, hashes;
|
|
43945
|
+
var init_import_freshness = __esm(() => {
|
|
43946
|
+
init_project_scan();
|
|
43947
|
+
IMPORTABLE_EXTENSIONS = [
|
|
43948
|
+
"png",
|
|
43949
|
+
"jpg",
|
|
43950
|
+
"jpeg",
|
|
43951
|
+
"webp",
|
|
43952
|
+
"svg",
|
|
43953
|
+
"bmp",
|
|
43954
|
+
"tga",
|
|
43955
|
+
"exr",
|
|
43956
|
+
"hdr",
|
|
43957
|
+
"dds",
|
|
43958
|
+
"ktx",
|
|
43959
|
+
"wav",
|
|
43960
|
+
"mp3",
|
|
43961
|
+
"ogg",
|
|
43962
|
+
"ttf",
|
|
43963
|
+
"otf",
|
|
43964
|
+
"ttc",
|
|
43965
|
+
"otc",
|
|
43966
|
+
"woff",
|
|
43967
|
+
"woff2",
|
|
43968
|
+
"pfb",
|
|
43969
|
+
"pfm",
|
|
43970
|
+
"fnt",
|
|
43971
|
+
"font",
|
|
43972
|
+
"glb",
|
|
43973
|
+
"gltf",
|
|
43974
|
+
"fbx",
|
|
43975
|
+
"obj",
|
|
43976
|
+
"dae"
|
|
43977
|
+
];
|
|
43978
|
+
IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
|
|
43979
|
+
hashes = new Map;
|
|
43980
|
+
});
|
|
43981
|
+
|
|
43746
43982
|
// src/keeper-channel.ts
|
|
43747
43983
|
import { connect as connect2, createServer as createServer2 } from "node:net";
|
|
43748
43984
|
async function askTheKeeperToStop(address, withinMs) {
|
|
@@ -43757,9 +43993,6 @@ async function askTheKeepersDebugger(address, request, withinMs) {
|
|
|
43757
43993
|
if (typeof answered !== "string") {
|
|
43758
43994
|
return answered;
|
|
43759
43995
|
}
|
|
43760
|
-
if (answered === "unknown") {
|
|
43761
|
-
return { unreached: "its keeper was started by a gdharness from before game debuggers, and has none" };
|
|
43762
|
-
}
|
|
43763
43996
|
try {
|
|
43764
43997
|
const parsed = JSON.parse(answered);
|
|
43765
43998
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
@@ -43809,17 +44042,17 @@ var DEBUG_PREFIX = "debug ";
|
|
|
43809
44042
|
var init_keeper_channel = () => {};
|
|
43810
44043
|
|
|
43811
44044
|
// src/lifecycle-log.ts
|
|
43812
|
-
import { appendFileSync, mkdirSync as mkdirSync14, renameSync as renameSync5, statSync as
|
|
43813
|
-
import { dirname as dirname20, join as
|
|
44045
|
+
import { appendFileSync, mkdirSync as mkdirSync14, renameSync as renameSync5, statSync as statSync13 } from "node:fs";
|
|
44046
|
+
import { dirname as dirname20, join as join30 } from "node:path";
|
|
43814
44047
|
import process11 from "node:process";
|
|
43815
44048
|
function lifecycleLogPath() {
|
|
43816
|
-
return
|
|
44049
|
+
return join30(runtimeDirectory(), "servers.log");
|
|
43817
44050
|
}
|
|
43818
44051
|
function writeLifecycle(event, fields = {}) {
|
|
43819
44052
|
const path = lifecycleLogPath();
|
|
43820
44053
|
try {
|
|
43821
44054
|
mkdirSync14(dirname20(path), { recursive: true });
|
|
43822
|
-
if ((
|
|
44055
|
+
if ((statSync13(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
|
|
43823
44056
|
try {
|
|
43824
44057
|
renameSync5(path, `${path}.1`);
|
|
43825
44058
|
} catch {}
|
|
@@ -44133,10 +44366,10 @@ var init_prompts = __esm(() => {
|
|
|
44133
44366
|
});
|
|
44134
44367
|
|
|
44135
44368
|
// src/restart-note.ts
|
|
44136
|
-
import { mkdirSync as mkdirSync15, readFileSync as
|
|
44137
|
-
import { dirname as dirname21, join as
|
|
44369
|
+
import { mkdirSync as mkdirSync15, readFileSync as readFileSync25, rmSync as rmSync13, writeFileSync as writeFileSync17 } from "node:fs";
|
|
44370
|
+
import { dirname as dirname21, join as join31 } from "node:path";
|
|
44138
44371
|
function restartNotePath(projectPath) {
|
|
44139
|
-
return
|
|
44372
|
+
return join31(projectPath, ".godot", "gdharness-restart.json");
|
|
44140
44373
|
}
|
|
44141
44374
|
function noteRestartBegun(note) {
|
|
44142
44375
|
const path = restartNotePath(note.projectPath);
|
|
@@ -44150,7 +44383,7 @@ function noteRestartBegun(note) {
|
|
|
44150
44383
|
}
|
|
44151
44384
|
function restartOwed(projectPath) {
|
|
44152
44385
|
try {
|
|
44153
|
-
const parsed = JSON.parse(
|
|
44386
|
+
const parsed = JSON.parse(readFileSync25(restartNotePath(projectPath), "utf8"));
|
|
44154
44387
|
if (typeof parsed !== "object" || parsed === null) {
|
|
44155
44388
|
return null;
|
|
44156
44389
|
}
|
|
@@ -44175,18 +44408,18 @@ var init_restart_note = __esm(() => {
|
|
|
44175
44408
|
});
|
|
44176
44409
|
|
|
44177
44410
|
// src/run-record.ts
|
|
44178
|
-
import { createHash as
|
|
44179
|
-
import { mkdirSync as mkdirSync16, openSync as openSync3, readdirSync as
|
|
44180
|
-
import { basename as basename9, dirname as dirname22, join as
|
|
44411
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
44412
|
+
import { mkdirSync as mkdirSync16, openSync as openSync3, readdirSync as readdirSync14, readFileSync as readFileSync26, realpathSync as realpathSync6, rmSync as rmSync14, statSync as statSync14 } from "node:fs";
|
|
44413
|
+
import { basename as basename9, dirname as dirname22, join as join32, resolve as resolve7 } from "node:path";
|
|
44181
44414
|
function runsDirectory() {
|
|
44182
|
-
return
|
|
44415
|
+
return join32(runtimeDirectory(), "runs");
|
|
44183
44416
|
}
|
|
44184
44417
|
function realAsFarAsItExists(path) {
|
|
44185
44418
|
const missing = [];
|
|
44186
44419
|
let at = path;
|
|
44187
44420
|
for (;; ) {
|
|
44188
44421
|
try {
|
|
44189
|
-
return
|
|
44422
|
+
return join32(realpathSync6.native(at), ...missing);
|
|
44190
44423
|
} catch {
|
|
44191
44424
|
const parent = dirname22(at);
|
|
44192
44425
|
if (parent === at) {
|
|
@@ -44200,13 +44433,13 @@ function realAsFarAsItExists(path) {
|
|
|
44200
44433
|
function projectKey(projectPath) {
|
|
44201
44434
|
const real = realAsFarAsItExists(resolve7(projectPath)).replace(/[\\/]+$/, "");
|
|
44202
44435
|
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
44203
|
-
return
|
|
44436
|
+
return createHash6("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
44204
44437
|
}
|
|
44205
44438
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
44206
|
-
return
|
|
44439
|
+
return join32(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
44207
44440
|
}
|
|
44208
44441
|
function latestRunPath(servedBy, directory = runtimeDirectory()) {
|
|
44209
|
-
return
|
|
44442
|
+
return join32(directory, "runs", `latest-${projectKey(servedBy)}.json`);
|
|
44210
44443
|
}
|
|
44211
44444
|
function writeLatestRun(servedBy, latest) {
|
|
44212
44445
|
try {
|
|
@@ -44216,7 +44449,7 @@ function writeLatestRun(servedBy, latest) {
|
|
|
44216
44449
|
}
|
|
44217
44450
|
function readLatestRun(servedBy, directory = runtimeDirectory()) {
|
|
44218
44451
|
try {
|
|
44219
|
-
const fields = JSON.parse(
|
|
44452
|
+
const fields = JSON.parse(readFileSync26(latestRunPath(servedBy, directory), "utf8"));
|
|
44220
44453
|
if (typeof fields !== "object" || fields === null) {
|
|
44221
44454
|
return null;
|
|
44222
44455
|
}
|
|
@@ -44231,20 +44464,20 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
|
|
|
44231
44464
|
}
|
|
44232
44465
|
function openTranscript(startedAt) {
|
|
44233
44466
|
mkdirSync16(runsDirectory(), { recursive: true });
|
|
44234
|
-
const path =
|
|
44467
|
+
const path = join32(runsDirectory(), `run-${startedAt}.log`);
|
|
44235
44468
|
return { path, fd: openSync3(path, "a") };
|
|
44236
44469
|
}
|
|
44237
44470
|
function profilePath(startedAt) {
|
|
44238
|
-
return
|
|
44471
|
+
return join32(runsDirectory(), `run-${startedAt}.profile.json`);
|
|
44239
44472
|
}
|
|
44240
44473
|
function profileMarkPath(startedAt) {
|
|
44241
|
-
return
|
|
44474
|
+
return join32(runsDirectory(), `run-${startedAt}.profile-mark.json`);
|
|
44242
44475
|
}
|
|
44243
44476
|
function noteIsAbout(note, playing) {
|
|
44244
44477
|
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
44245
44478
|
}
|
|
44246
44479
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
44247
|
-
return
|
|
44480
|
+
return join32(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
44248
44481
|
}
|
|
44249
44482
|
function writeEditorRunNote(note) {
|
|
44250
44483
|
mkdirSync16(runsDirectory(), { recursive: true });
|
|
@@ -44255,7 +44488,7 @@ function writeEditorRunNote(note) {
|
|
|
44255
44488
|
}
|
|
44256
44489
|
function readEditorRunNote(projectPath) {
|
|
44257
44490
|
for (const directory of runtimeDirectories()) {
|
|
44258
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory))
|
|
44491
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory));
|
|
44259
44492
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
44260
44493
|
return note;
|
|
44261
44494
|
}
|
|
@@ -44265,7 +44498,7 @@ function readEditorRunNote(projectPath) {
|
|
|
44265
44498
|
function editorNoteAt(path) {
|
|
44266
44499
|
let parsed;
|
|
44267
44500
|
try {
|
|
44268
|
-
parsed = JSON.parse(
|
|
44501
|
+
parsed = JSON.parse(readFileSync26(path, "utf8"));
|
|
44269
44502
|
} catch {
|
|
44270
44503
|
return null;
|
|
44271
44504
|
}
|
|
@@ -44289,10 +44522,6 @@ function readRunRecord(projectPath) {
|
|
|
44289
44522
|
if (own !== null) {
|
|
44290
44523
|
return own;
|
|
44291
44524
|
}
|
|
44292
|
-
const shared = recordAt(join31(directory, "runs", SHARED_NOTE));
|
|
44293
|
-
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
44294
|
-
return shared;
|
|
44295
|
-
}
|
|
44296
44525
|
}
|
|
44297
44526
|
return null;
|
|
44298
44527
|
}
|
|
@@ -44301,13 +44530,13 @@ function everyRunRecord() {
|
|
|
44301
44530
|
for (const directory of runtimeDirectories()) {
|
|
44302
44531
|
let entries;
|
|
44303
44532
|
try {
|
|
44304
|
-
entries =
|
|
44533
|
+
entries = readdirSync14(join32(directory, "runs"));
|
|
44305
44534
|
} catch {
|
|
44306
44535
|
continue;
|
|
44307
44536
|
}
|
|
44308
44537
|
for (const entry of entries) {
|
|
44309
|
-
if (entry
|
|
44310
|
-
const record = recordAt(
|
|
44538
|
+
if (entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
44539
|
+
const record = recordAt(join32(directory, "runs", entry));
|
|
44311
44540
|
if (record !== null) {
|
|
44312
44541
|
found.push(record);
|
|
44313
44542
|
}
|
|
@@ -44319,7 +44548,7 @@ function everyRunRecord() {
|
|
|
44319
44548
|
function recordAt(path) {
|
|
44320
44549
|
let parsed;
|
|
44321
44550
|
try {
|
|
44322
|
-
parsed = JSON.parse(
|
|
44551
|
+
parsed = JSON.parse(readFileSync26(path, "utf8"));
|
|
44323
44552
|
} catch {
|
|
44324
44553
|
return null;
|
|
44325
44554
|
}
|
|
@@ -44334,17 +44563,23 @@ function recordAt(path) {
|
|
|
44334
44563
|
return null;
|
|
44335
44564
|
}
|
|
44336
44565
|
const args = fields["arguments"];
|
|
44566
|
+
const startedBy = fields["startedBy"];
|
|
44567
|
+
const projectPath = fields["projectPath"];
|
|
44568
|
+
const command = fields["command"];
|
|
44569
|
+
if (typeof startedBy !== "number" || typeof projectPath !== "string" || projectPath === "" || typeof command !== "string" || !Array.isArray(args)) {
|
|
44570
|
+
return null;
|
|
44571
|
+
}
|
|
44337
44572
|
return {
|
|
44338
44573
|
pid,
|
|
44339
44574
|
transcript,
|
|
44340
44575
|
startedAt,
|
|
44341
|
-
|
|
44342
|
-
projectPath
|
|
44576
|
+
startedBy,
|
|
44577
|
+
projectPath,
|
|
44343
44578
|
...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
|
|
44344
44579
|
...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
|
|
44345
44580
|
...typeof fields["profile"] === "string" && fields["profile"] !== "" ? { profile: fields["profile"] } : {},
|
|
44346
|
-
arguments:
|
|
44347
|
-
|
|
44581
|
+
arguments: args.filter((value) => typeof value === "string"),
|
|
44582
|
+
command,
|
|
44348
44583
|
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
44349
44584
|
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
|
|
44350
44585
|
...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
|
|
@@ -44372,7 +44607,7 @@ async function runningAs(pid) {
|
|
|
44372
44607
|
const began = await startedAt(pid, null);
|
|
44373
44608
|
const when = began === null ? {} : { startedAt: began };
|
|
44374
44609
|
if (process.platform === "linux") {
|
|
44375
|
-
const raw =
|
|
44610
|
+
const raw = readFileSync26(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
44376
44611
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
44377
44612
|
}
|
|
44378
44613
|
const args = (await runHidden("ps", ["-p", String(pid), "-o", "args="], { timeout: 15000 })).stdout.trim();
|
|
@@ -44472,26 +44707,20 @@ function whyNotTheRun(record, running, needed) {
|
|
|
44472
44707
|
if (running.kind === "none") {
|
|
44473
44708
|
return `the operating system would not describe that process (${running.unread ?? "no reason given"})`;
|
|
44474
44709
|
}
|
|
44475
|
-
const after = running.startedAt === undefined ? 0 : running.startedAt -
|
|
44710
|
+
const after = running.startedAt === undefined ? 0 : running.startedAt - record.startedBy;
|
|
44476
44711
|
if (after > SAME_RUN_WINDOW_MS) {
|
|
44477
44712
|
return `that process started ${Math.round(after / 1000)} seconds after the run had`;
|
|
44478
44713
|
}
|
|
44479
|
-
const engine =
|
|
44714
|
+
const engine = basename9(record.command);
|
|
44480
44715
|
const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
|
|
44481
|
-
const wanted =
|
|
44482
|
-
if (
|
|
44483
|
-
return `that process is not ${engine
|
|
44716
|
+
const wanted = process.platform === "win32" ? engine.toLowerCase() : engine;
|
|
44717
|
+
if (!said.includes(wanted)) {
|
|
44718
|
+
return `that process is not ${engine}, which the run was started with`;
|
|
44484
44719
|
}
|
|
44485
44720
|
if (running.kind === "commandLine" && readCommandLine(running.text).editor) {
|
|
44486
44721
|
return "that process is an editor";
|
|
44487
44722
|
}
|
|
44488
|
-
if (record.projectPath === "") {
|
|
44489
|
-
return wanted !== null ? null : "the record names neither the project nor the engine";
|
|
44490
|
-
}
|
|
44491
44723
|
if (running.kind === "image") {
|
|
44492
|
-
if (wanted === null) {
|
|
44493
|
-
return "the operating system named only its executable, and the record names no engine";
|
|
44494
|
-
}
|
|
44495
44724
|
return needed === "possible" ? null : `the operating system named only its executable and not its command line (${running.unread ?? "no reason given"}), which is not enough to signal it on`;
|
|
44496
44725
|
}
|
|
44497
44726
|
const project = process.platform === "win32" ? record.projectPath.toLowerCase() : record.projectPath;
|
|
@@ -44515,19 +44744,14 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
44515
44744
|
}
|
|
44516
44745
|
function clearRunRecord(projectPath) {
|
|
44517
44746
|
for (const directory of runtimeDirectories()) {
|
|
44518
|
-
|
|
44519
|
-
|
|
44520
|
-
|
|
44521
|
-
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
44522
|
-
try {
|
|
44523
|
-
rmSync14(path, { force: true });
|
|
44524
|
-
} catch {}
|
|
44525
|
-
}
|
|
44747
|
+
try {
|
|
44748
|
+
rmSync14(runRecordPath(projectPath, directory), { force: true });
|
|
44749
|
+
} catch {}
|
|
44526
44750
|
}
|
|
44527
44751
|
}
|
|
44528
44752
|
function ageMs(path, now) {
|
|
44529
44753
|
try {
|
|
44530
|
-
return now -
|
|
44754
|
+
return now - statSync14(path).mtimeMs;
|
|
44531
44755
|
} catch {
|
|
44532
44756
|
return 0;
|
|
44533
44757
|
}
|
|
@@ -44541,7 +44765,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
44541
44765
|
]));
|
|
44542
44766
|
let entries;
|
|
44543
44767
|
try {
|
|
44544
|
-
entries =
|
|
44768
|
+
entries = readdirSync14(directory);
|
|
44545
44769
|
} catch {
|
|
44546
44770
|
return;
|
|
44547
44771
|
}
|
|
@@ -44550,7 +44774,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
44550
44774
|
if (!transcript && !entry.endsWith(".partial")) {
|
|
44551
44775
|
continue;
|
|
44552
44776
|
}
|
|
44553
|
-
const path =
|
|
44777
|
+
const path = join32(directory, entry);
|
|
44554
44778
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
44555
44779
|
continue;
|
|
44556
44780
|
}
|
|
@@ -44559,7 +44783,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
44559
44783
|
} catch {}
|
|
44560
44784
|
}
|
|
44561
44785
|
}
|
|
44562
|
-
var
|
|
44786
|
+
var SAME_RUN_WINDOW_MS = 1e4;
|
|
44563
44787
|
var init_run_record = __esm(() => {
|
|
44564
44788
|
init_errors();
|
|
44565
44789
|
init_paths();
|
|
@@ -44571,10 +44795,10 @@ var init_run_record = __esm(() => {
|
|
|
44571
44795
|
});
|
|
44572
44796
|
|
|
44573
44797
|
// src/script-profile.ts
|
|
44574
|
-
import { readFileSync as
|
|
44798
|
+
import { readFileSync as readFileSync27 } from "node:fs";
|
|
44575
44799
|
function readProfileFile(path) {
|
|
44576
44800
|
try {
|
|
44577
|
-
const parsed = JSON.parse(
|
|
44801
|
+
const parsed = JSON.parse(readFileSync27(path, "utf8"));
|
|
44578
44802
|
return typeof parsed === "object" && typeof parsed.connected === "boolean" ? parsed : null;
|
|
44579
44803
|
} catch {
|
|
44580
44804
|
return null;
|
|
@@ -44631,7 +44855,7 @@ function writeProfileMark(path, mark) {
|
|
|
44631
44855
|
}
|
|
44632
44856
|
function readProfileMark(path) {
|
|
44633
44857
|
try {
|
|
44634
|
-
const parsed = JSON.parse(
|
|
44858
|
+
const parsed = JSON.parse(readFileSync27(path, "utf8"));
|
|
44635
44859
|
return typeof parsed === "object" && typeof parsed.totals === "object" ? parsed : null;
|
|
44636
44860
|
} catch {
|
|
44637
44861
|
return null;
|
|
@@ -44804,18 +45028,18 @@ import {
|
|
|
44804
45028
|
closeSync as closeSync3,
|
|
44805
45029
|
constants,
|
|
44806
45030
|
copyFileSync,
|
|
44807
|
-
existsSync as
|
|
45031
|
+
existsSync as existsSync26,
|
|
44808
45032
|
fstatSync,
|
|
44809
45033
|
mkdirSync as mkdirSync17,
|
|
44810
45034
|
openSync as openSync4,
|
|
44811
|
-
readdirSync as
|
|
44812
|
-
readFileSync as
|
|
45035
|
+
readdirSync as readdirSync15,
|
|
45036
|
+
readFileSync as readFileSync28,
|
|
44813
45037
|
readSync as readSync2,
|
|
44814
45038
|
rmdirSync as rmdirSync2,
|
|
44815
|
-
statSync as
|
|
45039
|
+
statSync as statSync15,
|
|
44816
45040
|
writeFileSync as writeFileSync18
|
|
44817
45041
|
} from "node:fs";
|
|
44818
|
-
import { basename as basename10, dirname as dirname23, extname as extname4, isAbsolute as isAbsolute5, join as
|
|
45042
|
+
import { basename as basename10, dirname as dirname23, extname as extname4, isAbsolute as isAbsolute5, join as join33, normalize as normalize2, relative as relative6 } from "node:path";
|
|
44819
45043
|
import { setTimeout as delay5 } from "node:timers/promises";
|
|
44820
45044
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
44821
45045
|
function editorRestartTimeoutMs() {
|
|
@@ -44852,15 +45076,15 @@ function writtenFileRefusal(path, rule) {
|
|
|
44852
45076
|
return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
|
|
44853
45077
|
}
|
|
44854
45078
|
const directory = realPathOr(dirname23(path));
|
|
44855
|
-
if (!
|
|
45079
|
+
if (!existsSync26(directory)) {
|
|
44856
45080
|
return `${named}'s directory ${dirname23(path)} does not exist; ${what} creates no directories.`;
|
|
44857
45081
|
}
|
|
44858
|
-
const where =
|
|
45082
|
+
const where = join33(directory, basename10(path));
|
|
44859
45083
|
const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
44860
45084
|
if (inside !== undefined) {
|
|
44861
45085
|
return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
|
|
44862
45086
|
}
|
|
44863
|
-
const taken = rule.writes(where).find((file) =>
|
|
45087
|
+
const taken = rule.writes(where).find((file) => existsSync26(file));
|
|
44864
45088
|
if (taken !== undefined) {
|
|
44865
45089
|
return taken === where ? `${named} ${path} already exists; ${what} is not written over a file. Name a new one.` : `${named} ${path} would write ${taken}, which already exists; ${what} is not written over a file. Name a new one.`;
|
|
44866
45090
|
}
|
|
@@ -45051,7 +45275,7 @@ async function closeLikeAPlayer(announced, alsoGoing, projectPath) {
|
|
|
45051
45275
|
}
|
|
45052
45276
|
const asked = await runtimeRequest(endpoint, "close_game", {}, CLOSE_ASK_MS);
|
|
45053
45277
|
if (!asked.ok) {
|
|
45054
|
-
return asked.reason === "error" ? `its runtime
|
|
45278
|
+
return asked.reason === "error" ? `its runtime refused the close (${asked.message})` : `its runtime did not answer the close within ${CLOSE_ASK_MS / 1000} seconds, as a game held at a breakpoint or stuck in a frame does not`;
|
|
45055
45279
|
}
|
|
45056
45280
|
if (await untilGone([announced, ...alsoGoing], closeWaitMs())) {
|
|
45057
45281
|
return null;
|
|
@@ -45373,6 +45597,18 @@ function outsideTheirRanges(spec, args) {
|
|
|
45373
45597
|
function scanWaitMs() {
|
|
45374
45598
|
return waitFromEnv("GDHARNESS_SCAN_WAIT_MS", 30000);
|
|
45375
45599
|
}
|
|
45600
|
+
function importedFirstAnswer(importedFirst) {
|
|
45601
|
+
if (importedFirst === undefined) {
|
|
45602
|
+
return {};
|
|
45603
|
+
}
|
|
45604
|
+
return {
|
|
45605
|
+
...importedFirst.imported.length === 0 ? {} : { importedFirst: importedFirst.imported },
|
|
45606
|
+
...importedFirst.notImported.length === 0 ? {} : { notImportedFirst: importedFirst.notImported },
|
|
45607
|
+
...importedFirst.uidsWritten.length === 0 ? {} : { uidsWrittenFirst: importedFirst.uidsWritten },
|
|
45608
|
+
importTookMs: importedFirst.tookMs,
|
|
45609
|
+
...importedFirst.note === undefined ? {} : { importNote: importedFirst.note }
|
|
45610
|
+
};
|
|
45611
|
+
}
|
|
45376
45612
|
function scanWaitIn(payload) {
|
|
45377
45613
|
const waited = readNumber(payload, "waitedForEditorScanMs");
|
|
45378
45614
|
const note = readString(payload, "scanNote");
|
|
@@ -45401,7 +45637,7 @@ function notRunNote(notRun, failFast) {
|
|
|
45401
45637
|
function exportPresetRefusal(projectPath, preset) {
|
|
45402
45638
|
let text;
|
|
45403
45639
|
try {
|
|
45404
|
-
text =
|
|
45640
|
+
text = readFileSync28(join33(projectPath, "export_presets.cfg"), "utf8");
|
|
45405
45641
|
} catch {
|
|
45406
45642
|
return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. project_export add_preset writes one, as the editor's Project > Export does.`;
|
|
45407
45643
|
}
|
|
@@ -45465,20 +45701,22 @@ function writtenBeside(before, after, output, within) {
|
|
|
45465
45701
|
function filesIn(folder) {
|
|
45466
45702
|
const found = new Map;
|
|
45467
45703
|
try {
|
|
45468
|
-
for (const entry of
|
|
45704
|
+
for (const entry of readdirSync15(folder, { withFileTypes: true })) {
|
|
45469
45705
|
if (entry.isFile()) {
|
|
45470
|
-
found.set(entry.name,
|
|
45706
|
+
found.set(entry.name, statSync15(join33(folder, entry.name)).mtimeMs);
|
|
45471
45707
|
}
|
|
45472
45708
|
}
|
|
45473
45709
|
} catch {}
|
|
45474
45710
|
return found;
|
|
45475
45711
|
}
|
|
45476
45712
|
function scanWaitAnswer(scanned, started = "game") {
|
|
45713
|
+
const imports = importedFirstAnswer(scanned.importedFirst);
|
|
45477
45714
|
if (scanned.waitedMs === 0) {
|
|
45478
|
-
return
|
|
45715
|
+
return imports;
|
|
45479
45716
|
}
|
|
45480
45717
|
const still = `The editor was still scanning the project after ${scanWaitMs() / 1000} seconds, so`;
|
|
45481
45718
|
return {
|
|
45719
|
+
...imports,
|
|
45482
45720
|
waitedForEditorScanMs: scanned.waitedMs,
|
|
45483
45721
|
scanNote: !scanned.stillScanning ? undefined : started === "game" ? `${still} this game started during the scan and may have read a class cache being rewritten. If ` + 'editor_output shows "Could not find type" for a class that is declared, editor_rescan ' + "waits for the scan to finish, and a start after it reads the finished cache." : `${still} the engine answering this started during the scan and may have read a class cache being ` + "rewritten, and a class cache it wrote can be written over when the scan finishes. If " + 'engine_messages shows "Could not find type" for a class that is declared, editor_rescan waits ' + "for the scan to finish, and the same call after it reads the finished cache."
|
|
45484
45722
|
};
|
|
@@ -45497,7 +45735,7 @@ function unsavedInEditorNote(scripts) {
|
|
|
45497
45735
|
return `${named} ${one ? "is" : "are"} open in the editor's script ${one ? "tab" : "tabs"} with unsaved edits, and saving ${one ? "that tab" : "them"} there writes ${one ? "its" : "their"} text over what was written here: close ${one ? "it" : "them"} without saving, or save first and edit again.`;
|
|
45498
45736
|
}
|
|
45499
45737
|
function editorAddonMissing(projectPath) {
|
|
45500
|
-
if (!
|
|
45738
|
+
if (!existsSync26(join33(projectPath, "project.godot"))) {
|
|
45501
45739
|
return null;
|
|
45502
45740
|
}
|
|
45503
45741
|
const report = inspectProject(projectPath);
|
|
@@ -45586,7 +45824,7 @@ function withUidsMissing(answer, missing) {
|
|
|
45586
45824
|
last.text = answerJson({
|
|
45587
45825
|
...parsed,
|
|
45588
45826
|
uidsMissing: missing,
|
|
45589
|
-
uidsNote: `${missing.length === 1 ? "This script has" : "These scripts have"} no .uid sidecar yet: the engine writes one when it imports the project,
|
|
45827
|
+
uidsNote: `${missing.length === 1 ? "This script has" : "These scripts have"} no .uid sidecar yet: the engine writes one when it imports the project, and this run had nothing to import, so a check that tracks the sidecars fails on ${missing.length === 1 ? "it" : "them"}. project_import refresh_uids writes them.`
|
|
45590
45828
|
});
|
|
45591
45829
|
}
|
|
45592
45830
|
} catch {}
|
|
@@ -45762,7 +46000,7 @@ function profileFrameFunctions(projectPath) {
|
|
|
45762
46000
|
let projectGodot = null;
|
|
45763
46001
|
if (projectPath !== null) {
|
|
45764
46002
|
try {
|
|
45765
|
-
projectGodot =
|
|
46003
|
+
projectGodot = readFileSync28(join33(projectPath, "project.godot"), "utf8");
|
|
45766
46004
|
} catch {}
|
|
45767
46005
|
}
|
|
45768
46006
|
return frameFunctionsFor(queuedMessagesOf(projectGodot));
|
|
@@ -45786,7 +46024,7 @@ function projectSourceOf(projectPath) {
|
|
|
45786
46024
|
let source = null;
|
|
45787
46025
|
if (inside.ok) {
|
|
45788
46026
|
try {
|
|
45789
|
-
source =
|
|
46027
|
+
source = readFileSync28(inside.absolutePath, "utf8");
|
|
45790
46028
|
} catch {}
|
|
45791
46029
|
}
|
|
45792
46030
|
read.set(script, source);
|
|
@@ -45941,7 +46179,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
|
|
|
45941
46179
|
}
|
|
45942
46180
|
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
45943
46181
|
try {
|
|
45944
|
-
return settingKeys(
|
|
46182
|
+
return settingKeys(readFileSync28(join33(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
45945
46183
|
} catch {
|
|
45946
46184
|
return true;
|
|
45947
46185
|
}
|
|
@@ -45956,13 +46194,13 @@ function uncaughtNote(uncaught) {
|
|
|
45956
46194
|
}
|
|
45957
46195
|
function elsewhereIn(projectPath, asked) {
|
|
45958
46196
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
45959
|
-
if (wanted === "" ||
|
|
46197
|
+
if (wanted === "" || existsSync26(join33(projectPath, wanted))) {
|
|
45960
46198
|
return "";
|
|
45961
46199
|
}
|
|
45962
46200
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
45963
46201
|
let near = [];
|
|
45964
46202
|
try {
|
|
45965
|
-
near =
|
|
46203
|
+
near = readdirSync15(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
|
|
45966
46204
|
} catch {
|
|
45967
46205
|
return "";
|
|
45968
46206
|
}
|
|
@@ -46053,23 +46291,23 @@ function engineChecked(why) {
|
|
|
46053
46291
|
}
|
|
46054
46292
|
function isDirectory(path) {
|
|
46055
46293
|
try {
|
|
46056
|
-
return
|
|
46294
|
+
return statSync15(path).isDirectory();
|
|
46057
46295
|
} catch {
|
|
46058
46296
|
return false;
|
|
46059
46297
|
}
|
|
46060
46298
|
}
|
|
46061
46299
|
function scriptsUnder(directory, prefix) {
|
|
46062
|
-
if (
|
|
46300
|
+
if (existsSync26(join33(directory, ".gdignore"))) {
|
|
46063
46301
|
return [];
|
|
46064
46302
|
}
|
|
46065
46303
|
const found = [];
|
|
46066
|
-
for (const entry of
|
|
46304
|
+
for (const entry of readdirSync15(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
|
|
46067
46305
|
if (entry.name.startsWith(".")) {
|
|
46068
46306
|
continue;
|
|
46069
46307
|
}
|
|
46070
46308
|
const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
46071
46309
|
if (entry.isDirectory()) {
|
|
46072
|
-
found.push(...scriptsUnder(
|
|
46310
|
+
found.push(...scriptsUnder(join33(directory, entry.name), path));
|
|
46073
46311
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
46074
46312
|
found.push(path);
|
|
46075
46313
|
}
|
|
@@ -46077,29 +46315,18 @@ function scriptsUnder(directory, prefix) {
|
|
|
46077
46315
|
return found;
|
|
46078
46316
|
}
|
|
46079
46317
|
function mainSceneOf(projectFile) {
|
|
46080
|
-
const scene = parseProjectGodot(
|
|
46318
|
+
const scene = parseProjectGodot(readFileSync28(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
46081
46319
|
return typeof scene === "string" && scene !== "" ? scene : null;
|
|
46082
46320
|
}
|
|
46083
|
-
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
46084
|
-
if (projectPath === null) {
|
|
46085
|
-
return false;
|
|
46086
|
-
}
|
|
46087
|
-
try {
|
|
46088
|
-
return readFileSync27(join32(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
46089
|
-
} catch {
|
|
46090
|
-
return false;
|
|
46091
|
-
}
|
|
46092
|
-
}
|
|
46093
46321
|
function editorPlaysHeadless(projectFile) {
|
|
46094
|
-
const runArgs = parseProjectGodot(
|
|
46322
|
+
const runArgs = parseProjectGodot(readFileSync28(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
46095
46323
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
46096
46324
|
}
|
|
46097
|
-
function
|
|
46325
|
+
function ownArguments(params) {
|
|
46098
46326
|
const result = emptyRecord();
|
|
46099
46327
|
const source = asParams(params);
|
|
46100
46328
|
for (const key of Object.keys(source)) {
|
|
46101
|
-
|
|
46102
|
-
result[name] = source[key];
|
|
46329
|
+
result[key] = source[key];
|
|
46103
46330
|
}
|
|
46104
46331
|
return result;
|
|
46105
46332
|
}
|
|
@@ -46110,7 +46337,7 @@ function needsItsOwnProcess(asked) {
|
|
|
46110
46337
|
class GodotServer {
|
|
46111
46338
|
stdio = null;
|
|
46112
46339
|
locator = new GodotLocator;
|
|
46113
|
-
operationsScript =
|
|
46340
|
+
operationsScript = join33(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
46114
46341
|
godotBridge;
|
|
46115
46342
|
tools = buildToolDefinitions();
|
|
46116
46343
|
activeProcess = null;
|
|
@@ -46185,6 +46412,7 @@ class GodotServer {
|
|
|
46185
46412
|
console.error(`[SERVER] Using Godot at: ${located.path}`);
|
|
46186
46413
|
}
|
|
46187
46414
|
this.stdio = serveStdio(() => this.protocol(), {
|
|
46415
|
+
legacy: "reject",
|
|
46188
46416
|
onerror: (error) => {
|
|
46189
46417
|
console.error("[MCP Error]", error);
|
|
46190
46418
|
}
|
|
@@ -46371,14 +46599,14 @@ class GodotServer {
|
|
|
46371
46599
|
}
|
|
46372
46600
|
noteTheStart() {
|
|
46373
46601
|
const cwd = process.cwd();
|
|
46374
|
-
const mcpJson =
|
|
46602
|
+
const mcpJson = join33(cwd, ".mcp.json");
|
|
46375
46603
|
const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
|
|
46376
46604
|
const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
|
|
46377
46605
|
writeLifecycle("started", {
|
|
46378
46606
|
argv: process.argv.slice(1),
|
|
46379
46607
|
cwd,
|
|
46380
46608
|
project: this.ownProject,
|
|
46381
|
-
mcpJson:
|
|
46609
|
+
mcpJson: existsSync26(mcpJson) ? mcpJson : null
|
|
46382
46610
|
});
|
|
46383
46611
|
if (predecessor !== null) {
|
|
46384
46612
|
writeLifecycle("replaced", { project: this.ownProject, predecessor });
|
|
@@ -46500,7 +46728,7 @@ class GodotServer {
|
|
|
46500
46728
|
return { name, version };
|
|
46501
46729
|
}
|
|
46502
46730
|
}
|
|
46503
|
-
return
|
|
46731
|
+
return;
|
|
46504
46732
|
};
|
|
46505
46733
|
protocol.setRequestHandler("tools/list", (_request, ctx) => {
|
|
46506
46734
|
this.noteTheClient(clientOf(ctx.mcpReq.envelope));
|
|
@@ -46513,13 +46741,13 @@ class GodotServer {
|
|
|
46513
46741
|
if (!spec) {
|
|
46514
46742
|
throw new ProtocolError(ProtocolErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}. Tools: ${TOOL_SPECS.map((tool) => tool.name).join(", ")}`);
|
|
46515
46743
|
}
|
|
46516
|
-
const args =
|
|
46744
|
+
const args = ownArguments(request.params.arguments);
|
|
46517
46745
|
const checked = this.validateArguments(spec, args);
|
|
46518
46746
|
if (!checked.ok) {
|
|
46519
46747
|
return checked.response;
|
|
46520
46748
|
}
|
|
46521
46749
|
const named = args["projectPath"];
|
|
46522
|
-
if (typeof named === "string" &&
|
|
46750
|
+
if (typeof named === "string" && existsSync26(join33(named, "project.godot"))) {
|
|
46523
46751
|
this.lastProjectPath = named;
|
|
46524
46752
|
}
|
|
46525
46753
|
this.updates.refresh();
|
|
@@ -46555,7 +46783,7 @@ class GodotServer {
|
|
|
46555
46783
|
let notSaved;
|
|
46556
46784
|
try {
|
|
46557
46785
|
savedAt = answer.content.map((item, index) => {
|
|
46558
|
-
const file =
|
|
46786
|
+
const file = join33(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
|
|
46559
46787
|
writeFileSync18(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
|
|
46560
46788
|
return file;
|
|
46561
46789
|
});
|
|
@@ -46636,7 +46864,7 @@ class GodotServer {
|
|
|
46636
46864
|
project_addons_behind_this_server: {
|
|
46637
46865
|
server_is: SERVER_VERSION,
|
|
46638
46866
|
project_is: moved,
|
|
46639
|
-
what_to_do: `The project's gdharness addons are ${moved}, older than this ${SERVER_VERSION} server
|
|
46867
|
+
what_to_do: `The project's gdharness addons are ${moved}, older than this ${SERVER_VERSION} server. ` + "Restarting the editor would load that older addon code again, and reconnecting would start " + "this same server version again, so neither brings the two into step. Tell the " + `user, and offer to run ${gdharnessCommand(SERVER_VERSION, this.ownProject === null ? "upgrade" : `upgrade "${this.ownProject}"`)} ` + "in a shell, which replaces the addons with this server's; ask first, since it changes " + "files in their project. editor_launch restart then loads them."
|
|
46640
46868
|
}
|
|
46641
46869
|
});
|
|
46642
46870
|
}
|
|
@@ -46689,11 +46917,6 @@ class GodotServer {
|
|
|
46689
46917
|
const installed = installedAddonVersion(this.ownProject);
|
|
46690
46918
|
return installed === null || installed === SERVER_VERSION ? null : installed;
|
|
46691
46919
|
}
|
|
46692
|
-
editorCodeOnDisk() {
|
|
46693
|
-
const status = this.godotBridge.getStatus();
|
|
46694
|
-
const project = (status.connected ? status.projectPath : undefined) ?? this.ownProject;
|
|
46695
|
-
return project === null || project === "" ? undefined : installedEditorDigest(project);
|
|
46696
|
-
}
|
|
46697
46920
|
runtimeStaleNote(endpoint) {
|
|
46698
46921
|
const project = endpoint.project.path;
|
|
46699
46922
|
return runtimeMismatch(endpoint.pid, endpoint.addon, SERVER_VERSION, shippedRuntimeDigest(), project === "" ? undefined : installedRuntimeDigest(project), project);
|
|
@@ -46728,9 +46951,13 @@ class GodotServer {
|
|
|
46728
46951
|
});
|
|
46729
46952
|
const takes = wanted.length === 0 ? "It takes no arguments." : `It takes: ${shaped.join(", ")}.`;
|
|
46730
46953
|
const where = named === null ? spec.name : `${spec.name} ${named}`;
|
|
46954
|
+
const spelled = unknown.flatMap((key) => {
|
|
46955
|
+
const camel = key.replace(/_([a-zA-Z0-9])/g, (_, letter) => letter.toUpperCase());
|
|
46956
|
+
return camel !== key && known.has(camel) ? [`${key} is spelled ${camel}`] : [];
|
|
46957
|
+
});
|
|
46731
46958
|
return {
|
|
46732
46959
|
ok: false,
|
|
46733
|
-
response: this.createErrorResponse(`${where} does not take ${unknown.join(", ")}. ${takes} This is gdharness ${SERVER_VERSION}, which is what decides whether an argument exists.`)
|
|
46960
|
+
response: this.createErrorResponse(`${where} does not take ${unknown.join(", ")}.${spelled.length === 0 ? "" : ` ${spelled.join(", ")}.`} ${takes} This is gdharness ${SERVER_VERSION}, which is what decides whether an argument exists.`)
|
|
46734
46961
|
};
|
|
46735
46962
|
}
|
|
46736
46963
|
const wrong = [
|
|
@@ -46847,7 +47074,7 @@ class GodotServer {
|
|
|
46847
47074
|
const contained = this.containProjectFiles({ ...arguments_, ...extra });
|
|
46848
47075
|
return contained.ok ? await this.handleViaBridge(command, contained.value, op === "" ? tool : `${tool} ${op}`) : contained.response;
|
|
46849
47076
|
};
|
|
46850
|
-
const editorOrEngine = async (command, effect
|
|
47077
|
+
const editorOrEngine = async (command, effect) => await this.editorOrEngine(command, arguments_, `${tool} ${op}`, effect);
|
|
46851
47078
|
switch (tool) {
|
|
46852
47079
|
case "project_info":
|
|
46853
47080
|
return await this.handleProjectInfo(args);
|
|
@@ -46876,13 +47103,13 @@ class GodotServer {
|
|
|
46876
47103
|
case "set":
|
|
46877
47104
|
return await editorOrEngine("set_node_properties", "writes");
|
|
46878
47105
|
case "duplicate":
|
|
46879
|
-
return await editorOrEngine("duplicate_node", "writes"
|
|
47106
|
+
return await editorOrEngine("duplicate_node", "writes");
|
|
46880
47107
|
case "reparent":
|
|
46881
47108
|
return await editorOrEngine("reparent_node", "writes");
|
|
46882
47109
|
case "delete":
|
|
46883
47110
|
return await editorOrEngine("delete_node", "writes");
|
|
46884
47111
|
default:
|
|
46885
|
-
return await editorOrEngine("set_tilemap_cells", "writes"
|
|
47112
|
+
return await editorOrEngine("set_tilemap_cells", "writes");
|
|
46886
47113
|
}
|
|
46887
47114
|
case "scene_signal":
|
|
46888
47115
|
switch (op) {
|
|
@@ -46946,13 +47173,13 @@ class GodotServer {
|
|
|
46946
47173
|
case "modify":
|
|
46947
47174
|
return await editorOrEngine("modify_resource", "writes");
|
|
46948
47175
|
case "create_shader":
|
|
46949
|
-
return await editorOrEngine("create_shader", "writes"
|
|
47176
|
+
return await editorOrEngine("create_shader", "writes");
|
|
46950
47177
|
case "create_tileset":
|
|
46951
|
-
return await editorOrEngine("create_tileset", "writes"
|
|
47178
|
+
return await editorOrEngine("create_tileset", "writes");
|
|
46952
47179
|
case "set_theme_color":
|
|
46953
|
-
return await editorOrEngine("set_theme_color", "writes"
|
|
47180
|
+
return await editorOrEngine("set_theme_color", "writes");
|
|
46954
47181
|
default:
|
|
46955
|
-
return await editorOrEngine("set_theme_font_size", "writes"
|
|
47182
|
+
return await editorOrEngine("set_theme_font_size", "writes");
|
|
46956
47183
|
}
|
|
46957
47184
|
case "editor_launch":
|
|
46958
47185
|
if (op === "close") {
|
|
@@ -47173,8 +47400,8 @@ class GodotServer {
|
|
|
47173
47400
|
if (path === undefined) {
|
|
47174
47401
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
47175
47402
|
}
|
|
47176
|
-
const file =
|
|
47177
|
-
if (!
|
|
47403
|
+
const file = join33(path, "project.godot");
|
|
47404
|
+
if (!existsSync26(file)) {
|
|
47178
47405
|
return {
|
|
47179
47406
|
ok: false,
|
|
47180
47407
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -47345,6 +47572,42 @@ class GodotServer {
|
|
|
47345
47572
|
}
|
|
47346
47573
|
return { ok: true, value: stale };
|
|
47347
47574
|
}
|
|
47575
|
+
async importWhatChanged(projectPath) {
|
|
47576
|
+
if (staleImports(projectPath).length === 0) {
|
|
47577
|
+
return;
|
|
47578
|
+
}
|
|
47579
|
+
const unminted = scriptsWithoutUid(projectPath);
|
|
47580
|
+
const began = Date.now();
|
|
47581
|
+
const done = await this.reimport(projectPath, undefined, false, scanWaitMs());
|
|
47582
|
+
const tookMs = Date.now() - began;
|
|
47583
|
+
const stillUnminted = scriptsWithoutUid(projectPath);
|
|
47584
|
+
const uidsWritten = unminted.filter((script) => !stillUnminted.includes(script)).map((script) => `res://${script}`);
|
|
47585
|
+
if (!done.ok) {
|
|
47586
|
+
const why = done.response.content.map((block) => block.text).join(" ");
|
|
47587
|
+
return {
|
|
47588
|
+
imported: [],
|
|
47589
|
+
notImported: [],
|
|
47590
|
+
uidsWritten,
|
|
47591
|
+
tookMs,
|
|
47592
|
+
note: `Files changed since they were imported could not be imported before this run, so it reads their previous import: ${why}`
|
|
47593
|
+
};
|
|
47594
|
+
}
|
|
47595
|
+
const imported = (readArray(done.payload, "reimported") ?? []).map(String);
|
|
47596
|
+
const notImported = readArray(done.payload, "notReimported") ?? [];
|
|
47597
|
+
const note = done.payload["stillImporting"] === true ? `The editor was still importing files changed since they were imported when the wait ran out after ${scanWaitMs() / 1000} seconds, so this run may read their previous import. project_import status says where each stands.` : notImported.length > 0 ? "The files under notImportedFirst are still on their previous import, so this run shows what they held before. project_import status says why." : undefined;
|
|
47598
|
+
return { imported, notImported, uidsWritten, tookMs, ...note === undefined ? {} : { note } };
|
|
47599
|
+
}
|
|
47600
|
+
async afterTheImport(projectPath, waited, importedFirst) {
|
|
47601
|
+
if (importedFirst === undefined) {
|
|
47602
|
+
return waited;
|
|
47603
|
+
}
|
|
47604
|
+
const settled = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
47605
|
+
return {
|
|
47606
|
+
waitedMs: waited.waitedMs + settled.waitedMs,
|
|
47607
|
+
stillScanning: settled.stillScanning,
|
|
47608
|
+
importedFirst
|
|
47609
|
+
};
|
|
47610
|
+
}
|
|
47348
47611
|
async untilTheEditorsScanIsWritten(projectPath) {
|
|
47349
47612
|
const open = this.godotBridge.getStatus().projectPath;
|
|
47350
47613
|
if (!this.godotBridge.isConnected() || open === undefined || !isSameDirectory(open, projectPath)) {
|
|
@@ -47465,18 +47728,24 @@ class GodotServer {
|
|
|
47465
47728
|
const projectPath = project.value.path;
|
|
47466
47729
|
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
47467
47730
|
const before = scriptsWithoutUid(projectPath);
|
|
47731
|
+
const cachedBefore = pathsInUidCache(projectPath);
|
|
47468
47732
|
const imported = await runImport(engine.value, projectPath);
|
|
47469
47733
|
if (!imported.ok) {
|
|
47470
47734
|
return this.answer(withScanWait(imported, scanned));
|
|
47471
47735
|
}
|
|
47472
47736
|
const after = scriptsWithoutUid(projectPath);
|
|
47473
47737
|
const given = before.filter((script) => !after.includes(script));
|
|
47738
|
+
const cachedAfter = pathsInUidCache(projectPath);
|
|
47739
|
+
const cached = cachedAfter === null ? null : [...cachedAfter].filter((path) => cachedBefore?.has(path) !== true).sort();
|
|
47474
47740
|
return this.answer(withScanWait({
|
|
47475
47741
|
ok: true,
|
|
47476
47742
|
messages: imported.messages,
|
|
47477
47743
|
...imported.messagesOmitted === undefined ? {} : { messagesOmitted: imported.messagesOmitted },
|
|
47478
47744
|
payload: {
|
|
47479
47745
|
uidsCreated: given,
|
|
47746
|
+
...cached === null ? {
|
|
47747
|
+
uidCacheNote: "The import pass left no uid cache this server could read, .godot/uid_cache.bin."
|
|
47748
|
+
} : { uidsCached: cached },
|
|
47480
47749
|
stillWithoutUid: after,
|
|
47481
47750
|
note: uidsLeftNote(after.length),
|
|
47482
47751
|
...imported.extensionNote === undefined ? {} : { extensionNote: imported.extensionNote },
|
|
@@ -47549,9 +47818,8 @@ class GodotServer {
|
|
|
47549
47818
|
return {};
|
|
47550
47819
|
} catch (error) {
|
|
47551
47820
|
const message = errorMessage(error);
|
|
47552
|
-
const why = message.includes("Unknown tool") ? "runs an addon from before writes were handed to it" : `could not take the change (${message})`;
|
|
47553
47821
|
return {
|
|
47554
|
-
editorNote: `The open editor ${
|
|
47822
|
+
editorNote: error instanceof AddonOfAnotherRelease ? `The open editor was not asked to take the change, so it still holds the old values, answers them when asked, and writes them back over the file the next time it saves. ${message}` : `The open editor could not take the change (${message}), so it still holds the old values, answers them when asked, and writes them back over the file the next time it saves. editor_launch restart loads the file again.`
|
|
47555
47823
|
};
|
|
47556
47824
|
}
|
|
47557
47825
|
}
|
|
@@ -47764,8 +48032,7 @@ class GodotServer {
|
|
|
47764
48032
|
const message = errorMessage(error);
|
|
47765
48033
|
return {
|
|
47766
48034
|
ok: false,
|
|
47767
|
-
response:
|
|
47768
|
-
"editor_launch restart loads the addon this server ships",
|
|
48035
|
+
response: error instanceof AddonOfAnotherRelease ? this.createErrorResponse(message, [
|
|
47769
48036
|
"Or close the editor, and project_import reimport runs the engine import instead"
|
|
47770
48037
|
]) : this.createErrorResponse(`The editor answered reimport_files with an error: ${message}`)
|
|
47771
48038
|
};
|
|
@@ -47802,7 +48069,7 @@ class GodotServer {
|
|
|
47802
48069
|
}
|
|
47803
48070
|
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
47804
48071
|
this.logDebug(`Running ${operation} in ${projectPath}: ${JSON.stringify(params)}`);
|
|
47805
|
-
const outcome = await runOperation({ godotPath: engine.value, script: this.operationsScript, debug:
|
|
48072
|
+
const outcome = await runOperation({ godotPath: engine.value, script: this.operationsScript, debug: DEBUG_MODE }, operation, params, projectPath);
|
|
47806
48073
|
return withScanWait(outcome, scanned);
|
|
47807
48074
|
}
|
|
47808
48075
|
answer(outcome) {
|
|
@@ -47834,7 +48101,7 @@ class GodotServer {
|
|
|
47834
48101
|
if (!engine.ok) {
|
|
47835
48102
|
return engine.response;
|
|
47836
48103
|
}
|
|
47837
|
-
const application = parseProjectGodot(
|
|
48104
|
+
const application = parseProjectGodot(readFileSync28(project.value.file, "utf8"))["application"] ?? {};
|
|
47838
48105
|
const name = application["config/name"];
|
|
47839
48106
|
const mainScene = application["run/main_scene"];
|
|
47840
48107
|
const info = {
|
|
@@ -47932,7 +48199,7 @@ class GodotServer {
|
|
|
47932
48199
|
const exportArgs = [
|
|
47933
48200
|
"--headless",
|
|
47934
48201
|
"--log-file",
|
|
47935
|
-
|
|
48202
|
+
join33(exportLogs, "engine.log"),
|
|
47936
48203
|
"--path",
|
|
47937
48204
|
project.value.path,
|
|
47938
48205
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -47940,7 +48207,7 @@ class GodotServer {
|
|
|
47940
48207
|
output.absolutePath
|
|
47941
48208
|
];
|
|
47942
48209
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
47943
|
-
const writtenAt = () =>
|
|
48210
|
+
const writtenAt = () => existsSync26(output.absolutePath) ? statSync15(output.absolutePath).mtimeMs : null;
|
|
47944
48211
|
const caches = editorCacheDirectory(engine.value);
|
|
47945
48212
|
const exportOnce = async () => {
|
|
47946
48213
|
const before = writtenAt();
|
|
@@ -48023,10 +48290,10 @@ class GodotServer {
|
|
|
48023
48290
|
if (movie !== null) {
|
|
48024
48291
|
return this.createErrorResponse(movie);
|
|
48025
48292
|
}
|
|
48026
|
-
const missing = paths.value.filter((path) => !
|
|
48293
|
+
const missing = paths.value.filter((path) => !existsSync26(join33(project.value.path, path.slice("res://".length))));
|
|
48027
48294
|
const asked = paths.value.filter((path) => !missing.includes(path));
|
|
48028
48295
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
48029
|
-
const noRunner = !
|
|
48296
|
+
const noRunner = !existsSync26(join33(project.value.path, runner));
|
|
48030
48297
|
if (asked.length === 0) {
|
|
48031
48298
|
const verdict = `nothing at ${missing.join(" and ")}`;
|
|
48032
48299
|
const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
|
|
@@ -48063,7 +48330,15 @@ class GodotServer {
|
|
|
48063
48330
|
if (!classes.ok) {
|
|
48064
48331
|
return this.answer(classes);
|
|
48065
48332
|
}
|
|
48066
|
-
const
|
|
48333
|
+
const rebuildWaited = scanWaitIn(classes.payload);
|
|
48334
|
+
const importWaited = scanWaitAnswer(await this.afterTheImport(project.value.path, { waitedMs: 0, stillScanning: false }, await this.importWhatChanged(project.value.path)));
|
|
48335
|
+
const waitedMs = (rebuildWaited.waitedForEditorScanMs ?? 0) + (importWaited.waitedForEditorScanMs ?? 0);
|
|
48336
|
+
const scanNote = importWaited.scanNote ?? rebuildWaited.scanNote;
|
|
48337
|
+
const scanned = {
|
|
48338
|
+
...importWaited,
|
|
48339
|
+
...waitedMs > 0 ? { waitedForEditorScanMs: waitedMs } : {},
|
|
48340
|
+
...scanNote === undefined ? {} : { scanNote }
|
|
48341
|
+
};
|
|
48067
48342
|
const ours = `gdharness-reports/${randomUUID2()}`;
|
|
48068
48343
|
const reports = `res://.godot/${ours}`;
|
|
48069
48344
|
const ignored = readStringArray(args, "ignore") ?? [];
|
|
@@ -48134,14 +48409,14 @@ class GodotServer {
|
|
|
48134
48409
|
await run.settled;
|
|
48135
48410
|
const tookMs = Date.now() - began;
|
|
48136
48411
|
const notCompiled = stopped.for ?? run.log.all.map((entry) => autoloadNotCompiled(entry, autoloaded)).find((found) => found !== null) ?? null;
|
|
48137
|
-
const reportsDir =
|
|
48412
|
+
const reportsDir = join33(project.value.path, ".godot", ...ours.split("/"));
|
|
48138
48413
|
let report = null;
|
|
48139
48414
|
let reportProblem = null;
|
|
48140
48415
|
try {
|
|
48141
|
-
const written =
|
|
48416
|
+
const written = existsSync26(reportsDir) && notCompiled === null ? readdirSync15(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
48142
48417
|
const newest = written.at(-1);
|
|
48143
48418
|
if (newest !== undefined) {
|
|
48144
|
-
report = parseJUnit(
|
|
48419
|
+
report = parseJUnit(readFileSync28(join33(reportsDir, newest, "results.xml"), "utf8"));
|
|
48145
48420
|
}
|
|
48146
48421
|
} catch (error) {
|
|
48147
48422
|
reportProblem = errorMessage(error);
|
|
@@ -48341,7 +48616,7 @@ class GodotServer {
|
|
|
48341
48616
|
const checked = project.ok ? await this.engineDiagnostics(project.value.path, [readString(args, "scriptPath") ?? ""], one.noLanguageServer) : { refusal: project.response };
|
|
48342
48617
|
return "refusal" in checked ? checked.refusal : this.jsonTextResponse({ ...checked.answers[0], ...engineChecked(one.noLanguageServer.reason) });
|
|
48343
48618
|
}
|
|
48344
|
-
return "refusal" in one ? one.refusal : this.jsonTextResponse(
|
|
48619
|
+
return "refusal" in one ? one.refusal : this.jsonTextResponse(one.answer);
|
|
48345
48620
|
}
|
|
48346
48621
|
const progress = callProgress();
|
|
48347
48622
|
const cancelled = callSignal();
|
|
@@ -48391,7 +48666,7 @@ class GodotServer {
|
|
|
48391
48666
|
reported.push(one.answer);
|
|
48392
48667
|
}
|
|
48393
48668
|
}
|
|
48394
|
-
return this.jsonTextResponse(
|
|
48669
|
+
return this.jsonTextResponse({
|
|
48395
48670
|
clean: errors === 0 && unread === 0,
|
|
48396
48671
|
scriptsChecked: scripts.value.length,
|
|
48397
48672
|
errors,
|
|
@@ -48400,7 +48675,7 @@ class GodotServer {
|
|
|
48400
48675
|
scripts: reported,
|
|
48401
48676
|
nothingReported,
|
|
48402
48677
|
...byEngine === null ? {} : engineChecked(byEngine)
|
|
48403
|
-
})
|
|
48678
|
+
});
|
|
48404
48679
|
}
|
|
48405
48680
|
async compileScripts(projectPath, paths) {
|
|
48406
48681
|
const payload = { checked: [], failed: [], scripts: [] };
|
|
@@ -48457,13 +48732,6 @@ class GodotServer {
|
|
|
48457
48732
|
})
|
|
48458
48733
|
};
|
|
48459
48734
|
}
|
|
48460
|
-
markedIfStale(answer) {
|
|
48461
|
-
const status = this.godotBridge.getStatus();
|
|
48462
|
-
if (!hasSaidWhoItIs(status)) {
|
|
48463
|
-
return answer;
|
|
48464
|
-
}
|
|
48465
|
-
return markIfStale(answer, status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
|
|
48466
|
-
}
|
|
48467
48735
|
async diagnosticsOf(args) {
|
|
48468
48736
|
const waited = await this.waitForTheEditorOpenedHere(args, []);
|
|
48469
48737
|
if (waited !== null) {
|
|
@@ -48509,11 +48777,11 @@ class GodotServer {
|
|
|
48509
48777
|
const cached = cachedClasses(projectPath);
|
|
48510
48778
|
const sourceOf = (resourcePath) => {
|
|
48511
48779
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
48512
|
-
if (!contained.ok || !
|
|
48780
|
+
if (!contained.ok || !existsSync26(contained.absolutePath)) {
|
|
48513
48781
|
return null;
|
|
48514
48782
|
}
|
|
48515
48783
|
try {
|
|
48516
|
-
return
|
|
48784
|
+
return readFileSync28(contained.absolutePath, "utf8");
|
|
48517
48785
|
} catch {
|
|
48518
48786
|
return null;
|
|
48519
48787
|
}
|
|
@@ -48603,11 +48871,11 @@ class GodotServer {
|
|
|
48603
48871
|
}
|
|
48604
48872
|
sourceOfClass(projectPath, resourcePath) {
|
|
48605
48873
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
48606
|
-
if (!contained.ok || !
|
|
48874
|
+
if (!contained.ok || !existsSync26(contained.absolutePath)) {
|
|
48607
48875
|
return null;
|
|
48608
48876
|
}
|
|
48609
48877
|
try {
|
|
48610
|
-
return
|
|
48878
|
+
return readFileSync28(contained.absolutePath, "utf8");
|
|
48611
48879
|
} catch {
|
|
48612
48880
|
return null;
|
|
48613
48881
|
}
|
|
@@ -48730,12 +48998,11 @@ class GodotServer {
|
|
|
48730
48998
|
return client;
|
|
48731
48999
|
}
|
|
48732
49000
|
breakpointsAtRisk() {
|
|
48733
|
-
|
|
48734
|
-
if (!hasSaidWhoItIs(status) || status.syncsBreakpoints === true) {
|
|
49001
|
+
if (this.godotBridge.getStatus().syncsBreakpoints !== false) {
|
|
48735
49002
|
return {};
|
|
48736
49003
|
}
|
|
48737
49004
|
return {
|
|
48738
|
-
breakpointsAtRisk:
|
|
49005
|
+
breakpointsAtRisk: "This editor clears every breakpoint in its script editor whenever a debug session opens on its adapter, which this server does for its first breakpoint or stack read: network/debug_adapter/sync_breakpoints is off. The addon turns it on as it loads, so it was turned off since; turn it on in Editor Settings > Network > Debug Adapter, or editor_launch restart turns it on again."
|
|
48739
49006
|
};
|
|
48740
49007
|
}
|
|
48741
49008
|
breakpointsForAGame(projectPath) {
|
|
@@ -48765,7 +49032,7 @@ class GodotServer {
|
|
|
48765
49032
|
...Array.isArray(stack) ? {
|
|
48766
49033
|
stack: stack.map((frame) => ({
|
|
48767
49034
|
...frame,
|
|
48768
|
-
...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path:
|
|
49035
|
+
...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join33(projectPath, frame["script"].slice("res://".length)) } : {}
|
|
48769
49036
|
}))
|
|
48770
49037
|
} : {}
|
|
48771
49038
|
});
|
|
@@ -48829,13 +49096,13 @@ class GodotServer {
|
|
|
48829
49096
|
const arriving = status.connected ? [] : arrivingFound;
|
|
48830
49097
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
48831
49098
|
const greeted = hasSaidWhoItIs(status);
|
|
48832
|
-
const
|
|
49099
|
+
const staleNote = this.godotBridge.anotherRelease();
|
|
48833
49100
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
48834
49101
|
const cannotConnect = status.connected || this.ownProject === null ? null : editorAddonMissing(this.ownProject);
|
|
48835
49102
|
return {
|
|
48836
49103
|
...status,
|
|
48837
49104
|
serverVersion: SERVER_VERSION,
|
|
48838
|
-
addonIsStale: greeted ?
|
|
49105
|
+
addonIsStale: greeted ? staleNote !== undefined : undefined,
|
|
48839
49106
|
greeting: status.connected && !greeted ? "The editor has connected and has not yet said who it is: its project, pid, addon version and ports arrive with its greeting a moment after the socket opens, later while it imports. Ask again for them." : undefined,
|
|
48840
49107
|
bridgeAvailable: this.bridgeStartupError === null && status.port !== null,
|
|
48841
49108
|
mayYetConnect: status.connected ? undefined : cannotConnect !== null ? false : arriving.length > 0 ? true : unfinished !== null ? unfinished.underway : this.closedHere !== null ? false : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
@@ -48869,8 +49136,8 @@ class GodotServer {
|
|
|
48869
49136
|
port: this.godotBridge.configuredPort,
|
|
48870
49137
|
note: `The gdharness server this one replaced, pid ${this.handingOverFrom}, still holds port ${this.godotBridge.configuredPort} and is standing down; this server takes the port when it lets go, and takes another after ${HANDOVER_MS / 1000} seconds if it does not.`
|
|
48871
49138
|
},
|
|
48872
|
-
staleNote
|
|
48873
|
-
addonNote: greeted ? sameCodeNote(status.addonVersion
|
|
49139
|
+
staleNote,
|
|
49140
|
+
addonNote: greeted && staleNote === undefined ? sameCodeNote(status.addonVersion ?? "", SERVER_VERSION) : undefined,
|
|
48874
49141
|
startedByAnEditorNote: status.startedByAnEditor === true ? "Another editor started this one: Godot's own restart, or the project manager opening a project. None of the arguments a server gave the editor before it reached this one, so it serves the ports its editor settings name (and none at all while another editor holds those, when the script and debug tools reach that editor instead), its console is not captured, and it restarts itself. Close it and use editor_launch open to have an editor this server started." : undefined,
|
|
48875
49142
|
...this.breakpointsAtRisk(),
|
|
48876
49143
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
@@ -48955,7 +49222,7 @@ class GodotServer {
|
|
|
48955
49222
|
project: endpoint.project,
|
|
48956
49223
|
...this.ownProject === null ? {} : { ofThisProject: isSameDirectory(endpoint.project.path, this.ownProject) },
|
|
48957
49224
|
editorPid: endpoint.editorPid,
|
|
48958
|
-
addonVersion: endpoint.addon
|
|
49225
|
+
addonVersion: endpoint.addon.version === "" ? null : endpoint.addon.version,
|
|
48959
49226
|
...unknown ? {} : { addonIsStale: staleNote !== undefined },
|
|
48960
49227
|
...staleNote === undefined ? {} : { staleNote },
|
|
48961
49228
|
reachable: reply.ok,
|
|
@@ -49044,7 +49311,7 @@ class GodotServer {
|
|
|
49044
49311
|
return null;
|
|
49045
49312
|
}
|
|
49046
49313
|
try {
|
|
49047
|
-
return settingKeys(
|
|
49314
|
+
return settingKeys(readFileSync28(join33(projectPath, "project.godot"), "utf8"));
|
|
49048
49315
|
} catch {
|
|
49049
49316
|
return null;
|
|
49050
49317
|
}
|
|
@@ -49068,7 +49335,7 @@ class GodotServer {
|
|
|
49068
49335
|
]);
|
|
49069
49336
|
}
|
|
49070
49337
|
const settingsBefore = this.settingKeysOf(before.projectPath);
|
|
49071
|
-
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, args["hidden"] === true) : await this.handleViaBridge("restart_editor", {});
|
|
49338
|
+
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, args["hidden"] === true, resolveSilent(args["silent"])) : await this.handleViaBridge("restart_editor", {});
|
|
49072
49339
|
if (asked.isError === true) {
|
|
49073
49340
|
return asked;
|
|
49074
49341
|
}
|
|
@@ -49090,10 +49357,11 @@ class GodotServer {
|
|
|
49090
49357
|
editorPid: now.editorPid,
|
|
49091
49358
|
addonVersion: now.addonVersion,
|
|
49092
49359
|
serverVersion: SERVER_VERSION,
|
|
49093
|
-
addonIsStale:
|
|
49094
|
-
staleNote:
|
|
49360
|
+
addonIsStale: this.godotBridge.anotherRelease() !== undefined,
|
|
49361
|
+
staleNote: this.godotBridge.anotherRelease(),
|
|
49095
49362
|
...this.whatTheEditorDropped(settingsBefore, after),
|
|
49096
49363
|
...servers,
|
|
49364
|
+
...mine && this.launchedEditor !== null ? { arguments: this.launchedEditor.arguments } : {},
|
|
49097
49365
|
tookMs: Date.now() - began
|
|
49098
49366
|
});
|
|
49099
49367
|
}
|
|
@@ -49182,7 +49450,7 @@ class GodotServer {
|
|
|
49182
49450
|
whatTheEditorDropped(before, after) {
|
|
49183
49451
|
return settingsDroppedReport(before, after);
|
|
49184
49452
|
}
|
|
49185
|
-
async startItAgain(projectPath, editorPid, hidden) {
|
|
49453
|
+
async startItAgain(projectPath, editorPid, hidden, silent) {
|
|
49186
49454
|
const engine = await this.engine();
|
|
49187
49455
|
if (!engine.ok) {
|
|
49188
49456
|
return engine.response;
|
|
@@ -49211,7 +49479,7 @@ class GodotServer {
|
|
|
49211
49479
|
`editor_launch open starts one on ${projectPath} once it has gone`
|
|
49212
49480
|
]);
|
|
49213
49481
|
}
|
|
49214
|
-
const opened = await this.openAnEditor(engine.value, projectPath, ports, hidden);
|
|
49482
|
+
const opened = await this.openAnEditor(engine.value, projectPath, ports, hidden, silent);
|
|
49215
49483
|
restartSettled(projectPath);
|
|
49216
49484
|
if (opened.error !== null) {
|
|
49217
49485
|
return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
|
|
@@ -49261,7 +49529,7 @@ class GodotServer {
|
|
|
49261
49529
|
const ports = await this.portsForAnEditor();
|
|
49262
49530
|
const before = this.settingKeysOf(project.value.path);
|
|
49263
49531
|
const unfinished = restartOwed(project.value.path);
|
|
49264
|
-
const opened = await this.openAnEditor(engine.value, project.value.path, ports, args["hidden"] === true);
|
|
49532
|
+
const opened = await this.openAnEditor(engine.value, project.value.path, ports, args["hidden"] === true, resolveSilent(args["silent"]));
|
|
49265
49533
|
if (opened.error !== null) {
|
|
49266
49534
|
return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
|
|
49267
49535
|
}
|
|
@@ -49273,6 +49541,7 @@ class GodotServer {
|
|
|
49273
49541
|
projectPath: project.value.path,
|
|
49274
49542
|
lspPort: ports.lsp,
|
|
49275
49543
|
dapPort: ports.dap,
|
|
49544
|
+
arguments: opened.arguments,
|
|
49276
49545
|
finishesRestart: unfinished === null ? undefined : `This is the launch half of a restart begun at ${unfinished.quitAt} by server pid ${unfinished.byPid}, which quit editor pid ${unfinished.editorPid ?? "unknown"} and was ended before it could start one.`,
|
|
49277
49546
|
settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
|
|
49278
49547
|
});
|
|
@@ -49307,12 +49576,13 @@ class GodotServer {
|
|
|
49307
49576
|
}
|
|
49308
49577
|
return null;
|
|
49309
49578
|
}
|
|
49310
|
-
async openAnEditor(engine, projectPath, ports, hidden) {
|
|
49579
|
+
async openAnEditor(engine, projectPath, ports, hidden, silent) {
|
|
49311
49580
|
clearEditorLog(projectPath);
|
|
49312
49581
|
freeLibraryCopies(projectPath);
|
|
49582
|
+
const args = editorArguments(projectPath, ports, editorLogPath(projectPath), hidden, silent);
|
|
49313
49583
|
const launched = await launchOutsideTheTree({
|
|
49314
49584
|
command: engine,
|
|
49315
|
-
args
|
|
49585
|
+
args,
|
|
49316
49586
|
env: {
|
|
49317
49587
|
...process.env,
|
|
49318
49588
|
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
@@ -49323,16 +49593,17 @@ class GodotServer {
|
|
|
49323
49593
|
...hidden ? { desktop: HIDDEN_DESKTOP } : { noActivate: true }
|
|
49324
49594
|
});
|
|
49325
49595
|
if ("error" in launched) {
|
|
49326
|
-
return { pid: null, error: launched.error };
|
|
49596
|
+
return { pid: null, error: launched.error, arguments: args };
|
|
49327
49597
|
}
|
|
49328
49598
|
writeEditorLogNote(projectPath, launched.pid);
|
|
49329
49599
|
this.launchedEditor = {
|
|
49330
49600
|
pid: launched.pid,
|
|
49331
49601
|
startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null,
|
|
49332
|
-
projectPath
|
|
49602
|
+
projectPath,
|
|
49603
|
+
arguments: args
|
|
49333
49604
|
};
|
|
49334
49605
|
this.closedHere = null;
|
|
49335
|
-
return { pid: launched.pid, error: null };
|
|
49606
|
+
return { pid: launched.pid, error: null, arguments: args };
|
|
49336
49607
|
}
|
|
49337
49608
|
async portsForAnEditor() {
|
|
49338
49609
|
const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
|
|
@@ -49397,11 +49668,13 @@ class GodotServer {
|
|
|
49397
49668
|
if (!engine.ok) {
|
|
49398
49669
|
return engine.response;
|
|
49399
49670
|
}
|
|
49400
|
-
const
|
|
49671
|
+
const waited = await this.untilTheEditorsScanIsWritten(project.value.path);
|
|
49401
49672
|
const refreshed = await this.refreshStaleClasses(project.value.path);
|
|
49402
49673
|
if (!refreshed.ok) {
|
|
49403
49674
|
return refreshed.response;
|
|
49404
49675
|
}
|
|
49676
|
+
const imported = exported === null ? await this.importWhatChanged(project.value.path) : undefined;
|
|
49677
|
+
const scanned = await this.afterTheImport(project.value.path, waited, imported);
|
|
49405
49678
|
const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
|
|
49406
49679
|
if (op === "check") {
|
|
49407
49680
|
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, scanned, asked.value, flags.value);
|
|
@@ -49430,9 +49703,10 @@ class GodotServer {
|
|
|
49430
49703
|
});
|
|
49431
49704
|
const runtimeWaitMs = this.announceBudget(args, project.value.path, sceneArgument);
|
|
49432
49705
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
49433
|
-
const silent = resolveSilent(args["silent"]
|
|
49706
|
+
const silent = resolveSilent(args["silent"]) && !headless;
|
|
49707
|
+
const silentOnlyHere = silent && !editorPlaysHeadless(project.value.file);
|
|
49434
49708
|
const profiled = readBoolean(args, "profile") === true;
|
|
49435
|
-
if (exported === null && this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && flags.value.length === 0 && !needsItsOwnProcess(asked.value) && !
|
|
49709
|
+
if (exported === null && this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && flags.value.length === 0 && !needsItsOwnProcess(asked.value) && !silentOnlyHere) {
|
|
49436
49710
|
return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended, profiled);
|
|
49437
49711
|
}
|
|
49438
49712
|
const cmdArgs = runArguments({
|
|
@@ -49453,7 +49727,7 @@ class GodotServer {
|
|
|
49453
49727
|
started.announcedBefore = alreadyPlaying;
|
|
49454
49728
|
started.bootScene = runtimeWaitMs.scene;
|
|
49455
49729
|
this.activeProcess = started;
|
|
49456
|
-
const spawnedInstead = exported !== null ? " An exported build is started without a debugger, since a release build quits when given one, so breakpoints and debug_state do not reach it." : this.godotBridge.isConnected() && editorWouldPlay && (given.value.length > 0 || flags.value.length > 0) ? ` The editor cannot be handed arguments for a game it plays, so this one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? ` The editor plays a game with its own environment, so a run given one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay &&
|
|
49730
|
+
const spawnedInstead = exported !== null ? " An exported build is started without a debugger, since a release build quits when given one, so breakpoints and debug_state do not reach it." : this.godotBridge.isConnected() && editorWouldPlay && (given.value.length > 0 || flags.value.length > 0) ? ` The editor cannot be handed arguments for a game it plays, so this one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? ` The editor plays a game with its own environment, so a run given one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && silentOnlyHere ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here, silent as every run is unless asked otherwise, and silent: false plays it through the editor, with sound.${OWN_DEBUGGER}` : "";
|
|
49457
49731
|
const endedForThis = endedToStartThis(ended);
|
|
49458
49732
|
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
49459
49733
|
const handed = this.dapHoldingBreakpointsOf(project.value.path).breakpointsHeld();
|
|
@@ -49482,7 +49756,7 @@ class GodotServer {
|
|
|
49482
49756
|
}
|
|
49483
49757
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
49484
49758
|
const budgetMs = budget.ms;
|
|
49485
|
-
if (!
|
|
49759
|
+
if (!existsSync26(join33(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
49486
49760
|
return runtimeVerdict(null, {
|
|
49487
49761
|
addon: false,
|
|
49488
49762
|
budgetMs,
|
|
@@ -49501,7 +49775,7 @@ class GodotServer {
|
|
|
49501
49775
|
const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
|
|
49502
49776
|
const fresh = await announcedSince(projectPath, before, {
|
|
49503
49777
|
budgetMs: Math.max(runBegan + budgetMs - waitBegan, 0),
|
|
49504
|
-
accept: (one) => this.isTheGameOf(this.activeProcess, one
|
|
49778
|
+
accept: (one) => this.isTheGameOf(this.activeProcess, one),
|
|
49505
49779
|
giveUp: () => {
|
|
49506
49780
|
if (this.dapClient?.isStopped() === true) {
|
|
49507
49781
|
return true;
|
|
@@ -49568,7 +49842,7 @@ class GodotServer {
|
|
|
49568
49842
|
}
|
|
49569
49843
|
let announcedAt;
|
|
49570
49844
|
try {
|
|
49571
|
-
announcedAt =
|
|
49845
|
+
announcedAt = statSync15(endpoint.file).mtimeMs;
|
|
49572
49846
|
} catch {
|
|
49573
49847
|
return;
|
|
49574
49848
|
}
|
|
@@ -49751,7 +50025,7 @@ class GodotServer {
|
|
|
49751
50025
|
return;
|
|
49752
50026
|
}
|
|
49753
50027
|
const before = run.announcedBefore ?? new Set;
|
|
49754
|
-
const theOne = this.announcedOfTheRunsProject(run, runtimesAnnounced().running).find((one) => one.pid === Number(named) && !before.has(one.pid) && this.isTheGameOf(run, one
|
|
50028
|
+
const theOne = this.announcedOfTheRunsProject(run, runtimesAnnounced().running).find((one) => one.pid === Number(named) && !before.has(one.pid) && this.isTheGameOf(run, one));
|
|
49755
50029
|
if (theOne !== undefined) {
|
|
49756
50030
|
this.tie(run, theOne);
|
|
49757
50031
|
}
|
|
@@ -49762,7 +50036,7 @@ class GodotServer {
|
|
|
49762
50036
|
return 0;
|
|
49763
50037
|
}
|
|
49764
50038
|
try {
|
|
49765
|
-
return reportAlreadyInTranscript(
|
|
50039
|
+
return reportAlreadyInTranscript(readFileSync28(game.transcript), readFileSync28(report));
|
|
49766
50040
|
} catch (error) {
|
|
49767
50041
|
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
49768
50042
|
return 0;
|
|
@@ -49845,16 +50119,16 @@ class GodotServer {
|
|
|
49845
50119
|
return ended;
|
|
49846
50120
|
}
|
|
49847
50121
|
}
|
|
49848
|
-
const
|
|
50122
|
+
const { startedBy, command, projectPath } = running;
|
|
50123
|
+
const why = startedBy === undefined || command === undefined || projectPath === null ? "this server holds no engine, project and start of it to check the process against" : await whyNotStillTheRecordedRun({
|
|
49849
50124
|
pid: running.pid,
|
|
49850
50125
|
transcript: running.transcript ?? "",
|
|
49851
50126
|
startedAt: running.startedAt,
|
|
49852
|
-
|
|
49853
|
-
projectPath
|
|
50127
|
+
startedBy,
|
|
50128
|
+
projectPath,
|
|
49854
50129
|
arguments: [],
|
|
49855
|
-
|
|
49856
|
-
};
|
|
49857
|
-
const why = await whyNotStillTheRecordedRun(startedAs);
|
|
50130
|
+
command
|
|
50131
|
+
});
|
|
49858
50132
|
if (why !== null) {
|
|
49859
50133
|
clearTheRecord();
|
|
49860
50134
|
running.endedHere = null;
|
|
@@ -50139,7 +50413,7 @@ class GodotServer {
|
|
|
50139
50413
|
return run.announcedPid;
|
|
50140
50414
|
}
|
|
50141
50415
|
const before = run.announcedBefore ?? new Set;
|
|
50142
|
-
const candidates = ours.filter((one) => !before.has(one.pid) && this.isTheGameOf(run, one
|
|
50416
|
+
const candidates = ours.filter((one) => !before.has(one.pid) && this.isTheGameOf(run, one));
|
|
50143
50417
|
const theOne = candidates.length === 1 ? candidates[0] : undefined;
|
|
50144
50418
|
if (theOne !== undefined) {
|
|
50145
50419
|
this.tie(run, theOne);
|
|
@@ -50249,22 +50523,16 @@ class GodotServer {
|
|
|
50249
50523
|
run.announcedPid = endpoint.pid;
|
|
50250
50524
|
this.noteBootOf(run, endpoint);
|
|
50251
50525
|
}
|
|
50252
|
-
playedByOurEditor(endpoint
|
|
50526
|
+
playedByOurEditor(endpoint) {
|
|
50253
50527
|
const editor = this.godotBridge.getStatus().editorPid;
|
|
50254
|
-
|
|
50255
|
-
return true;
|
|
50256
|
-
}
|
|
50257
|
-
if (endpoint.editorPid !== undefined) {
|
|
50258
|
-
return endpoint.editorPid === editor;
|
|
50259
|
-
}
|
|
50260
|
-
return !runtimeAddonAnnouncesEditors(projectPath ?? this.ownProject);
|
|
50528
|
+
return editor === undefined || endpoint.editorPid === editor;
|
|
50261
50529
|
}
|
|
50262
|
-
isTheGameOf(run, endpoint
|
|
50530
|
+
isTheGameOf(run, endpoint) {
|
|
50263
50531
|
if (run === null) {
|
|
50264
50532
|
return false;
|
|
50265
50533
|
}
|
|
50266
50534
|
if (run.throughEditor) {
|
|
50267
|
-
return this.playedByOurEditor(endpoint
|
|
50535
|
+
return this.playedByOurEditor(endpoint);
|
|
50268
50536
|
}
|
|
50269
50537
|
const editor = this.godotBridge.getStatus().editorPid;
|
|
50270
50538
|
return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
|
|
@@ -50282,12 +50550,12 @@ class GodotServer {
|
|
|
50282
50550
|
log: new GameLog,
|
|
50283
50551
|
transcript: record.transcript,
|
|
50284
50552
|
readOffset: 0,
|
|
50285
|
-
projectPath: record.projectPath
|
|
50553
|
+
projectPath: record.projectPath,
|
|
50286
50554
|
startedAt: record.startedAt,
|
|
50287
|
-
|
|
50555
|
+
startedBy: record.startedBy,
|
|
50288
50556
|
...record.keeper === undefined ? {} : { keeper: record.keeper },
|
|
50289
50557
|
...record.profile === undefined ? {} : { profile: record.profile },
|
|
50290
|
-
|
|
50558
|
+
command: record.command,
|
|
50291
50559
|
exitCode: null,
|
|
50292
50560
|
exitSignal: null,
|
|
50293
50561
|
throughEditor: false,
|
|
@@ -50340,9 +50608,8 @@ class GodotServer {
|
|
|
50340
50608
|
if (record === null) {
|
|
50341
50609
|
return `No game is running.${noEditor} Start one with editor_run.`;
|
|
50342
50610
|
}
|
|
50343
|
-
const whose = record.projectPath === "" ? "a project it does not name" : record.projectPath;
|
|
50344
50611
|
const ours = mine === null ? "this server was started without GDHARNESS_PROJECT and has no editor connected, so it cannot say whose that run is" : `this server serves ${mine}`;
|
|
50345
|
-
return `No game of this server's is running.${noEditor} A run started from ${
|
|
50612
|
+
return `No game of this server's is running.${noEditor} A run started from ${record.projectPath} is recorded on this machine, and ${ours}, so it is not this server's to answer for or to end. Start one with editor_run.`;
|
|
50346
50613
|
}
|
|
50347
50614
|
aRuntimeOfOursIsAnswering() {
|
|
50348
50615
|
return this.announcedForOurProject(discoverRuntimes());
|
|
@@ -50464,7 +50731,7 @@ class GodotServer {
|
|
|
50464
50731
|
};
|
|
50465
50732
|
const going = await this.runStillGoing(run);
|
|
50466
50733
|
const projectPath = run.projectPath;
|
|
50467
|
-
const projectGone = projectPath !== null && !
|
|
50734
|
+
const projectGone = projectPath !== null && !existsSync26(join33(projectPath, "project.godot"));
|
|
50468
50735
|
if (projectGone && callersName !== undefined) {
|
|
50469
50736
|
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"]);
|
|
50470
50737
|
}
|
|
@@ -50811,7 +51078,7 @@ class GodotServer {
|
|
|
50811
51078
|
notes.push(endedWithoutACode({
|
|
50812
51079
|
throughEditor: run.throughEditor,
|
|
50813
51080
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
50814
|
-
couldAnnounce: run.projectPath !== null &&
|
|
51081
|
+
couldAnnounce: run.projectPath !== null && existsSync26(join33(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
50815
51082
|
endedHere: typeof run.endedHere === "string",
|
|
50816
51083
|
pickedUp: run.pickedUp === true
|
|
50817
51084
|
}));
|
|
@@ -51128,10 +51395,9 @@ class GodotServer {
|
|
|
51128
51395
|
idleSince = null;
|
|
51129
51396
|
continue;
|
|
51130
51397
|
}
|
|
51131
|
-
const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
|
|
51132
51398
|
const completed = readNumber(status, "scansCompleted");
|
|
51133
51399
|
const notYetOver = completedBefore !== undefined && completed !== undefined && completed <= completedBefore;
|
|
51134
|
-
busy = Boolean(status["scanning"]) || Boolean(status["importing"]) ||
|
|
51400
|
+
busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || notYetOver;
|
|
51135
51401
|
if (busy) {
|
|
51136
51402
|
idleSince = null;
|
|
51137
51403
|
} else {
|
|
@@ -51191,7 +51457,7 @@ class GodotServer {
|
|
|
51191
51457
|
constantsBefore: listed("heldConstantsBefore")["heldConstantsBefore"] ?? [],
|
|
51192
51458
|
constantsAfter: listed("constants")["constants"] ?? [],
|
|
51193
51459
|
names: projectPath === "" ? [] : this.classesAScriptNames(projectPath, script),
|
|
51194
|
-
restored:
|
|
51460
|
+
restored: answer["restored"] === true,
|
|
51195
51461
|
restoredAs: readString(answer, "restoredAs") ?? ""
|
|
51196
51462
|
})
|
|
51197
51463
|
};
|
|
@@ -51295,7 +51561,7 @@ class GodotServer {
|
|
|
51295
51561
|
}
|
|
51296
51562
|
const failed = answer["failed"];
|
|
51297
51563
|
if (typeof failed === "number" && failed !== 0) {
|
|
51298
|
-
const left = answer["restored"] === true ? "" :
|
|
51564
|
+
const left = answer["restored"] === true ? "" : "; compiling it again from the text it was built from failed too, so the editor holds it unusable until it compiles";
|
|
51299
51565
|
notReloaded.push({
|
|
51300
51566
|
scriptPath,
|
|
51301
51567
|
problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}${left}`
|
|
@@ -51441,7 +51707,7 @@ class GodotServer {
|
|
|
51441
51707
|
if (!scriptPath.ok) {
|
|
51442
51708
|
return this.createErrorResponse(scriptPath.reason);
|
|
51443
51709
|
}
|
|
51444
|
-
if (!
|
|
51710
|
+
if (!existsSync26(scriptPath.absolutePath)) {
|
|
51445
51711
|
return this.createErrorResponse(`Script file does not exist: ${scriptPath.absolutePath}`);
|
|
51446
51712
|
}
|
|
51447
51713
|
const asked = readNonEmptyString(args, "newScriptPath");
|
|
@@ -51855,7 +52121,7 @@ class GodotServer {
|
|
|
51855
52121
|
}
|
|
51856
52122
|
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
51857
52123
|
const declared = new RegExp(`^\\s*(?:@\\w+(?:\\([^)]*\\))?\\s+)*class_name\\s+${escaped}\\b`, "m");
|
|
51858
|
-
if (declared.test(
|
|
52124
|
+
if (declared.test(readFileSync28(scriptPath, "utf8"))) {
|
|
51859
52125
|
return null;
|
|
51860
52126
|
}
|
|
51861
52127
|
const checked = await this.classesTheEditorCannotSee(args);
|
|
@@ -51954,20 +52220,18 @@ class GodotServer {
|
|
|
51954
52220
|
]);
|
|
51955
52221
|
}
|
|
51956
52222
|
try {
|
|
51957
|
-
return this.jsonTextResponse(
|
|
52223
|
+
return this.jsonTextResponse(await this.godotBridge.invokeTool(toolName, args));
|
|
51958
52224
|
} catch (error) {
|
|
51959
52225
|
if (error instanceof BridgeTimeout) {
|
|
51960
52226
|
return error;
|
|
51961
52227
|
}
|
|
52228
|
+
if (error instanceof AddonOfAnotherRelease) {
|
|
52229
|
+
return this.createErrorResponse(error.message, ["editor_status says which addon the editor holds"]);
|
|
52230
|
+
}
|
|
51962
52231
|
const message = errorMessage(error);
|
|
51963
52232
|
if (message.includes(`Unknown tool: ${toolName}`)) {
|
|
51964
|
-
const
|
|
51965
|
-
|
|
51966
|
-
if (remedy === undefined) {
|
|
51967
|
-
const log = open === undefined || open === "" ? "the editor's Output panel" : `${editorLogPath(open)} for an editor editor_launch opened, or the Output panel of one opened by hand`;
|
|
51968
|
-
return this.createErrorResponse(`The editor's gdharness addon is this ${SERVER_VERSION} server's own and still has no ${named}, so a script of the addon failed to load in this editor and the commands it holds were never registered. The script and the error are in ${log}.`, ["editor_status says which addon the editor holds"]);
|
|
51969
|
-
}
|
|
51970
|
-
return this.createErrorResponse(`The editor's gdharness addon is older than this ${SERVER_VERSION} server and has no ${named}, so it cannot answer this call. ${remedy}`, ["editor_status says which addon the editor holds"]);
|
|
52233
|
+
const log = open === undefined || open === "" ? "the editor's Output panel" : `${editorLogPath(open)} for an editor editor_launch opened, or the Output panel of one opened by hand`;
|
|
52234
|
+
return this.createErrorResponse(`The editor's gdharness addon is this ${SERVER_VERSION} server's own and still has no ${named}, so a script of the addon failed to load in this editor and the commands it holds were never registered. The script and the error are in ${log}.`, ["editor_status says which addon the editor holds"]);
|
|
51971
52235
|
}
|
|
51972
52236
|
return this.createErrorResponse(`The editor answered ${named} with an error: ${message}`);
|
|
51973
52237
|
}
|
|
@@ -52023,7 +52287,7 @@ class GodotServer {
|
|
|
52023
52287
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
52024
52288
|
const run = await this.currentRun();
|
|
52025
52289
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
52026
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
52290
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync26(join33(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
|
|
52027
52291
|
return null;
|
|
52028
52292
|
}
|
|
52029
52293
|
const already = runtimesAnnounced().running;
|
|
@@ -52032,12 +52296,12 @@ class GodotServer {
|
|
|
52032
52296
|
return !run.announcedBefore.has(one.pid);
|
|
52033
52297
|
}
|
|
52034
52298
|
try {
|
|
52035
|
-
return
|
|
52299
|
+
return statSync15(one.file).mtimeMs >= run.startedAt;
|
|
52036
52300
|
} catch {
|
|
52037
52301
|
return false;
|
|
52038
52302
|
}
|
|
52039
52303
|
};
|
|
52040
|
-
if (already.some((one) => announcedSinceTheStart(one) && this.isTheGameOf(run, one
|
|
52304
|
+
if (already.some((one) => announcedSinceTheStart(one) && this.isTheGameOf(run, one))) {
|
|
52041
52305
|
return null;
|
|
52042
52306
|
}
|
|
52043
52307
|
const which = run.throughEditor ? "the game the editor is playing for it" : `pid ${run.pid ?? "unknown"}`;
|
|
@@ -52049,7 +52313,7 @@ class GodotServer {
|
|
|
52049
52313
|
}
|
|
52050
52314
|
const fresh = await announcedSince(project, new Set(already.map((one) => one.pid)), {
|
|
52051
52315
|
budgetMs: Math.min(budgetMs, remaining),
|
|
52052
|
-
accept: (one) => this.isTheGameOf(this.activeProcess, one
|
|
52316
|
+
accept: (one) => this.isTheGameOf(this.activeProcess, one),
|
|
52053
52317
|
giveUp: () => this.activeProcess !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
|
|
52054
52318
|
});
|
|
52055
52319
|
const endpoint = await this.theRunsAmong(run, fresh);
|
|
@@ -52112,13 +52376,15 @@ class GodotServer {
|
|
|
52112
52376
|
return this.createErrorResponse(refusal);
|
|
52113
52377
|
}
|
|
52114
52378
|
}
|
|
52115
|
-
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
52116
|
-
const screenshotPath = screenshotDir ? join32(screenshotDir, "capture.png") : null;
|
|
52117
52379
|
const staleNote = this.runtimeStaleNote(choice.endpoint);
|
|
52118
|
-
|
|
52119
|
-
|
|
52120
|
-
|
|
52121
|
-
|
|
52380
|
+
if (staleNote !== undefined) {
|
|
52381
|
+
return this.createErrorResponse(staleNote, [
|
|
52382
|
+
"editor_status lists the game and the runtime addon it loaded"
|
|
52383
|
+
]);
|
|
52384
|
+
}
|
|
52385
|
+
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
52386
|
+
const screenshotPath = screenshotDir ? join33(screenshotDir, "capture.png") : null;
|
|
52387
|
+
const refused = (message) => this.createErrorResponse(message);
|
|
52122
52388
|
const onGame = {
|
|
52123
52389
|
pid: choice.endpoint.pid,
|
|
52124
52390
|
what: `${asked.tool} ${asked.op}`,
|
|
@@ -52144,7 +52410,6 @@ ${staleNote}`);
|
|
|
52144
52410
|
waitedMs: timeoutMs,
|
|
52145
52411
|
note: `${reply.message} ${longerWaitNote(asked.tool, asked.op, timeoutMs)}`,
|
|
52146
52412
|
...answeredBy,
|
|
52147
|
-
...stale,
|
|
52148
52413
|
...alongside()
|
|
52149
52414
|
});
|
|
52150
52415
|
}
|
|
@@ -52153,7 +52418,7 @@ ${staleNote}`);
|
|
|
52153
52418
|
}
|
|
52154
52419
|
const { id: _id, ...payload } = reply.payload;
|
|
52155
52420
|
if (!expectsScreenshot) {
|
|
52156
|
-
return this.jsonTextResponse({ ...payload, ...answeredBy, ...
|
|
52421
|
+
return this.jsonTextResponse({ ...payload, ...answeredBy, ...alongside() });
|
|
52157
52422
|
}
|
|
52158
52423
|
const returnedPath = readString(payload, "path");
|
|
52159
52424
|
if (readString(payload, "type") !== "screenshot_file" || !returnedPath || !screenshotPath) {
|
|
@@ -52177,11 +52442,9 @@ ${staleNote}`);
|
|
|
52177
52442
|
type: "text",
|
|
52178
52443
|
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${ranWith.length === 0 ? "" : `
|
|
52179
52444
|
|
|
52180
|
-
Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}
|
|
52181
|
-
|
|
52182
|
-
${staleNote}`}`
|
|
52445
|
+
Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}`
|
|
52183
52446
|
},
|
|
52184
|
-
{ type: "image", data:
|
|
52447
|
+
{ type: "image", data: readFileSync28(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
52185
52448
|
]
|
|
52186
52449
|
};
|
|
52187
52450
|
} finally {
|
|
@@ -52298,7 +52561,7 @@ async function runGodotServer() {
|
|
|
52298
52561
|
const server = new GodotServer;
|
|
52299
52562
|
await server.run();
|
|
52300
52563
|
}
|
|
52301
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, 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,
|
|
52564
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, 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_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, OUTPUT_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;
|
|
52302
52565
|
var init_server = __esm(() => {
|
|
52303
52566
|
init_dist();
|
|
52304
52567
|
init_stdio();
|
|
@@ -52327,6 +52590,7 @@ var init_server = __esm(() => {
|
|
|
52327
52590
|
init_godot_path();
|
|
52328
52591
|
init_headless();
|
|
52329
52592
|
init_headless_operations();
|
|
52593
|
+
init_import_freshness();
|
|
52330
52594
|
init_issues();
|
|
52331
52595
|
init_junit();
|
|
52332
52596
|
init_keeper_channel();
|
|
@@ -52388,6 +52652,7 @@ var init_server = __esm(() => {
|
|
|
52388
52652
|
PROJECT_FILE_ARGUMENTS = [
|
|
52389
52653
|
"scenePath",
|
|
52390
52654
|
"instancePath",
|
|
52655
|
+
"inherits",
|
|
52391
52656
|
"scriptPath",
|
|
52392
52657
|
"resourcePath",
|
|
52393
52658
|
"newPath",
|
|
@@ -52418,8 +52683,8 @@ var init_server = __esm(() => {
|
|
|
52418
52683
|
init_bridge_announce();
|
|
52419
52684
|
init_errors();
|
|
52420
52685
|
init_godot_path();
|
|
52421
|
-
import { existsSync as
|
|
52422
|
-
import { join as
|
|
52686
|
+
import { existsSync as existsSync27 } from "node:fs";
|
|
52687
|
+
import { join as join34, resolve as resolve8 } from "node:path";
|
|
52423
52688
|
|
|
52424
52689
|
// src/harnesses.ts
|
|
52425
52690
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -53414,10 +53679,10 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
53414
53679
|
- \`editor_status\` says whether an editor is connected and whether its addon matches the server.
|
|
53415
53680
|
\`addonIsStale\` means the editor is running different addon code from what the server ships
|
|
53416
53681
|
(an older \`addonVersion\` on the same code is not stale), and \`staleNote\` says which half is behind: an editor that
|
|
53417
|
-
needs restarting,
|
|
53418
|
-
|
|
53419
|
-
|
|
53420
|
-
|
|
53682
|
+
needs restarting, a project that needs the upgrade command, or a server that needs reconnecting
|
|
53683
|
+
in your harness. A stale editor is asked nothing but to quit, and every other call to it is
|
|
53684
|
+
refused with the same note. A game's runtime addon is judged the same way, under each entry in
|
|
53685
|
+
\`runtimes\`, and a \`runtime_*\` call to a stale game is refused with its note.
|
|
53421
53686
|
- Start the game with \`editor_run start\`, never by spawning an engine. The editor plays it, so
|
|
53422
53687
|
its debugger holds it, which is what gives the \`debug_*\` tools something to talk to. Its answer
|
|
53423
53688
|
says under \`runtime\` whether the game can be talked to yet; when it cannot, \`mayYetAnnounce\`
|
|
@@ -53703,7 +53968,7 @@ class UsageError extends Refusal {
|
|
|
53703
53968
|
function projectArgument(at) {
|
|
53704
53969
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
53705
53970
|
const projectPath = resolve8(named ?? ".");
|
|
53706
|
-
if (!
|
|
53971
|
+
if (!existsSync27(join34(projectPath, "project.godot"))) {
|
|
53707
53972
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
53708
53973
|
}
|
|
53709
53974
|
return projectPath;
|
|
@@ -53711,11 +53976,11 @@ function projectArgument(at) {
|
|
|
53711
53976
|
async function engine(projectPath) {
|
|
53712
53977
|
const located = await new GodotLocator().find();
|
|
53713
53978
|
if (located.ok) {
|
|
53714
|
-
return { godotPath: located.path, script: shippedOperationsScript(), debug:
|
|
53979
|
+
return { godotPath: located.path, script: shippedOperationsScript(), debug: DEBUG_MODE };
|
|
53715
53980
|
}
|
|
53716
53981
|
const recorded = located.named === null && projectPath !== undefined ? recordedEnginePath(projectPath) : null;
|
|
53717
53982
|
if (recorded !== null) {
|
|
53718
|
-
return { godotPath: recorded, script: shippedOperationsScript(), debug:
|
|
53983
|
+
return { godotPath: recorded, script: shippedOperationsScript(), debug: DEBUG_MODE };
|
|
53719
53984
|
}
|
|
53720
53985
|
const main = located.named === null && projectPath !== undefined ? mainCheckoutOf(projectPath) : null;
|
|
53721
53986
|
const theirs = main === null ? null : recordedEnginePath(main);
|
|
@@ -53724,7 +53989,7 @@ async function engine(projectPath) {
|
|
|
53724
53989
|
throw new UsageError(`${refusal.message} ${refusal.advice.join(". ")}.`);
|
|
53725
53990
|
}
|
|
53726
53991
|
console.error(`engine: ${theirs}, which the harness config of ${main} names; this project is a git worktree of it`);
|
|
53727
|
-
return { godotPath: theirs, script: shippedOperationsScript(), debug:
|
|
53992
|
+
return { godotPath: theirs, script: shippedOperationsScript(), debug: DEBUG_MODE };
|
|
53728
53993
|
}
|
|
53729
53994
|
function said2(outcome, what) {
|
|
53730
53995
|
if (!outcome.ok) {
|
|
@@ -53748,7 +54013,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
53748
54013
|
return;
|
|
53749
54014
|
}
|
|
53750
54015
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
53751
|
-
if (!
|
|
54016
|
+
if (!existsSync27(join34(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
53752
54017
|
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}`);
|
|
53753
54018
|
}
|
|
53754
54019
|
}
|
|
@@ -53880,7 +54145,7 @@ async function setup() {
|
|
|
53880
54145
|
console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
|
|
53881
54146
|
}
|
|
53882
54147
|
if (!args.includes("--no-skill")) {
|
|
53883
|
-
const directories = skillDirectories(chosen.harnesses, projectPath,
|
|
54148
|
+
const directories = skillDirectories(chosen.harnesses, projectPath, existsSync27);
|
|
53884
54149
|
for (const written of writeSkill(directories, getLocalVersion())) {
|
|
53885
54150
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
53886
54151
|
}
|
|
@@ -53994,14 +54259,14 @@ async function upgrade() {
|
|
|
53994
54259
|
moved.push(took.from);
|
|
53995
54260
|
}
|
|
53996
54261
|
}
|
|
53997
|
-
for (const written of writeSkill(skillDirectories(already, projectPath,
|
|
54262
|
+
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync27), version)) {
|
|
53998
54263
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
53999
54264
|
}
|
|
54000
54265
|
console.log(`
|
|
54001
54266
|
On ${version}. What is left for you:`);
|
|
54002
54267
|
console.log(digestBefore !== null && digestBefore === installedEditorDigest(projectPath) ? ` 1. Nothing for the editor: this upgrade left the editor addons the same code as before,
|
|
54003
54268
|
` + ` so it gives an open editor nothing to restart for. editor_status says addonIsStale
|
|
54004
|
-
` + " if the editor is
|
|
54269
|
+
` + " if the editor is running other code." : ` 1. The open editor is still running the addons it loaded at startup. Restart it with the
|
|
54005
54270
|
` + " editor_launch restart tool, which closes and reopens the window.");
|
|
54006
54271
|
const serving = await serverServing(projectPath);
|
|
54007
54272
|
console.log(harnessNote({ moved, written, byHand }, [launch.command, ...launch.args].join(" "), serving === null ? null : { server: serving, isThisVersion: serving.version === version }));
|