gdharness 1.2.42 → 1.3.1
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 +571 -363
- package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -19
- package/build/godot/addons/gdharness_editor/bridge_client.gd +9 -9
- package/build/godot/addons/gdharness_editor/file_reads.gd +60 -25
- package/build/godot/addons/gdharness_editor/headless_check.gd +7 -7
- package/build/godot/addons/gdharness_editor/plugin.gd +6 -6
- package/build/godot/addons/gdharness_editor/property_values.gd +124 -62
- package/build/godot/addons/gdharness_editor/reading.gd +28 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +28 -23
- package/build/godot/addons/gdharness_editor/serialisation.gd +45 -94
- package/build/godot/addons/gdharness_editor/tool_executor.gd +24 -24
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +52 -41
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +4 -4
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +2 -2
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +14 -14
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +57 -50
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +17 -17
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +14 -12
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +7 -7
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +82 -78
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +4 -4
- package/build/godot/addons/gdharness_editor/value_shapes.gd +111 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +28 -0
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +18 -18
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +32 -32
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +50 -18
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +86 -80
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +65 -69
- package/build/godot/addons/gdharness_runtime/runtime_items.gd +6 -6
- package/build/godot/addons/gdharness_runtime/runtime_menus.gd +8 -8
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +13 -21
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +10 -10
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +7 -7
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +41 -41
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +3 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +33 -33
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +20 -20
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +25 -24
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +56 -50
- package/build/godot/addons/gdharness_runtime/serialisation.gd +45 -94
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +111 -0
- package/build/godot/addons/gdharness_runtime/value_shapes.gd.uid +1 -0
- package/build/godot/operations/animation_reads.gd +7 -19
- package/build/godot/operations/audio_buses.gd +10 -10
- package/build/godot/operations/class_cache.gd +16 -16
- package/build/godot/operations/classdb_queries.gd +12 -12
- package/build/godot/operations/dependencies.gd +33 -29
- package/build/godot/operations/editor_tools.gd +12 -12
- package/build/godot/operations/export_presets.gd +10 -8
- package/build/godot/operations/file_reads.gd +60 -25
- package/build/godot/operations/gdscript_analysis.gd +20 -20
- package/build/godot/operations/gdscript_authoring.gd +17 -15
- package/build/godot/operations/godot_operations.gd +64 -63
- package/build/godot/operations/import_pipeline.gd +20 -20
- package/build/godot/operations/input_actions.gd +17 -17
- package/build/godot/operations/logger.gd +1 -1
- package/build/godot/operations/plugins.gd +8 -8
- package/build/godot/operations/project_config.gd +24 -17
- package/build/godot/operations/project_diagnostics.gd +9 -9
- package/build/godot/operations/property_values.gd +124 -62
- package/build/godot/operations/reading.gd +28 -0
- package/build/godot/operations/rename_checks.gd +3 -3
- package/build/godot/operations/resource_files.gd +5 -5
- package/build/godot/operations/script_check.gd +7 -7
- package/build/godot/operations/serialisation.gd +45 -94
- package/build/godot/operations/value_shapes.gd +111 -0
- package/build/keeper.js +10 -4
- package/package.json +1 -2
- package/build/index.js +0 -45101
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.";
|
|
@@ -11568,24 +11555,12 @@ function addonsDigest(names, from) {
|
|
|
11568
11555
|
return hash.digest("hex");
|
|
11569
11556
|
}
|
|
11570
11557
|
function shippedEditorDigest() {
|
|
11571
|
-
|
|
11572
|
-
|
|
11573
|
-
shippedDigest = editorAddonDigest();
|
|
11574
|
-
} catch {
|
|
11575
|
-
shippedDigest = null;
|
|
11576
|
-
}
|
|
11577
|
-
}
|
|
11578
|
-
return shippedDigest ?? undefined;
|
|
11558
|
+
shippedDigest ??= editorAddonDigest();
|
|
11559
|
+
return shippedDigest;
|
|
11579
11560
|
}
|
|
11580
11561
|
function shippedRuntimeDigest() {
|
|
11581
|
-
|
|
11582
|
-
|
|
11583
|
-
shippedRuntime = runtimeAddonDigest();
|
|
11584
|
-
} catch {
|
|
11585
|
-
shippedRuntime = null;
|
|
11586
|
-
}
|
|
11587
|
-
}
|
|
11588
|
-
return shippedRuntime ?? undefined;
|
|
11562
|
+
shippedRuntime ??= runtimeAddonDigest();
|
|
11563
|
+
return shippedRuntime;
|
|
11589
11564
|
}
|
|
11590
11565
|
function installedEditorDigest(projectPath) {
|
|
11591
11566
|
return digestMarker(projectPath, ADDONS[0]);
|
|
@@ -11899,7 +11874,7 @@ function annotationsOf(spec) {
|
|
|
11899
11874
|
openWorldHint: false
|
|
11900
11875
|
};
|
|
11901
11876
|
}
|
|
11902
|
-
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.", 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;
|
|
11903
11878
|
var init_tool_definitions = __esm(() => {
|
|
11904
11879
|
init_launch();
|
|
11905
11880
|
PROJECT_INFO_SECTIONS = [
|
|
@@ -11937,7 +11912,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11937
11912
|
};
|
|
11938
11913
|
PROPERTIES = {
|
|
11939
11914
|
type: "object",
|
|
11940
|
-
description: `Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. A key may also be a colon path into one of the components of a vector, a colour, a rectangle or a transform, as GDScript names them, so one axis is written alone: "position:x", "modulate:a", "region_rect:size:y"; a component the value does not have is refused, naming the ones it has. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds. ${WHOLE_NUMBERS}`,
|
|
11915
|
+
description: `Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. One carrying a field its type has not, such as a z for a Vector2, is refused naming it. A colour may also be given as its html code, "#ff8000" or "#ff800080". A key may also be a colon path into one of the components of a vector, a colour, a rectangle or a transform, as GDScript names them, so one axis is written alone: "position:x", "modulate:a", "region_rect:size:y"; a component the value does not have is refused, naming the ones it has. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds. ${WHOLE_NUMBERS}`,
|
|
11941
11916
|
additionalProperties: true
|
|
11942
11917
|
};
|
|
11943
11918
|
XY = {
|
|
@@ -12090,7 +12065,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12090
12065
|
{
|
|
12091
12066
|
name: "project_settings",
|
|
12092
12067
|
title: "Project settings",
|
|
12093
|
-
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.",
|
|
12094
12069
|
parameters: {
|
|
12095
12070
|
projectPath: PROJECT_PATH,
|
|
12096
12071
|
setting: {
|
|
@@ -12112,7 +12087,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12112
12087
|
value: {
|
|
12113
12088
|
blank: true,
|
|
12114
12089
|
ops: ["set"],
|
|
12115
|
-
description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}, and infinity as {"_type": "float", "value": "inf"}, the form a read answers with. "" writes an empty string.'
|
|
12090
|
+
description: 'The value to write, fitted to the type the setting is registered as, the way scene_node set fits a property. Engine types may be written as their fields, {"x": 1, "y": 2}, or tagged, {"_type": "Vector2", "x": 1, "y": 2}, and infinity as {"_type": "float", "value": "inf"}, the form a read answers with; one carrying a field its type has not is refused naming it, and a colour may also be its html code, "#ff8000". "" writes an empty string.'
|
|
12116
12091
|
},
|
|
12117
12092
|
name: { type: "string", ops: ["add_autoload", "remove_autoload"], description: "Autoload name." },
|
|
12118
12093
|
path: {
|
|
@@ -12371,8 +12346,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
12371
12346
|
name: "project_test",
|
|
12372
12347
|
title: "Run tests",
|
|
12373
12348
|
effect: "additive",
|
|
12374
|
-
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
|
|
12375
|
-
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.",
|
|
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. What changed on disk since it was imported is then imported before any suite runs, and named under importedFirst. 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, 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. 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.",
|
|
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,
|
|
12376
12351
|
parameters: {
|
|
12377
12352
|
projectPath: PROJECT_PATH,
|
|
12378
12353
|
path: {
|
|
@@ -12462,7 +12437,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12462
12437
|
{
|
|
12463
12438
|
name: "scene_node",
|
|
12464
12439
|
title: "Scene nodes",
|
|
12465
|
-
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}`,
|
|
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. Other properties the engine changed in step, as a Control's offsets with its size, come back under alsoChanged as {was, now}.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}`,
|
|
12466
12441
|
parameters: {
|
|
12467
12442
|
projectPath: PROJECT_PATH,
|
|
12468
12443
|
scenePath: SCENE_PATH,
|
|
@@ -12807,7 +12782,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12807
12782
|
scriptPath: {
|
|
12808
12783
|
type: ["string", "array"],
|
|
12809
12784
|
items: { type: "string" },
|
|
12810
|
-
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.'
|
|
12811
12786
|
}
|
|
12812
12787
|
},
|
|
12813
12788
|
requires: ["projectPath", "scriptPath"]
|
|
@@ -12815,7 +12790,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12815
12790
|
{
|
|
12816
12791
|
name: "resource_edit",
|
|
12817
12792
|
title: "Edit resources",
|
|
12818
|
-
description: 'Resource files: read one, create any ClassDB resource as .tres, change one, write a shader, build a TileSet, or set a Theme colour or font size. A material is a create with resourceType StandardMaterial3D, ShaderMaterial or CanvasItemMaterial. read answers with the properties the file sets, in the form modify takes, and a Theme lists its items there as "Button/colors/font_color". move takes any file of the project, a script or an image as much as a resource, or a directory, and works from the files, so it needs no editor; one serving the project is brought up to it as after a move in its own FileSystem dock, its open scenes and cached resources named for the new paths and the project settings the move rewrote taken from the file. read needs no editor either: with none serving the project, a headless engine reads the file. The ops that write a resource go through the editor when one serves the project; with none, a headless engine runs the tool the editor would and the answer says so under writtenBy. That engine runs the script of the resource, 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 the save, and a resource where that cannot be done is refused and the file left as it was.',
|
|
12793
|
+
description: 'Resource files: read one, create any ClassDB resource as .tres, change one, write a shader, build a TileSet, or set a Theme colour or font size. A material is a create with resourceType StandardMaterial3D, ShaderMaterial or CanvasItemMaterial. read answers with the properties the file sets, in the form modify takes, and a Theme lists its items there as "Button/colors/font_color". A create or modify whose properties made the engine change others, as a Gradient given more colours than offsets gives each new point an offset of 0, names each of those under alsoChanged with what it held before and holds now. move takes any file of the project, a script or an image as much as a resource, or a directory, and works from the files, so it needs no editor; one serving the project is brought up to it as after a move in its own FileSystem dock, its open scenes and cached resources named for the new paths and the project settings the move rewrote taken from the file. read needs no editor either: with none serving the project, a headless engine reads the file. The ops that write a resource go through the editor when one serves the project; with none, a headless engine runs the tool the editor would and the answer says so under writtenBy. That engine runs the script of the resource, 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 the save, and a resource where that cannot be done is refused and the file left as it was.',
|
|
12819
12794
|
parameters: {
|
|
12820
12795
|
projectPath: PROJECT_PATH,
|
|
12821
12796
|
resourcePath: {
|
|
@@ -12924,6 +12899,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
12924
12899
|
type: "boolean",
|
|
12925
12900
|
ops: ["open", "restart"],
|
|
12926
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."
|
|
12927
12907
|
}
|
|
12928
12908
|
},
|
|
12929
12909
|
requires: [],
|
|
@@ -12945,7 +12925,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12945
12925
|
{
|
|
12946
12926
|
name: "editor_run",
|
|
12947
12927
|
title: "Run the game",
|
|
12948
|
-
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. start and check both wait first for a scan the open editor is running and for the class cache it writes just after, since a game booted in between resolves no global class, as does every other engine this server starts on the project; waitedForEditorScanMs says how long, and scanNote says when a scan ran past thirty seconds and the game started during it. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for, and the note then says what a runtime_* call naming no projectPath meets until it announces. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
12928
|
+
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. start and check both wait first for a scan the open editor is running and for the class cache it writes just after, since a game booted in between resolves no global class, as does every other engine this server starts on the project; waitedForEditorScanMs says how long, and scanNote says when a scan ran past thirty seconds and the game started during it. Then whatever changed on disk since it was imported is imported, and named under importedFirst. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for, and the note then says what a runtime_* call naming no projectPath meets until it announces. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
12949
12929
|
parameters: {
|
|
12950
12930
|
projectPath: {
|
|
12951
12931
|
...PROJECT_PATH,
|
|
@@ -12978,7 +12958,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12978
12958
|
silent: {
|
|
12979
12959
|
type: "boolean",
|
|
12980
12960
|
ops: ["start"],
|
|
12981
|
-
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."
|
|
12982
12962
|
},
|
|
12983
12963
|
executable: {
|
|
12984
12964
|
type: "string",
|
|
@@ -13048,7 +13028,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13048
13028
|
graceful: {
|
|
13049
13029
|
type: "boolean",
|
|
13050
13030
|
ops: ["start", "stop"],
|
|
13051
|
-
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."
|
|
13052
13032
|
},
|
|
13053
13033
|
pid: {
|
|
13054
13034
|
type: "integer",
|
|
@@ -13085,13 +13065,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
13085
13065
|
requires: []
|
|
13086
13066
|
}
|
|
13087
13067
|
},
|
|
13088
|
-
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. runtime carries addonIsStale and staleNote when the game announced on a runtime addon other than this server's, which no runtime_* call is then sent to, and staleNote says what brings it into step. 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). exitedBeforeStop is true when the run was already over when the stop came, so nothing was ended and exitCode, when anything collected it, is the one it ended with. 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, and durationMs runs from that engine's start to the check's answer, after its exit, so it holds no import that came first. start and check: " + IMPORTED_FIRST + " An exported build carries its own imports and is not asked. waitedForEditorScanMs and scanNote are left out when no editor scan was waited for. A silent windowed run has --audio-driver Dummy in its arguments; a headless run is silent without it, and one the editor plays with sound has no arguments of its own. 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.",
|
|
13089
13069
|
defaultOperation: "start"
|
|
13090
13070
|
},
|
|
13091
13071
|
{
|
|
13092
13072
|
name: "editor_output",
|
|
13093
13073
|
title: "Game and editor output",
|
|
13094
13074
|
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries: clean is true when no error was reported, and warnings, counted beside it, do not make it false. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
|
|
13075
|
+
answer: "Each entry's source says where it was read: transcript for a line of the run's transcript file, which holds both its streams and is how every run with one is read; stdout or stderr for a line of that stream of a run with no transcript, such as an editor-played run picked up after a reconnect whose first server left no note; debugger for what the editor's debug adapter reported and the game never printed, such as the script error a game stopped on.",
|
|
13095
13076
|
parameters: {
|
|
13096
13077
|
projectPath: {
|
|
13097
13078
|
type: "string",
|
|
@@ -13148,7 +13129,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13148
13129
|
title: "Editor status",
|
|
13149
13130
|
effect: "read",
|
|
13150
13131
|
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.",
|
|
13151
|
-
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.
|
|
13132
|
+
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.",
|
|
13152
13133
|
parameters: {},
|
|
13153
13134
|
requires: []
|
|
13154
13135
|
},
|
|
@@ -13369,7 +13350,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13369
13350
|
value: {
|
|
13370
13351
|
ops: ["set"],
|
|
13371
13352
|
blank: true,
|
|
13372
|
-
description: `set: the value, fitted to the property's type. ${WHOLE_NUMBERS} A number the property would have to cut is refused rather than truncated: a fraction into an int, a number other than 0 or 1 into a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent, a fraction into one holding an int included. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
|
|
13353
|
+
description: `set: the value, fitted to the property's type. ${WHOLE_NUMBERS} A number the property would have to cut is refused rather than truncated: a fraction into an int, a number other than 0 or 1 into a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with, and one carrying a field its type has not, such as a z for a Vector2, is refused naming it. A colour may also be given as its html code, "#ff8000" or "#ff800080". "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent, a fraction into one holding an int included. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
|
|
13373
13354
|
},
|
|
13374
13355
|
method: {
|
|
13375
13356
|
type: "string",
|
|
@@ -13422,14 +13403,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
13422
13403
|
{
|
|
13423
13404
|
name: "runtime_capture",
|
|
13424
13405
|
title: "Capture the screen",
|
|
13425
|
-
description: "A picture of the running game: the whole screen, one region or node of it at full resolution, or one viewport, as an image. The whole screen
|
|
13406
|
+
description: "A picture of the running game: the whole screen, one region or node of it at full resolution, or one viewport, as an image. The whole screen is the game's window, with the game's own dialogs and popups laid over it where they sit, also when a project opens them as windows of their own (subwindows not embedded); one of its windows lying outside the game's window, or minimised, is not in it, and viewport captures a shown one by its path. Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
13426
13407
|
parameters: {
|
|
13427
13408
|
projectPath: RUNNING_PROJECT_PATH,
|
|
13428
13409
|
pid: RUNNING_PID,
|
|
13429
13410
|
viewportPath: {
|
|
13430
13411
|
type: "string",
|
|
13431
13412
|
ops: ["viewport"],
|
|
13432
|
-
description: "viewport: the Viewport node. Default the root viewport."
|
|
13413
|
+
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."
|
|
13433
13414
|
},
|
|
13434
13415
|
width: {
|
|
13435
13416
|
type: "integer",
|
|
@@ -13735,7 +13716,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13735
13716
|
{
|
|
13736
13717
|
name: "debug_breakpoint",
|
|
13737
13718
|
title: "Breakpoints",
|
|
13738
|
-
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
|
|
13719
|
+
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.",
|
|
13739
13720
|
parameters: {
|
|
13740
13721
|
projectPath: PROJECT_PATH,
|
|
13741
13722
|
scriptPath: SCRIPT_PATH,
|
|
@@ -35683,6 +35664,10 @@ function parseAnnouncement(file, pid) {
|
|
|
35683
35664
|
}
|
|
35684
35665
|
const editorPid = readNumber(fields, "editor_pid");
|
|
35685
35666
|
const addonVersion = readString(fields, "addon_version");
|
|
35667
|
+
const addonDigest = readString(fields, "addon_digest");
|
|
35668
|
+
if (addonVersion === undefined || addonDigest === undefined) {
|
|
35669
|
+
return { kind: "rubbish" };
|
|
35670
|
+
}
|
|
35686
35671
|
return {
|
|
35687
35672
|
kind: "runtime",
|
|
35688
35673
|
endpoint: {
|
|
@@ -35693,7 +35678,7 @@ function parseAnnouncement(file, pid) {
|
|
|
35693
35678
|
file,
|
|
35694
35679
|
...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {},
|
|
35695
35680
|
...when,
|
|
35696
|
-
|
|
35681
|
+
addon: { version: addonVersion, digest: addonDigest }
|
|
35697
35682
|
}
|
|
35698
35683
|
};
|
|
35699
35684
|
}
|
|
@@ -36148,18 +36133,16 @@ function runtimeRequest(endpoint, command, params, timeoutMs, afterTheWait = "dr
|
|
|
36148
36133
|
if (!welcomed) {
|
|
36149
36134
|
welcomed = true;
|
|
36150
36135
|
taken();
|
|
36151
|
-
|
|
36152
|
-
|
|
36153
|
-
|
|
36154
|
-
|
|
36155
|
-
|
|
36156
|
-
|
|
36157
|
-
|
|
36158
|
-
|
|
36159
|
-
return;
|
|
36160
|
-
}
|
|
36161
|
-
continue;
|
|
36136
|
+
const spoken = readString(message, "type") === "welcome" ? readNumber(message, "protocol") : undefined;
|
|
36137
|
+
if (spoken !== RUNTIME_PROTOCOL) {
|
|
36138
|
+
finish({
|
|
36139
|
+
ok: false,
|
|
36140
|
+
reason: "protocol",
|
|
36141
|
+
message: `The game (${describe2(endpoint)}) speaks runtime protocol ${spoken ?? "unknown"} and this server speaks ${RUNTIME_PROTOCOL}. Reinstall the runtime addon into the project.`
|
|
36142
|
+
});
|
|
36143
|
+
return;
|
|
36162
36144
|
}
|
|
36145
|
+
continue;
|
|
36163
36146
|
}
|
|
36164
36147
|
if (message["id"] !== id) {
|
|
36165
36148
|
continue;
|
|
@@ -36224,7 +36207,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, afterTheWait = "dr
|
|
|
36224
36207
|
});
|
|
36225
36208
|
});
|
|
36226
36209
|
}
|
|
36227
|
-
var RUNTIME_PROTOCOL =
|
|
36210
|
+
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;
|
|
36228
36211
|
var init_runtime_client = __esm(() => {
|
|
36229
36212
|
init_alive();
|
|
36230
36213
|
init_launch();
|
|
@@ -36890,8 +36873,8 @@ class GodotDAPClient {
|
|
|
36890
36873
|
}
|
|
36891
36874
|
const response = await this.sendRequest("initialize", {
|
|
36892
36875
|
adapterID: "godot",
|
|
36893
|
-
clientID: "
|
|
36894
|
-
clientName: "
|
|
36876
|
+
clientID: "gdharness",
|
|
36877
|
+
clientName: "gdharness",
|
|
36895
36878
|
locale: "en",
|
|
36896
36879
|
linesStartAt1: true,
|
|
36897
36880
|
columnsStartAt1: true,
|
|
@@ -37598,8 +37581,8 @@ function theConsoleWasNotCaptured(why, projectPath) {
|
|
|
37598
37581
|
if (why.kind === "not ours") {
|
|
37599
37582
|
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.";
|
|
37600
37583
|
}
|
|
37601
|
-
if (why.kind === "
|
|
37602
|
-
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.
|
|
37584
|
+
if (why.kind === "another editor") {
|
|
37585
|
+
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.";
|
|
37603
37586
|
}
|
|
37604
37587
|
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.";
|
|
37605
37588
|
}
|
|
@@ -37609,7 +37592,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
|
|
|
37609
37592
|
}
|
|
37610
37593
|
const noted = readEditorLogNote(projectPath);
|
|
37611
37594
|
if (noted === null || editorPid === null || noted.pid !== editorPid) {
|
|
37612
|
-
return { kind: "
|
|
37595
|
+
return { kind: "another editor", notedPid: noted?.pid ?? null };
|
|
37613
37596
|
}
|
|
37614
37597
|
const path = editorLogPath(projectPath);
|
|
37615
37598
|
if (!existsSync20(path)) {
|
|
@@ -43172,8 +43155,11 @@ function resolveDefaultBridgeHost() {
|
|
|
43172
43155
|
const host = process.env["GDHARNESS_BRIDGE_HOST"]?.trim();
|
|
43173
43156
|
return host === undefined || host === "" ? DEFAULT_HOST : host;
|
|
43174
43157
|
}
|
|
43158
|
+
function greetingFaults(message) {
|
|
43159
|
+
return Object.entries(GREETING_FIELDS).filter(([field, type]) => typeof message[field] !== type).map(([field]) => field);
|
|
43160
|
+
}
|
|
43175
43161
|
function servedPort(said) {
|
|
43176
|
-
return
|
|
43162
|
+
return Number.isInteger(said) && said > 0 && said <= 65535 ? said : undefined;
|
|
43177
43163
|
}
|
|
43178
43164
|
function mayYetConnect(listeningSince, now = Date.now()) {
|
|
43179
43165
|
return listeningSince === undefined || now - listeningSince.getTime() < CONNECT_WINDOW_MS;
|
|
@@ -43201,13 +43187,27 @@ function getDefaultBridge(ownProject = null) {
|
|
|
43201
43187
|
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
|
|
43202
43188
|
return defaultBridge;
|
|
43203
43189
|
}
|
|
43204
|
-
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;
|
|
43190
|
+
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;
|
|
43205
43191
|
var init_godot_bridge = __esm(() => {
|
|
43206
43192
|
init_wrapper();
|
|
43207
43193
|
init_errors();
|
|
43208
43194
|
init_launch();
|
|
43209
43195
|
init_paths();
|
|
43210
43196
|
init_ports();
|
|
43197
|
+
init_server_version();
|
|
43198
|
+
init_setup();
|
|
43199
|
+
GREETING_FIELDS = {
|
|
43200
|
+
project_path: "string",
|
|
43201
|
+
addon_version: "string",
|
|
43202
|
+
addon_digest: "string",
|
|
43203
|
+
editor_pid: "number",
|
|
43204
|
+
lsp_port: "number",
|
|
43205
|
+
dap_port: "number",
|
|
43206
|
+
debug_port: "number",
|
|
43207
|
+
syncs_breakpoints: "boolean",
|
|
43208
|
+
opened_by_a_server: "boolean",
|
|
43209
|
+
started_by_an_editor: "boolean"
|
|
43210
|
+
};
|
|
43211
43211
|
BridgeTimeout = class BridgeTimeout extends Error {
|
|
43212
43212
|
toolName;
|
|
43213
43213
|
waitedMs;
|
|
@@ -43217,6 +43217,9 @@ var init_godot_bridge = __esm(() => {
|
|
|
43217
43217
|
this.waitedMs = waitedMs;
|
|
43218
43218
|
}
|
|
43219
43219
|
};
|
|
43220
|
+
AddonOfAnotherRelease = class AddonOfAnotherRelease extends Error {
|
|
43221
|
+
};
|
|
43222
|
+
ASKED_OF_ANY_RELEASE = new Set(["quit_editor", "restart_editor"]);
|
|
43220
43223
|
EditorBusy = class EditorBusy extends Error {
|
|
43221
43224
|
constructor(toolName) {
|
|
43222
43225
|
super(`The editor was scanning or importing its files, so ${toolName} was not run.`);
|
|
@@ -43391,6 +43394,14 @@ var init_godot_bridge = __esm(() => {
|
|
|
43391
43394
|
portWanted: this.wantedPort !== 0 && this.boundPort !== null && this.boundPort !== this.wantedPort ? this.wantedPort : undefined
|
|
43392
43395
|
};
|
|
43393
43396
|
}
|
|
43397
|
+
anotherRelease() {
|
|
43398
|
+
const info = this.connectionInfo;
|
|
43399
|
+
if (info?.greeted !== true || info.addonVersion === undefined || info.addonDigest === undefined) {
|
|
43400
|
+
return;
|
|
43401
|
+
}
|
|
43402
|
+
const project = info.projectPath;
|
|
43403
|
+
return addonMismatch(info.addonVersion, SERVER_VERSION, info.addonDigest, shippedEditorDigest(), project === undefined || project === "" ? undefined : installedEditorDigest(project));
|
|
43404
|
+
}
|
|
43394
43405
|
invokeTool(toolName, args, options = {}) {
|
|
43395
43406
|
const counted = options.counted ?? true;
|
|
43396
43407
|
const resourceKey = this.getResourceKey(args);
|
|
@@ -43538,24 +43549,33 @@ var init_godot_bridge = __esm(() => {
|
|
|
43538
43549
|
case "godot_ready":
|
|
43539
43550
|
if (this.connectionInfo) {
|
|
43540
43551
|
if (this.belongsElsewhere(message.project_path)) {
|
|
43541
|
-
this.elsewhereUnlessOneShot(message.project_path, message.editor_pid, message.opened_by_a_server
|
|
43552
|
+
this.elsewhereUnlessOneShot(message.project_path, message.editor_pid, message.opened_by_a_server);
|
|
43542
43553
|
return;
|
|
43543
43554
|
}
|
|
43544
43555
|
this.connectionInfo.projectPath = withoutTrailingSeparator(message.project_path);
|
|
43545
|
-
|
|
43556
|
+
const faults = greetingFaults(message);
|
|
43557
|
+
if (faults.length > 0) {
|
|
43558
|
+
this.log("info", `Godot ready from another release's addon, which sent no ${faults.join(", ")}`);
|
|
43559
|
+
this.connectionInfo.addonVersion = typeof message.addon_version === "string" ? message.addon_version : "";
|
|
43560
|
+
this.connectionInfo.addonDigest = "";
|
|
43561
|
+
this.connectionInfo.editorPid = typeof message.editor_pid === "number" ? message.editor_pid : undefined;
|
|
43562
|
+
this.connectionInfo.greeted = true;
|
|
43563
|
+
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
43564
|
+
return;
|
|
43565
|
+
}
|
|
43566
|
+
this.connectionInfo.addonVersion = message.addon_version;
|
|
43546
43567
|
this.connectionInfo.addonDigest = message.addon_digest;
|
|
43547
43568
|
this.connectionInfo.editorPid = message.editor_pid;
|
|
43548
43569
|
this.connectionInfo.lspPort = servedPort(message.lsp_port);
|
|
43549
43570
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
43550
43571
|
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
43551
|
-
this.connectionInfo.openedByAServer = message.opened_by_a_server
|
|
43552
|
-
this.connectionInfo.startedByAnEditor =
|
|
43553
|
-
this.connectionInfo.syncsBreakpoints =
|
|
43572
|
+
this.connectionInfo.openedByAServer = message.opened_by_a_server;
|
|
43573
|
+
this.connectionInfo.startedByAnEditor = message.started_by_an_editor;
|
|
43574
|
+
this.connectionInfo.syncsBreakpoints = message.syncs_breakpoints;
|
|
43554
43575
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
43555
43576
|
this.connectionInfo.greeted = true;
|
|
43556
|
-
|
|
43557
|
-
|
|
43558
|
-
this.identify(pid);
|
|
43577
|
+
if (!message.opened_by_a_server && this.oneShotReader !== null) {
|
|
43578
|
+
this.identify(message.editor_pid);
|
|
43559
43579
|
return;
|
|
43560
43580
|
}
|
|
43561
43581
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
@@ -43572,6 +43592,10 @@ var init_godot_bridge = __esm(() => {
|
|
|
43572
43592
|
if (!this.isConnected()) {
|
|
43573
43593
|
return Promise.reject(new Error("Godot is not connected"));
|
|
43574
43594
|
}
|
|
43595
|
+
const otherRelease = ASKED_OF_ANY_RELEASE.has(toolName) ? undefined : this.anotherRelease();
|
|
43596
|
+
if (otherRelease !== undefined) {
|
|
43597
|
+
return Promise.reject(new AddonOfAnotherRelease(otherRelease));
|
|
43598
|
+
}
|
|
43575
43599
|
const requestId = randomUUID();
|
|
43576
43600
|
const message = {
|
|
43577
43601
|
type: "tool_invoke",
|
|
@@ -43794,6 +43818,168 @@ var init_godot_bridge = __esm(() => {
|
|
|
43794
43818
|
};
|
|
43795
43819
|
});
|
|
43796
43820
|
|
|
43821
|
+
// src/import-freshness.ts
|
|
43822
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
43823
|
+
import { existsSync as existsSync25, readdirSync as readdirSync13, readFileSync as readFileSync24, statSync as statSync12 } from "node:fs";
|
|
43824
|
+
import { join as join29 } from "node:path";
|
|
43825
|
+
function md5Of(file) {
|
|
43826
|
+
let stats;
|
|
43827
|
+
try {
|
|
43828
|
+
stats = statSync12(file);
|
|
43829
|
+
} catch {
|
|
43830
|
+
return null;
|
|
43831
|
+
}
|
|
43832
|
+
const known = hashes.get(file);
|
|
43833
|
+
if (known !== undefined && known.size === stats.size && known.mtimeMs === stats.mtimeMs) {
|
|
43834
|
+
return known.md5;
|
|
43835
|
+
}
|
|
43836
|
+
let md5;
|
|
43837
|
+
try {
|
|
43838
|
+
md5 = createHash5("md5").update(readFileSync24(file)).digest("hex");
|
|
43839
|
+
} catch {
|
|
43840
|
+
return null;
|
|
43841
|
+
}
|
|
43842
|
+
hashes.set(file, { size: stats.size, mtimeMs: stats.mtimeMs, md5 });
|
|
43843
|
+
return md5;
|
|
43844
|
+
}
|
|
43845
|
+
function sectionOf(text, section) {
|
|
43846
|
+
const keys = new Map;
|
|
43847
|
+
let inside = false;
|
|
43848
|
+
for (const line of text.split(/\r?\n/)) {
|
|
43849
|
+
const header = /^\[([^\]]+)\]\s*$/.exec(line);
|
|
43850
|
+
if (header !== null) {
|
|
43851
|
+
inside = header[1] === section;
|
|
43852
|
+
continue;
|
|
43853
|
+
}
|
|
43854
|
+
const pair = inside ? /^([A-Za-z0-9_./]+)=(.*)$/.exec(line) : null;
|
|
43855
|
+
if (pair?.[1] !== undefined && pair[2] !== undefined) {
|
|
43856
|
+
keys.set(pair[1], pair[2]);
|
|
43857
|
+
}
|
|
43858
|
+
}
|
|
43859
|
+
return keys;
|
|
43860
|
+
}
|
|
43861
|
+
function unquoted(value) {
|
|
43862
|
+
return value === undefined ? "" : value.replace(/^"(.*)"$/, "$1");
|
|
43863
|
+
}
|
|
43864
|
+
function listed2(value) {
|
|
43865
|
+
return [...(value ?? "").matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1] ?? "");
|
|
43866
|
+
}
|
|
43867
|
+
function onDisk(projectPath, resPath) {
|
|
43868
|
+
return join29(projectPath, resPath.slice("res://".length));
|
|
43869
|
+
}
|
|
43870
|
+
function behind(projectPath, resPath, sidecar) {
|
|
43871
|
+
const remap = sectionOf(sidecar, "remap");
|
|
43872
|
+
const importer = unquoted(remap.get("importer"));
|
|
43873
|
+
if (importer === "keep" || importer === "skip") {
|
|
43874
|
+
return null;
|
|
43875
|
+
}
|
|
43876
|
+
const outputs = listed2(sectionOf(sidecar, "deps").get("dest_files"));
|
|
43877
|
+
if (outputs.some((output) => !existsSync25(onDisk(projectPath, output)))) {
|
|
43878
|
+
return "its imported output is not on disk";
|
|
43879
|
+
}
|
|
43880
|
+
const name = resPath.slice(resPath.lastIndexOf("/") + 1);
|
|
43881
|
+
const record = join29(projectPath, ".godot", "imported", `${name}-${createHash5("md5").update(resPath, "utf8").digest("hex")}.md5`);
|
|
43882
|
+
let recorded = "";
|
|
43883
|
+
try {
|
|
43884
|
+
recorded = /^source_md5="([0-9a-f]*)"/m.exec(readFileSync24(record, "utf8"))?.[1] ?? "";
|
|
43885
|
+
} catch {
|
|
43886
|
+
recorded = "";
|
|
43887
|
+
}
|
|
43888
|
+
if (recorded === "") {
|
|
43889
|
+
return "its import recorded no hash of its source";
|
|
43890
|
+
}
|
|
43891
|
+
const current = md5Of(onDisk(projectPath, resPath));
|
|
43892
|
+
return current === null || current === recorded ? null : "the source changed after it was imported";
|
|
43893
|
+
}
|
|
43894
|
+
function staleImports(projectPath) {
|
|
43895
|
+
const stale = [];
|
|
43896
|
+
const visit = (directory, resDirectory) => {
|
|
43897
|
+
if (existsSync25(join29(directory, ".gdignore")) || existsSync25(join29(directory, VERSION_MARKER))) {
|
|
43898
|
+
return;
|
|
43899
|
+
}
|
|
43900
|
+
let entries;
|
|
43901
|
+
try {
|
|
43902
|
+
entries = readdirSync13(directory, { withFileTypes: true });
|
|
43903
|
+
} catch {
|
|
43904
|
+
return;
|
|
43905
|
+
}
|
|
43906
|
+
const names = new Set(entries.map((entry) => entry.name));
|
|
43907
|
+
for (const entry of entries) {
|
|
43908
|
+
if (entry.name.startsWith(".")) {
|
|
43909
|
+
continue;
|
|
43910
|
+
}
|
|
43911
|
+
const resPath = `${resDirectory}${entry.name}`;
|
|
43912
|
+
if (entry.isDirectory()) {
|
|
43913
|
+
visit(join29(directory, entry.name), `${resPath}/`);
|
|
43914
|
+
continue;
|
|
43915
|
+
}
|
|
43916
|
+
if (!entry.isFile()) {
|
|
43917
|
+
continue;
|
|
43918
|
+
}
|
|
43919
|
+
if (entry.name.endsWith(".import")) {
|
|
43920
|
+
const source = resPath.slice(0, -".import".length);
|
|
43921
|
+
if (!names.has(entry.name.slice(0, -".import".length))) {
|
|
43922
|
+
continue;
|
|
43923
|
+
}
|
|
43924
|
+
let sidecar;
|
|
43925
|
+
try {
|
|
43926
|
+
sidecar = readFileSync24(join29(directory, entry.name), "utf8");
|
|
43927
|
+
} catch {
|
|
43928
|
+
continue;
|
|
43929
|
+
}
|
|
43930
|
+
const reason = behind(projectPath, source, sidecar);
|
|
43931
|
+
if (reason !== null) {
|
|
43932
|
+
stale.push({ path: source, reason });
|
|
43933
|
+
}
|
|
43934
|
+
continue;
|
|
43935
|
+
}
|
|
43936
|
+
const extension = entry.name.slice(entry.name.lastIndexOf(".") + 1).toLowerCase();
|
|
43937
|
+
if (IMPORTABLE.has(extension) && !names.has(`${entry.name}.import`)) {
|
|
43938
|
+
stale.push({ path: resPath, reason: "it has not been imported" });
|
|
43939
|
+
}
|
|
43940
|
+
}
|
|
43941
|
+
};
|
|
43942
|
+
visit(projectPath, "res://");
|
|
43943
|
+
return stale.sort((one, other) => one.path.localeCompare(other.path));
|
|
43944
|
+
}
|
|
43945
|
+
var IMPORTABLE_EXTENSIONS, IMPORTABLE, hashes;
|
|
43946
|
+
var init_import_freshness = __esm(() => {
|
|
43947
|
+
init_project_scan();
|
|
43948
|
+
IMPORTABLE_EXTENSIONS = [
|
|
43949
|
+
"png",
|
|
43950
|
+
"jpg",
|
|
43951
|
+
"jpeg",
|
|
43952
|
+
"webp",
|
|
43953
|
+
"svg",
|
|
43954
|
+
"bmp",
|
|
43955
|
+
"tga",
|
|
43956
|
+
"exr",
|
|
43957
|
+
"hdr",
|
|
43958
|
+
"dds",
|
|
43959
|
+
"ktx",
|
|
43960
|
+
"wav",
|
|
43961
|
+
"mp3",
|
|
43962
|
+
"ogg",
|
|
43963
|
+
"ttf",
|
|
43964
|
+
"otf",
|
|
43965
|
+
"ttc",
|
|
43966
|
+
"otc",
|
|
43967
|
+
"woff",
|
|
43968
|
+
"woff2",
|
|
43969
|
+
"pfb",
|
|
43970
|
+
"pfm",
|
|
43971
|
+
"fnt",
|
|
43972
|
+
"font",
|
|
43973
|
+
"glb",
|
|
43974
|
+
"gltf",
|
|
43975
|
+
"fbx",
|
|
43976
|
+
"obj",
|
|
43977
|
+
"dae"
|
|
43978
|
+
];
|
|
43979
|
+
IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
|
|
43980
|
+
hashes = new Map;
|
|
43981
|
+
});
|
|
43982
|
+
|
|
43797
43983
|
// src/keeper-channel.ts
|
|
43798
43984
|
import { connect as connect2, createServer as createServer2 } from "node:net";
|
|
43799
43985
|
async function askTheKeeperToStop(address, withinMs) {
|
|
@@ -43808,9 +43994,6 @@ async function askTheKeepersDebugger(address, request, withinMs) {
|
|
|
43808
43994
|
if (typeof answered !== "string") {
|
|
43809
43995
|
return answered;
|
|
43810
43996
|
}
|
|
43811
|
-
if (answered === "unknown") {
|
|
43812
|
-
return { unreached: "its keeper was started by a gdharness from before game debuggers, and has none" };
|
|
43813
|
-
}
|
|
43814
43997
|
try {
|
|
43815
43998
|
const parsed = JSON.parse(answered);
|
|
43816
43999
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
@@ -43860,17 +44043,17 @@ var DEBUG_PREFIX = "debug ";
|
|
|
43860
44043
|
var init_keeper_channel = () => {};
|
|
43861
44044
|
|
|
43862
44045
|
// src/lifecycle-log.ts
|
|
43863
|
-
import { appendFileSync, mkdirSync as mkdirSync14, renameSync as renameSync5, statSync as
|
|
43864
|
-
import { dirname as dirname20, join as
|
|
44046
|
+
import { appendFileSync, mkdirSync as mkdirSync14, renameSync as renameSync5, statSync as statSync13 } from "node:fs";
|
|
44047
|
+
import { dirname as dirname20, join as join30 } from "node:path";
|
|
43865
44048
|
import process11 from "node:process";
|
|
43866
44049
|
function lifecycleLogPath() {
|
|
43867
|
-
return
|
|
44050
|
+
return join30(runtimeDirectory(), "servers.log");
|
|
43868
44051
|
}
|
|
43869
44052
|
function writeLifecycle(event, fields = {}) {
|
|
43870
44053
|
const path = lifecycleLogPath();
|
|
43871
44054
|
try {
|
|
43872
44055
|
mkdirSync14(dirname20(path), { recursive: true });
|
|
43873
|
-
if ((
|
|
44056
|
+
if ((statSync13(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
|
|
43874
44057
|
try {
|
|
43875
44058
|
renameSync5(path, `${path}.1`);
|
|
43876
44059
|
} catch {}
|
|
@@ -44184,10 +44367,10 @@ var init_prompts = __esm(() => {
|
|
|
44184
44367
|
});
|
|
44185
44368
|
|
|
44186
44369
|
// src/restart-note.ts
|
|
44187
|
-
import { mkdirSync as mkdirSync15, readFileSync as
|
|
44188
|
-
import { dirname as dirname21, join as
|
|
44370
|
+
import { mkdirSync as mkdirSync15, readFileSync as readFileSync25, rmSync as rmSync13, writeFileSync as writeFileSync17 } from "node:fs";
|
|
44371
|
+
import { dirname as dirname21, join as join31 } from "node:path";
|
|
44189
44372
|
function restartNotePath(projectPath) {
|
|
44190
|
-
return
|
|
44373
|
+
return join31(projectPath, ".godot", "gdharness-restart.json");
|
|
44191
44374
|
}
|
|
44192
44375
|
function noteRestartBegun(note) {
|
|
44193
44376
|
const path = restartNotePath(note.projectPath);
|
|
@@ -44201,7 +44384,7 @@ function noteRestartBegun(note) {
|
|
|
44201
44384
|
}
|
|
44202
44385
|
function restartOwed(projectPath) {
|
|
44203
44386
|
try {
|
|
44204
|
-
const parsed = JSON.parse(
|
|
44387
|
+
const parsed = JSON.parse(readFileSync25(restartNotePath(projectPath), "utf8"));
|
|
44205
44388
|
if (typeof parsed !== "object" || parsed === null) {
|
|
44206
44389
|
return null;
|
|
44207
44390
|
}
|
|
@@ -44226,18 +44409,18 @@ var init_restart_note = __esm(() => {
|
|
|
44226
44409
|
});
|
|
44227
44410
|
|
|
44228
44411
|
// src/run-record.ts
|
|
44229
|
-
import { createHash as
|
|
44230
|
-
import { mkdirSync as mkdirSync16, openSync as openSync3, readdirSync as
|
|
44231
|
-
import { basename as basename9, dirname as dirname22, join as
|
|
44412
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
44413
|
+
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";
|
|
44414
|
+
import { basename as basename9, dirname as dirname22, join as join32, resolve as resolve7 } from "node:path";
|
|
44232
44415
|
function runsDirectory() {
|
|
44233
|
-
return
|
|
44416
|
+
return join32(runtimeDirectory(), "runs");
|
|
44234
44417
|
}
|
|
44235
44418
|
function realAsFarAsItExists(path) {
|
|
44236
44419
|
const missing = [];
|
|
44237
44420
|
let at = path;
|
|
44238
44421
|
for (;; ) {
|
|
44239
44422
|
try {
|
|
44240
|
-
return
|
|
44423
|
+
return join32(realpathSync6.native(at), ...missing);
|
|
44241
44424
|
} catch {
|
|
44242
44425
|
const parent = dirname22(at);
|
|
44243
44426
|
if (parent === at) {
|
|
@@ -44251,13 +44434,13 @@ function realAsFarAsItExists(path) {
|
|
|
44251
44434
|
function projectKey(projectPath) {
|
|
44252
44435
|
const real = realAsFarAsItExists(resolve7(projectPath)).replace(/[\\/]+$/, "");
|
|
44253
44436
|
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
44254
|
-
return
|
|
44437
|
+
return createHash6("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
44255
44438
|
}
|
|
44256
44439
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
44257
|
-
return
|
|
44440
|
+
return join32(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
44258
44441
|
}
|
|
44259
44442
|
function latestRunPath(servedBy, directory = runtimeDirectory()) {
|
|
44260
|
-
return
|
|
44443
|
+
return join32(directory, "runs", `latest-${projectKey(servedBy)}.json`);
|
|
44261
44444
|
}
|
|
44262
44445
|
function writeLatestRun(servedBy, latest) {
|
|
44263
44446
|
try {
|
|
@@ -44267,7 +44450,7 @@ function writeLatestRun(servedBy, latest) {
|
|
|
44267
44450
|
}
|
|
44268
44451
|
function readLatestRun(servedBy, directory = runtimeDirectory()) {
|
|
44269
44452
|
try {
|
|
44270
|
-
const fields = JSON.parse(
|
|
44453
|
+
const fields = JSON.parse(readFileSync26(latestRunPath(servedBy, directory), "utf8"));
|
|
44271
44454
|
if (typeof fields !== "object" || fields === null) {
|
|
44272
44455
|
return null;
|
|
44273
44456
|
}
|
|
@@ -44282,20 +44465,20 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
|
|
|
44282
44465
|
}
|
|
44283
44466
|
function openTranscript(startedAt) {
|
|
44284
44467
|
mkdirSync16(runsDirectory(), { recursive: true });
|
|
44285
|
-
const path =
|
|
44468
|
+
const path = join32(runsDirectory(), `run-${startedAt}.log`);
|
|
44286
44469
|
return { path, fd: openSync3(path, "a") };
|
|
44287
44470
|
}
|
|
44288
44471
|
function profilePath(startedAt) {
|
|
44289
|
-
return
|
|
44472
|
+
return join32(runsDirectory(), `run-${startedAt}.profile.json`);
|
|
44290
44473
|
}
|
|
44291
44474
|
function profileMarkPath(startedAt) {
|
|
44292
|
-
return
|
|
44475
|
+
return join32(runsDirectory(), `run-${startedAt}.profile-mark.json`);
|
|
44293
44476
|
}
|
|
44294
44477
|
function noteIsAbout(note, playing) {
|
|
44295
44478
|
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
44296
44479
|
}
|
|
44297
44480
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
44298
|
-
return
|
|
44481
|
+
return join32(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
44299
44482
|
}
|
|
44300
44483
|
function writeEditorRunNote(note) {
|
|
44301
44484
|
mkdirSync16(runsDirectory(), { recursive: true });
|
|
@@ -44306,7 +44489,7 @@ function writeEditorRunNote(note) {
|
|
|
44306
44489
|
}
|
|
44307
44490
|
function readEditorRunNote(projectPath) {
|
|
44308
44491
|
for (const directory of runtimeDirectories()) {
|
|
44309
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory))
|
|
44492
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory));
|
|
44310
44493
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
44311
44494
|
return note;
|
|
44312
44495
|
}
|
|
@@ -44316,7 +44499,7 @@ function readEditorRunNote(projectPath) {
|
|
|
44316
44499
|
function editorNoteAt(path) {
|
|
44317
44500
|
let parsed;
|
|
44318
44501
|
try {
|
|
44319
|
-
parsed = JSON.parse(
|
|
44502
|
+
parsed = JSON.parse(readFileSync26(path, "utf8"));
|
|
44320
44503
|
} catch {
|
|
44321
44504
|
return null;
|
|
44322
44505
|
}
|
|
@@ -44340,10 +44523,6 @@ function readRunRecord(projectPath) {
|
|
|
44340
44523
|
if (own !== null) {
|
|
44341
44524
|
return own;
|
|
44342
44525
|
}
|
|
44343
|
-
const shared = recordAt(join31(directory, "runs", SHARED_NOTE));
|
|
44344
|
-
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
44345
|
-
return shared;
|
|
44346
|
-
}
|
|
44347
44526
|
}
|
|
44348
44527
|
return null;
|
|
44349
44528
|
}
|
|
@@ -44352,13 +44531,13 @@ function everyRunRecord() {
|
|
|
44352
44531
|
for (const directory of runtimeDirectories()) {
|
|
44353
44532
|
let entries;
|
|
44354
44533
|
try {
|
|
44355
|
-
entries =
|
|
44534
|
+
entries = readdirSync14(join32(directory, "runs"));
|
|
44356
44535
|
} catch {
|
|
44357
44536
|
continue;
|
|
44358
44537
|
}
|
|
44359
44538
|
for (const entry of entries) {
|
|
44360
|
-
if (entry
|
|
44361
|
-
const record = recordAt(
|
|
44539
|
+
if (entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
44540
|
+
const record = recordAt(join32(directory, "runs", entry));
|
|
44362
44541
|
if (record !== null) {
|
|
44363
44542
|
found.push(record);
|
|
44364
44543
|
}
|
|
@@ -44370,7 +44549,7 @@ function everyRunRecord() {
|
|
|
44370
44549
|
function recordAt(path) {
|
|
44371
44550
|
let parsed;
|
|
44372
44551
|
try {
|
|
44373
|
-
parsed = JSON.parse(
|
|
44552
|
+
parsed = JSON.parse(readFileSync26(path, "utf8"));
|
|
44374
44553
|
} catch {
|
|
44375
44554
|
return null;
|
|
44376
44555
|
}
|
|
@@ -44385,17 +44564,23 @@ function recordAt(path) {
|
|
|
44385
44564
|
return null;
|
|
44386
44565
|
}
|
|
44387
44566
|
const args = fields["arguments"];
|
|
44567
|
+
const startedBy = fields["startedBy"];
|
|
44568
|
+
const projectPath = fields["projectPath"];
|
|
44569
|
+
const command = fields["command"];
|
|
44570
|
+
if (typeof startedBy !== "number" || typeof projectPath !== "string" || projectPath === "" || typeof command !== "string" || !Array.isArray(args)) {
|
|
44571
|
+
return null;
|
|
44572
|
+
}
|
|
44388
44573
|
return {
|
|
44389
44574
|
pid,
|
|
44390
44575
|
transcript,
|
|
44391
44576
|
startedAt,
|
|
44392
|
-
|
|
44393
|
-
projectPath
|
|
44577
|
+
startedBy,
|
|
44578
|
+
projectPath,
|
|
44394
44579
|
...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
|
|
44395
44580
|
...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
|
|
44396
44581
|
...typeof fields["profile"] === "string" && fields["profile"] !== "" ? { profile: fields["profile"] } : {},
|
|
44397
|
-
arguments:
|
|
44398
|
-
|
|
44582
|
+
arguments: args.filter((value) => typeof value === "string"),
|
|
44583
|
+
command,
|
|
44399
44584
|
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
44400
44585
|
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
|
|
44401
44586
|
...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
|
|
@@ -44423,7 +44608,7 @@ async function runningAs(pid) {
|
|
|
44423
44608
|
const began = await startedAt(pid, null);
|
|
44424
44609
|
const when = began === null ? {} : { startedAt: began };
|
|
44425
44610
|
if (process.platform === "linux") {
|
|
44426
|
-
const raw =
|
|
44611
|
+
const raw = readFileSync26(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
44427
44612
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
44428
44613
|
}
|
|
44429
44614
|
const args = (await runHidden("ps", ["-p", String(pid), "-o", "args="], { timeout: 15000 })).stdout.trim();
|
|
@@ -44523,26 +44708,20 @@ function whyNotTheRun(record, running, needed) {
|
|
|
44523
44708
|
if (running.kind === "none") {
|
|
44524
44709
|
return `the operating system would not describe that process (${running.unread ?? "no reason given"})`;
|
|
44525
44710
|
}
|
|
44526
|
-
const after = running.startedAt === undefined ? 0 : running.startedAt -
|
|
44711
|
+
const after = running.startedAt === undefined ? 0 : running.startedAt - record.startedBy;
|
|
44527
44712
|
if (after > SAME_RUN_WINDOW_MS) {
|
|
44528
44713
|
return `that process started ${Math.round(after / 1000)} seconds after the run had`;
|
|
44529
44714
|
}
|
|
44530
|
-
const engine =
|
|
44715
|
+
const engine = basename9(record.command);
|
|
44531
44716
|
const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
|
|
44532
|
-
const wanted =
|
|
44533
|
-
if (
|
|
44534
|
-
return `that process is not ${engine
|
|
44717
|
+
const wanted = process.platform === "win32" ? engine.toLowerCase() : engine;
|
|
44718
|
+
if (!said.includes(wanted)) {
|
|
44719
|
+
return `that process is not ${engine}, which the run was started with`;
|
|
44535
44720
|
}
|
|
44536
44721
|
if (running.kind === "commandLine" && readCommandLine(running.text).editor) {
|
|
44537
44722
|
return "that process is an editor";
|
|
44538
44723
|
}
|
|
44539
|
-
if (record.projectPath === "") {
|
|
44540
|
-
return wanted !== null ? null : "the record names neither the project nor the engine";
|
|
44541
|
-
}
|
|
44542
44724
|
if (running.kind === "image") {
|
|
44543
|
-
if (wanted === null) {
|
|
44544
|
-
return "the operating system named only its executable, and the record names no engine";
|
|
44545
|
-
}
|
|
44546
44725
|
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`;
|
|
44547
44726
|
}
|
|
44548
44727
|
const project = process.platform === "win32" ? record.projectPath.toLowerCase() : record.projectPath;
|
|
@@ -44566,19 +44745,14 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
44566
44745
|
}
|
|
44567
44746
|
function clearRunRecord(projectPath) {
|
|
44568
44747
|
for (const directory of runtimeDirectories()) {
|
|
44569
|
-
|
|
44570
|
-
|
|
44571
|
-
|
|
44572
|
-
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
44573
|
-
try {
|
|
44574
|
-
rmSync14(path, { force: true });
|
|
44575
|
-
} catch {}
|
|
44576
|
-
}
|
|
44748
|
+
try {
|
|
44749
|
+
rmSync14(runRecordPath(projectPath, directory), { force: true });
|
|
44750
|
+
} catch {}
|
|
44577
44751
|
}
|
|
44578
44752
|
}
|
|
44579
44753
|
function ageMs(path, now) {
|
|
44580
44754
|
try {
|
|
44581
|
-
return now -
|
|
44755
|
+
return now - statSync14(path).mtimeMs;
|
|
44582
44756
|
} catch {
|
|
44583
44757
|
return 0;
|
|
44584
44758
|
}
|
|
@@ -44592,7 +44766,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
44592
44766
|
]));
|
|
44593
44767
|
let entries;
|
|
44594
44768
|
try {
|
|
44595
|
-
entries =
|
|
44769
|
+
entries = readdirSync14(directory);
|
|
44596
44770
|
} catch {
|
|
44597
44771
|
return;
|
|
44598
44772
|
}
|
|
@@ -44601,7 +44775,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
44601
44775
|
if (!transcript && !entry.endsWith(".partial")) {
|
|
44602
44776
|
continue;
|
|
44603
44777
|
}
|
|
44604
|
-
const path =
|
|
44778
|
+
const path = join32(directory, entry);
|
|
44605
44779
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
44606
44780
|
continue;
|
|
44607
44781
|
}
|
|
@@ -44610,7 +44784,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
44610
44784
|
} catch {}
|
|
44611
44785
|
}
|
|
44612
44786
|
}
|
|
44613
|
-
var
|
|
44787
|
+
var SAME_RUN_WINDOW_MS = 1e4;
|
|
44614
44788
|
var init_run_record = __esm(() => {
|
|
44615
44789
|
init_errors();
|
|
44616
44790
|
init_paths();
|
|
@@ -44622,10 +44796,10 @@ var init_run_record = __esm(() => {
|
|
|
44622
44796
|
});
|
|
44623
44797
|
|
|
44624
44798
|
// src/script-profile.ts
|
|
44625
|
-
import { readFileSync as
|
|
44799
|
+
import { readFileSync as readFileSync27 } from "node:fs";
|
|
44626
44800
|
function readProfileFile(path) {
|
|
44627
44801
|
try {
|
|
44628
|
-
const parsed = JSON.parse(
|
|
44802
|
+
const parsed = JSON.parse(readFileSync27(path, "utf8"));
|
|
44629
44803
|
return typeof parsed === "object" && typeof parsed.connected === "boolean" ? parsed : null;
|
|
44630
44804
|
} catch {
|
|
44631
44805
|
return null;
|
|
@@ -44682,7 +44856,7 @@ function writeProfileMark(path, mark) {
|
|
|
44682
44856
|
}
|
|
44683
44857
|
function readProfileMark(path) {
|
|
44684
44858
|
try {
|
|
44685
|
-
const parsed = JSON.parse(
|
|
44859
|
+
const parsed = JSON.parse(readFileSync27(path, "utf8"));
|
|
44686
44860
|
return typeof parsed === "object" && typeof parsed.totals === "object" ? parsed : null;
|
|
44687
44861
|
} catch {
|
|
44688
44862
|
return null;
|
|
@@ -44855,18 +45029,18 @@ import {
|
|
|
44855
45029
|
closeSync as closeSync3,
|
|
44856
45030
|
constants,
|
|
44857
45031
|
copyFileSync,
|
|
44858
|
-
existsSync as
|
|
45032
|
+
existsSync as existsSync26,
|
|
44859
45033
|
fstatSync,
|
|
44860
45034
|
mkdirSync as mkdirSync17,
|
|
44861
45035
|
openSync as openSync4,
|
|
44862
|
-
readdirSync as
|
|
44863
|
-
readFileSync as
|
|
45036
|
+
readdirSync as readdirSync15,
|
|
45037
|
+
readFileSync as readFileSync28,
|
|
44864
45038
|
readSync as readSync2,
|
|
44865
45039
|
rmdirSync as rmdirSync2,
|
|
44866
|
-
statSync as
|
|
45040
|
+
statSync as statSync15,
|
|
44867
45041
|
writeFileSync as writeFileSync18
|
|
44868
45042
|
} from "node:fs";
|
|
44869
|
-
import { basename as basename10, dirname as dirname23, extname as extname4, isAbsolute as isAbsolute5, join as
|
|
45043
|
+
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";
|
|
44870
45044
|
import { setTimeout as delay5 } from "node:timers/promises";
|
|
44871
45045
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
44872
45046
|
function editorRestartTimeoutMs() {
|
|
@@ -44903,15 +45077,15 @@ function writtenFileRefusal(path, rule) {
|
|
|
44903
45077
|
return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
|
|
44904
45078
|
}
|
|
44905
45079
|
const directory = realPathOr(dirname23(path));
|
|
44906
|
-
if (!
|
|
45080
|
+
if (!existsSync26(directory)) {
|
|
44907
45081
|
return `${named}'s directory ${dirname23(path)} does not exist; ${what} creates no directories.`;
|
|
44908
45082
|
}
|
|
44909
|
-
const where =
|
|
45083
|
+
const where = join33(directory, basename10(path));
|
|
44910
45084
|
const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
44911
45085
|
if (inside !== undefined) {
|
|
44912
45086
|
return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
|
|
44913
45087
|
}
|
|
44914
|
-
const taken = rule.writes(where).find((file) =>
|
|
45088
|
+
const taken = rule.writes(where).find((file) => existsSync26(file));
|
|
44915
45089
|
if (taken !== undefined) {
|
|
44916
45090
|
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.`;
|
|
44917
45091
|
}
|
|
@@ -45102,7 +45276,7 @@ async function closeLikeAPlayer(announced, alsoGoing, projectPath) {
|
|
|
45102
45276
|
}
|
|
45103
45277
|
const asked = await runtimeRequest(endpoint, "close_game", {}, CLOSE_ASK_MS);
|
|
45104
45278
|
if (!asked.ok) {
|
|
45105
|
-
return asked.reason === "error" ? `its runtime
|
|
45279
|
+
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`;
|
|
45106
45280
|
}
|
|
45107
45281
|
if (await untilGone([announced, ...alsoGoing], closeWaitMs())) {
|
|
45108
45282
|
return null;
|
|
@@ -45424,6 +45598,18 @@ function outsideTheirRanges(spec, args) {
|
|
|
45424
45598
|
function scanWaitMs() {
|
|
45425
45599
|
return waitFromEnv("GDHARNESS_SCAN_WAIT_MS", 30000);
|
|
45426
45600
|
}
|
|
45601
|
+
function importedFirstAnswer(importedFirst) {
|
|
45602
|
+
if (importedFirst === undefined) {
|
|
45603
|
+
return {};
|
|
45604
|
+
}
|
|
45605
|
+
return {
|
|
45606
|
+
...importedFirst.imported.length === 0 ? {} : { importedFirst: importedFirst.imported },
|
|
45607
|
+
...importedFirst.notImported.length === 0 ? {} : { notImportedFirst: importedFirst.notImported },
|
|
45608
|
+
...importedFirst.uidsWritten.length === 0 ? {} : { uidsWrittenFirst: importedFirst.uidsWritten },
|
|
45609
|
+
importTookMs: importedFirst.tookMs,
|
|
45610
|
+
...importedFirst.note === undefined ? {} : { importNote: importedFirst.note }
|
|
45611
|
+
};
|
|
45612
|
+
}
|
|
45427
45613
|
function scanWaitIn(payload) {
|
|
45428
45614
|
const waited = readNumber(payload, "waitedForEditorScanMs");
|
|
45429
45615
|
const note = readString(payload, "scanNote");
|
|
@@ -45452,7 +45638,7 @@ function notRunNote(notRun, failFast) {
|
|
|
45452
45638
|
function exportPresetRefusal(projectPath, preset) {
|
|
45453
45639
|
let text;
|
|
45454
45640
|
try {
|
|
45455
|
-
text =
|
|
45641
|
+
text = readFileSync28(join33(projectPath, "export_presets.cfg"), "utf8");
|
|
45456
45642
|
} catch {
|
|
45457
45643
|
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.`;
|
|
45458
45644
|
}
|
|
@@ -45516,20 +45702,22 @@ function writtenBeside(before, after, output, within) {
|
|
|
45516
45702
|
function filesIn(folder) {
|
|
45517
45703
|
const found = new Map;
|
|
45518
45704
|
try {
|
|
45519
|
-
for (const entry of
|
|
45705
|
+
for (const entry of readdirSync15(folder, { withFileTypes: true })) {
|
|
45520
45706
|
if (entry.isFile()) {
|
|
45521
|
-
found.set(entry.name,
|
|
45707
|
+
found.set(entry.name, statSync15(join33(folder, entry.name)).mtimeMs);
|
|
45522
45708
|
}
|
|
45523
45709
|
}
|
|
45524
45710
|
} catch {}
|
|
45525
45711
|
return found;
|
|
45526
45712
|
}
|
|
45527
45713
|
function scanWaitAnswer(scanned, started = "game") {
|
|
45714
|
+
const imports = importedFirstAnswer(scanned.importedFirst);
|
|
45528
45715
|
if (scanned.waitedMs === 0) {
|
|
45529
|
-
return
|
|
45716
|
+
return imports;
|
|
45530
45717
|
}
|
|
45531
45718
|
const still = `The editor was still scanning the project after ${scanWaitMs() / 1000} seconds, so`;
|
|
45532
45719
|
return {
|
|
45720
|
+
...imports,
|
|
45533
45721
|
waitedForEditorScanMs: scanned.waitedMs,
|
|
45534
45722
|
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."
|
|
45535
45723
|
};
|
|
@@ -45548,7 +45736,7 @@ function unsavedInEditorNote(scripts) {
|
|
|
45548
45736
|
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.`;
|
|
45549
45737
|
}
|
|
45550
45738
|
function editorAddonMissing(projectPath) {
|
|
45551
|
-
if (!
|
|
45739
|
+
if (!existsSync26(join33(projectPath, "project.godot"))) {
|
|
45552
45740
|
return null;
|
|
45553
45741
|
}
|
|
45554
45742
|
const report = inspectProject(projectPath);
|
|
@@ -45637,7 +45825,7 @@ function withUidsMissing(answer, missing) {
|
|
|
45637
45825
|
last.text = answerJson({
|
|
45638
45826
|
...parsed,
|
|
45639
45827
|
uidsMissing: missing,
|
|
45640
|
-
uidsNote: `${missing.length === 1 ? "This script has" : "These scripts have"} no .uid sidecar yet: the engine writes one when it imports the project,
|
|
45828
|
+
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.`
|
|
45641
45829
|
});
|
|
45642
45830
|
}
|
|
45643
45831
|
} catch {}
|
|
@@ -45813,7 +46001,7 @@ function profileFrameFunctions(projectPath) {
|
|
|
45813
46001
|
let projectGodot = null;
|
|
45814
46002
|
if (projectPath !== null) {
|
|
45815
46003
|
try {
|
|
45816
|
-
projectGodot =
|
|
46004
|
+
projectGodot = readFileSync28(join33(projectPath, "project.godot"), "utf8");
|
|
45817
46005
|
} catch {}
|
|
45818
46006
|
}
|
|
45819
46007
|
return frameFunctionsFor(queuedMessagesOf(projectGodot));
|
|
@@ -45837,7 +46025,7 @@ function projectSourceOf(projectPath) {
|
|
|
45837
46025
|
let source = null;
|
|
45838
46026
|
if (inside.ok) {
|
|
45839
46027
|
try {
|
|
45840
|
-
source =
|
|
46028
|
+
source = readFileSync28(inside.absolutePath, "utf8");
|
|
45841
46029
|
} catch {}
|
|
45842
46030
|
}
|
|
45843
46031
|
read.set(script, source);
|
|
@@ -45992,7 +46180,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
|
|
|
45992
46180
|
}
|
|
45993
46181
|
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
45994
46182
|
try {
|
|
45995
|
-
return settingKeys(
|
|
46183
|
+
return settingKeys(readFileSync28(join33(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
45996
46184
|
} catch {
|
|
45997
46185
|
return true;
|
|
45998
46186
|
}
|
|
@@ -46007,13 +46195,13 @@ function uncaughtNote(uncaught) {
|
|
|
46007
46195
|
}
|
|
46008
46196
|
function elsewhereIn(projectPath, asked) {
|
|
46009
46197
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
46010
|
-
if (wanted === "" ||
|
|
46198
|
+
if (wanted === "" || existsSync26(join33(projectPath, wanted))) {
|
|
46011
46199
|
return "";
|
|
46012
46200
|
}
|
|
46013
46201
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
46014
46202
|
let near = [];
|
|
46015
46203
|
try {
|
|
46016
|
-
near =
|
|
46204
|
+
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()));
|
|
46017
46205
|
} catch {
|
|
46018
46206
|
return "";
|
|
46019
46207
|
}
|
|
@@ -46104,23 +46292,23 @@ function engineChecked(why) {
|
|
|
46104
46292
|
}
|
|
46105
46293
|
function isDirectory(path) {
|
|
46106
46294
|
try {
|
|
46107
|
-
return
|
|
46295
|
+
return statSync15(path).isDirectory();
|
|
46108
46296
|
} catch {
|
|
46109
46297
|
return false;
|
|
46110
46298
|
}
|
|
46111
46299
|
}
|
|
46112
46300
|
function scriptsUnder(directory, prefix) {
|
|
46113
|
-
if (
|
|
46301
|
+
if (existsSync26(join33(directory, ".gdignore"))) {
|
|
46114
46302
|
return [];
|
|
46115
46303
|
}
|
|
46116
46304
|
const found = [];
|
|
46117
|
-
for (const entry of
|
|
46305
|
+
for (const entry of readdirSync15(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
|
|
46118
46306
|
if (entry.name.startsWith(".")) {
|
|
46119
46307
|
continue;
|
|
46120
46308
|
}
|
|
46121
46309
|
const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
46122
46310
|
if (entry.isDirectory()) {
|
|
46123
|
-
found.push(...scriptsUnder(
|
|
46311
|
+
found.push(...scriptsUnder(join33(directory, entry.name), path));
|
|
46124
46312
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
46125
46313
|
found.push(path);
|
|
46126
46314
|
}
|
|
@@ -46128,29 +46316,18 @@ function scriptsUnder(directory, prefix) {
|
|
|
46128
46316
|
return found;
|
|
46129
46317
|
}
|
|
46130
46318
|
function mainSceneOf(projectFile) {
|
|
46131
|
-
const scene = parseProjectGodot(
|
|
46319
|
+
const scene = parseProjectGodot(readFileSync28(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
46132
46320
|
return typeof scene === "string" && scene !== "" ? scene : null;
|
|
46133
46321
|
}
|
|
46134
|
-
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
46135
|
-
if (projectPath === null) {
|
|
46136
|
-
return false;
|
|
46137
|
-
}
|
|
46138
|
-
try {
|
|
46139
|
-
return readFileSync27(join32(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
46140
|
-
} catch {
|
|
46141
|
-
return false;
|
|
46142
|
-
}
|
|
46143
|
-
}
|
|
46144
46322
|
function editorPlaysHeadless(projectFile) {
|
|
46145
|
-
const runArgs = parseProjectGodot(
|
|
46323
|
+
const runArgs = parseProjectGodot(readFileSync28(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
46146
46324
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
46147
46325
|
}
|
|
46148
|
-
function
|
|
46326
|
+
function ownArguments(params) {
|
|
46149
46327
|
const result = emptyRecord();
|
|
46150
46328
|
const source = asParams(params);
|
|
46151
46329
|
for (const key of Object.keys(source)) {
|
|
46152
|
-
|
|
46153
|
-
result[name] = source[key];
|
|
46330
|
+
result[key] = source[key];
|
|
46154
46331
|
}
|
|
46155
46332
|
return result;
|
|
46156
46333
|
}
|
|
@@ -46161,7 +46338,7 @@ function needsItsOwnProcess(asked) {
|
|
|
46161
46338
|
class GodotServer {
|
|
46162
46339
|
stdio = null;
|
|
46163
46340
|
locator = new GodotLocator;
|
|
46164
|
-
operationsScript =
|
|
46341
|
+
operationsScript = join33(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
46165
46342
|
godotBridge;
|
|
46166
46343
|
tools = buildToolDefinitions();
|
|
46167
46344
|
activeProcess = null;
|
|
@@ -46236,6 +46413,7 @@ class GodotServer {
|
|
|
46236
46413
|
console.error(`[SERVER] Using Godot at: ${located.path}`);
|
|
46237
46414
|
}
|
|
46238
46415
|
this.stdio = serveStdio(() => this.protocol(), {
|
|
46416
|
+
legacy: "reject",
|
|
46239
46417
|
onerror: (error) => {
|
|
46240
46418
|
console.error("[MCP Error]", error);
|
|
46241
46419
|
}
|
|
@@ -46422,14 +46600,14 @@ class GodotServer {
|
|
|
46422
46600
|
}
|
|
46423
46601
|
noteTheStart() {
|
|
46424
46602
|
const cwd = process.cwd();
|
|
46425
|
-
const mcpJson =
|
|
46603
|
+
const mcpJson = join33(cwd, ".mcp.json");
|
|
46426
46604
|
const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
|
|
46427
46605
|
const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
|
|
46428
46606
|
writeLifecycle("started", {
|
|
46429
46607
|
argv: process.argv.slice(1),
|
|
46430
46608
|
cwd,
|
|
46431
46609
|
project: this.ownProject,
|
|
46432
|
-
mcpJson:
|
|
46610
|
+
mcpJson: existsSync26(mcpJson) ? mcpJson : null
|
|
46433
46611
|
});
|
|
46434
46612
|
if (predecessor !== null) {
|
|
46435
46613
|
writeLifecycle("replaced", { project: this.ownProject, predecessor });
|
|
@@ -46551,7 +46729,7 @@ class GodotServer {
|
|
|
46551
46729
|
return { name, version };
|
|
46552
46730
|
}
|
|
46553
46731
|
}
|
|
46554
|
-
return
|
|
46732
|
+
return;
|
|
46555
46733
|
};
|
|
46556
46734
|
protocol.setRequestHandler("tools/list", (_request, ctx) => {
|
|
46557
46735
|
this.noteTheClient(clientOf(ctx.mcpReq.envelope));
|
|
@@ -46564,13 +46742,13 @@ class GodotServer {
|
|
|
46564
46742
|
if (!spec) {
|
|
46565
46743
|
throw new ProtocolError(ProtocolErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}. Tools: ${TOOL_SPECS.map((tool) => tool.name).join(", ")}`);
|
|
46566
46744
|
}
|
|
46567
|
-
const args =
|
|
46745
|
+
const args = ownArguments(request.params.arguments);
|
|
46568
46746
|
const checked = this.validateArguments(spec, args);
|
|
46569
46747
|
if (!checked.ok) {
|
|
46570
46748
|
return checked.response;
|
|
46571
46749
|
}
|
|
46572
46750
|
const named = args["projectPath"];
|
|
46573
|
-
if (typeof named === "string" &&
|
|
46751
|
+
if (typeof named === "string" && existsSync26(join33(named, "project.godot"))) {
|
|
46574
46752
|
this.lastProjectPath = named;
|
|
46575
46753
|
}
|
|
46576
46754
|
this.updates.refresh();
|
|
@@ -46606,7 +46784,7 @@ class GodotServer {
|
|
|
46606
46784
|
let notSaved;
|
|
46607
46785
|
try {
|
|
46608
46786
|
savedAt = answer.content.map((item, index) => {
|
|
46609
|
-
const file =
|
|
46787
|
+
const file = join33(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
|
|
46610
46788
|
writeFileSync18(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
|
|
46611
46789
|
return file;
|
|
46612
46790
|
});
|
|
@@ -46740,11 +46918,6 @@ class GodotServer {
|
|
|
46740
46918
|
const installed = installedAddonVersion(this.ownProject);
|
|
46741
46919
|
return installed === null || installed === SERVER_VERSION ? null : installed;
|
|
46742
46920
|
}
|
|
46743
|
-
editorCodeOnDisk() {
|
|
46744
|
-
const status = this.godotBridge.getStatus();
|
|
46745
|
-
const project = (status.connected ? status.projectPath : undefined) ?? this.ownProject;
|
|
46746
|
-
return project === null || project === "" ? undefined : installedEditorDigest(project);
|
|
46747
|
-
}
|
|
46748
46921
|
runtimeStaleNote(endpoint) {
|
|
46749
46922
|
const project = endpoint.project.path;
|
|
46750
46923
|
return runtimeMismatch(endpoint.pid, endpoint.addon, SERVER_VERSION, shippedRuntimeDigest(), project === "" ? undefined : installedRuntimeDigest(project), project);
|
|
@@ -46779,9 +46952,13 @@ class GodotServer {
|
|
|
46779
46952
|
});
|
|
46780
46953
|
const takes = wanted.length === 0 ? "It takes no arguments." : `It takes: ${shaped.join(", ")}.`;
|
|
46781
46954
|
const where = named === null ? spec.name : `${spec.name} ${named}`;
|
|
46955
|
+
const spelled = unknown.flatMap((key) => {
|
|
46956
|
+
const camel = key.replace(/_([a-zA-Z0-9])/g, (_, letter) => letter.toUpperCase());
|
|
46957
|
+
return camel !== key && known.has(camel) ? [`${key} is spelled ${camel}`] : [];
|
|
46958
|
+
});
|
|
46782
46959
|
return {
|
|
46783
46960
|
ok: false,
|
|
46784
|
-
response: this.createErrorResponse(`${where} does not take ${unknown.join(", ")}. ${takes} This is gdharness ${SERVER_VERSION}, which is what decides whether an argument exists.`)
|
|
46961
|
+
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.`)
|
|
46785
46962
|
};
|
|
46786
46963
|
}
|
|
46787
46964
|
const wrong = [
|
|
@@ -46898,7 +47075,7 @@ class GodotServer {
|
|
|
46898
47075
|
const contained = this.containProjectFiles({ ...arguments_, ...extra });
|
|
46899
47076
|
return contained.ok ? await this.handleViaBridge(command, contained.value, op === "" ? tool : `${tool} ${op}`) : contained.response;
|
|
46900
47077
|
};
|
|
46901
|
-
const editorOrEngine = async (command, effect
|
|
47078
|
+
const editorOrEngine = async (command, effect) => await this.editorOrEngine(command, arguments_, `${tool} ${op}`, effect);
|
|
46902
47079
|
switch (tool) {
|
|
46903
47080
|
case "project_info":
|
|
46904
47081
|
return await this.handleProjectInfo(args);
|
|
@@ -46927,13 +47104,13 @@ class GodotServer {
|
|
|
46927
47104
|
case "set":
|
|
46928
47105
|
return await editorOrEngine("set_node_properties", "writes");
|
|
46929
47106
|
case "duplicate":
|
|
46930
|
-
return await editorOrEngine("duplicate_node", "writes"
|
|
47107
|
+
return await editorOrEngine("duplicate_node", "writes");
|
|
46931
47108
|
case "reparent":
|
|
46932
47109
|
return await editorOrEngine("reparent_node", "writes");
|
|
46933
47110
|
case "delete":
|
|
46934
47111
|
return await editorOrEngine("delete_node", "writes");
|
|
46935
47112
|
default:
|
|
46936
|
-
return await editorOrEngine("set_tilemap_cells", "writes"
|
|
47113
|
+
return await editorOrEngine("set_tilemap_cells", "writes");
|
|
46937
47114
|
}
|
|
46938
47115
|
case "scene_signal":
|
|
46939
47116
|
switch (op) {
|
|
@@ -46997,13 +47174,13 @@ class GodotServer {
|
|
|
46997
47174
|
case "modify":
|
|
46998
47175
|
return await editorOrEngine("modify_resource", "writes");
|
|
46999
47176
|
case "create_shader":
|
|
47000
|
-
return await editorOrEngine("create_shader", "writes"
|
|
47177
|
+
return await editorOrEngine("create_shader", "writes");
|
|
47001
47178
|
case "create_tileset":
|
|
47002
|
-
return await editorOrEngine("create_tileset", "writes"
|
|
47179
|
+
return await editorOrEngine("create_tileset", "writes");
|
|
47003
47180
|
case "set_theme_color":
|
|
47004
|
-
return await editorOrEngine("set_theme_color", "writes"
|
|
47181
|
+
return await editorOrEngine("set_theme_color", "writes");
|
|
47005
47182
|
default:
|
|
47006
|
-
return await editorOrEngine("set_theme_font_size", "writes"
|
|
47183
|
+
return await editorOrEngine("set_theme_font_size", "writes");
|
|
47007
47184
|
}
|
|
47008
47185
|
case "editor_launch":
|
|
47009
47186
|
if (op === "close") {
|
|
@@ -47224,8 +47401,8 @@ class GodotServer {
|
|
|
47224
47401
|
if (path === undefined) {
|
|
47225
47402
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
47226
47403
|
}
|
|
47227
|
-
const file =
|
|
47228
|
-
if (!
|
|
47404
|
+
const file = join33(path, "project.godot");
|
|
47405
|
+
if (!existsSync26(file)) {
|
|
47229
47406
|
return {
|
|
47230
47407
|
ok: false,
|
|
47231
47408
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -47396,6 +47573,42 @@ class GodotServer {
|
|
|
47396
47573
|
}
|
|
47397
47574
|
return { ok: true, value: stale };
|
|
47398
47575
|
}
|
|
47576
|
+
async importWhatChanged(projectPath) {
|
|
47577
|
+
if (staleImports(projectPath).length === 0) {
|
|
47578
|
+
return;
|
|
47579
|
+
}
|
|
47580
|
+
const unminted = scriptsWithoutUid(projectPath);
|
|
47581
|
+
const began = Date.now();
|
|
47582
|
+
const done = await this.reimport(projectPath, undefined, false, scanWaitMs());
|
|
47583
|
+
const tookMs = Date.now() - began;
|
|
47584
|
+
const stillUnminted = scriptsWithoutUid(projectPath);
|
|
47585
|
+
const uidsWritten = unminted.filter((script) => !stillUnminted.includes(script)).map((script) => `res://${script}`);
|
|
47586
|
+
if (!done.ok) {
|
|
47587
|
+
const why = done.response.content.map((block) => block.text).join(" ");
|
|
47588
|
+
return {
|
|
47589
|
+
imported: [],
|
|
47590
|
+
notImported: [],
|
|
47591
|
+
uidsWritten,
|
|
47592
|
+
tookMs,
|
|
47593
|
+
note: `Files changed since they were imported could not be imported before this run, so it reads their previous import: ${why}`
|
|
47594
|
+
};
|
|
47595
|
+
}
|
|
47596
|
+
const imported = (readArray(done.payload, "reimported") ?? []).map(String);
|
|
47597
|
+
const notImported = readArray(done.payload, "notReimported") ?? [];
|
|
47598
|
+
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;
|
|
47599
|
+
return { imported, notImported, uidsWritten, tookMs, ...note === undefined ? {} : { note } };
|
|
47600
|
+
}
|
|
47601
|
+
async afterTheImport(projectPath, waited, importedFirst) {
|
|
47602
|
+
if (importedFirst === undefined) {
|
|
47603
|
+
return waited;
|
|
47604
|
+
}
|
|
47605
|
+
const settled = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
47606
|
+
return {
|
|
47607
|
+
waitedMs: waited.waitedMs + settled.waitedMs,
|
|
47608
|
+
stillScanning: settled.stillScanning,
|
|
47609
|
+
importedFirst
|
|
47610
|
+
};
|
|
47611
|
+
}
|
|
47399
47612
|
async untilTheEditorsScanIsWritten(projectPath) {
|
|
47400
47613
|
const open = this.godotBridge.getStatus().projectPath;
|
|
47401
47614
|
if (!this.godotBridge.isConnected() || open === undefined || !isSameDirectory(open, projectPath)) {
|
|
@@ -47606,9 +47819,8 @@ class GodotServer {
|
|
|
47606
47819
|
return {};
|
|
47607
47820
|
} catch (error) {
|
|
47608
47821
|
const message = errorMessage(error);
|
|
47609
|
-
const why = message.includes("Unknown tool") ? "runs an addon from before writes were handed to it" : `could not take the change (${message})`;
|
|
47610
47822
|
return {
|
|
47611
|
-
editorNote: `The open editor ${
|
|
47823
|
+
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.`
|
|
47612
47824
|
};
|
|
47613
47825
|
}
|
|
47614
47826
|
}
|
|
@@ -47821,8 +48033,7 @@ class GodotServer {
|
|
|
47821
48033
|
const message = errorMessage(error);
|
|
47822
48034
|
return {
|
|
47823
48035
|
ok: false,
|
|
47824
|
-
response:
|
|
47825
|
-
"editor_launch restart loads the addon this server ships",
|
|
48036
|
+
response: error instanceof AddonOfAnotherRelease ? this.createErrorResponse(message, [
|
|
47826
48037
|
"Or close the editor, and project_import reimport runs the engine import instead"
|
|
47827
48038
|
]) : this.createErrorResponse(`The editor answered reimport_files with an error: ${message}`)
|
|
47828
48039
|
};
|
|
@@ -47859,7 +48070,7 @@ class GodotServer {
|
|
|
47859
48070
|
}
|
|
47860
48071
|
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
47861
48072
|
this.logDebug(`Running ${operation} in ${projectPath}: ${JSON.stringify(params)}`);
|
|
47862
|
-
const outcome = await runOperation({ godotPath: engine.value, script: this.operationsScript, debug:
|
|
48073
|
+
const outcome = await runOperation({ godotPath: engine.value, script: this.operationsScript, debug: DEBUG_MODE }, operation, params, projectPath);
|
|
47863
48074
|
return withScanWait(outcome, scanned);
|
|
47864
48075
|
}
|
|
47865
48076
|
answer(outcome) {
|
|
@@ -47891,7 +48102,7 @@ class GodotServer {
|
|
|
47891
48102
|
if (!engine.ok) {
|
|
47892
48103
|
return engine.response;
|
|
47893
48104
|
}
|
|
47894
|
-
const application = parseProjectGodot(
|
|
48105
|
+
const application = parseProjectGodot(readFileSync28(project.value.file, "utf8"))["application"] ?? {};
|
|
47895
48106
|
const name = application["config/name"];
|
|
47896
48107
|
const mainScene = application["run/main_scene"];
|
|
47897
48108
|
const info = {
|
|
@@ -47989,7 +48200,7 @@ class GodotServer {
|
|
|
47989
48200
|
const exportArgs = [
|
|
47990
48201
|
"--headless",
|
|
47991
48202
|
"--log-file",
|
|
47992
|
-
|
|
48203
|
+
join33(exportLogs, "engine.log"),
|
|
47993
48204
|
"--path",
|
|
47994
48205
|
project.value.path,
|
|
47995
48206
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -47997,7 +48208,7 @@ class GodotServer {
|
|
|
47997
48208
|
output.absolutePath
|
|
47998
48209
|
];
|
|
47999
48210
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
48000
|
-
const writtenAt = () =>
|
|
48211
|
+
const writtenAt = () => existsSync26(output.absolutePath) ? statSync15(output.absolutePath).mtimeMs : null;
|
|
48001
48212
|
const caches = editorCacheDirectory(engine.value);
|
|
48002
48213
|
const exportOnce = async () => {
|
|
48003
48214
|
const before = writtenAt();
|
|
@@ -48080,10 +48291,10 @@ class GodotServer {
|
|
|
48080
48291
|
if (movie !== null) {
|
|
48081
48292
|
return this.createErrorResponse(movie);
|
|
48082
48293
|
}
|
|
48083
|
-
const missing = paths.value.filter((path) => !
|
|
48294
|
+
const missing = paths.value.filter((path) => !existsSync26(join33(project.value.path, path.slice("res://".length))));
|
|
48084
48295
|
const asked = paths.value.filter((path) => !missing.includes(path));
|
|
48085
48296
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
48086
|
-
const noRunner = !
|
|
48297
|
+
const noRunner = !existsSync26(join33(project.value.path, runner));
|
|
48087
48298
|
if (asked.length === 0) {
|
|
48088
48299
|
const verdict = `nothing at ${missing.join(" and ")}`;
|
|
48089
48300
|
const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
|
|
@@ -48120,7 +48331,15 @@ class GodotServer {
|
|
|
48120
48331
|
if (!classes.ok) {
|
|
48121
48332
|
return this.answer(classes);
|
|
48122
48333
|
}
|
|
48123
|
-
const
|
|
48334
|
+
const rebuildWaited = scanWaitIn(classes.payload);
|
|
48335
|
+
const importWaited = scanWaitAnswer(await this.afterTheImport(project.value.path, { waitedMs: 0, stillScanning: false }, await this.importWhatChanged(project.value.path)));
|
|
48336
|
+
const waitedMs = (rebuildWaited.waitedForEditorScanMs ?? 0) + (importWaited.waitedForEditorScanMs ?? 0);
|
|
48337
|
+
const scanNote = importWaited.scanNote ?? rebuildWaited.scanNote;
|
|
48338
|
+
const scanned = {
|
|
48339
|
+
...importWaited,
|
|
48340
|
+
...waitedMs > 0 ? { waitedForEditorScanMs: waitedMs } : {},
|
|
48341
|
+
...scanNote === undefined ? {} : { scanNote }
|
|
48342
|
+
};
|
|
48124
48343
|
const ours = `gdharness-reports/${randomUUID2()}`;
|
|
48125
48344
|
const reports = `res://.godot/${ours}`;
|
|
48126
48345
|
const ignored = readStringArray(args, "ignore") ?? [];
|
|
@@ -48191,14 +48410,14 @@ class GodotServer {
|
|
|
48191
48410
|
await run.settled;
|
|
48192
48411
|
const tookMs = Date.now() - began;
|
|
48193
48412
|
const notCompiled = stopped.for ?? run.log.all.map((entry) => autoloadNotCompiled(entry, autoloaded)).find((found) => found !== null) ?? null;
|
|
48194
|
-
const reportsDir =
|
|
48413
|
+
const reportsDir = join33(project.value.path, ".godot", ...ours.split("/"));
|
|
48195
48414
|
let report = null;
|
|
48196
48415
|
let reportProblem = null;
|
|
48197
48416
|
try {
|
|
48198
|
-
const written =
|
|
48417
|
+
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))) : [];
|
|
48199
48418
|
const newest = written.at(-1);
|
|
48200
48419
|
if (newest !== undefined) {
|
|
48201
|
-
report = parseJUnit(
|
|
48420
|
+
report = parseJUnit(readFileSync28(join33(reportsDir, newest, "results.xml"), "utf8"));
|
|
48202
48421
|
}
|
|
48203
48422
|
} catch (error) {
|
|
48204
48423
|
reportProblem = errorMessage(error);
|
|
@@ -48398,7 +48617,7 @@ class GodotServer {
|
|
|
48398
48617
|
const checked = project.ok ? await this.engineDiagnostics(project.value.path, [readString(args, "scriptPath") ?? ""], one.noLanguageServer) : { refusal: project.response };
|
|
48399
48618
|
return "refusal" in checked ? checked.refusal : this.jsonTextResponse({ ...checked.answers[0], ...engineChecked(one.noLanguageServer.reason) });
|
|
48400
48619
|
}
|
|
48401
|
-
return "refusal" in one ? one.refusal : this.jsonTextResponse(
|
|
48620
|
+
return "refusal" in one ? one.refusal : this.jsonTextResponse(one.answer);
|
|
48402
48621
|
}
|
|
48403
48622
|
const progress = callProgress();
|
|
48404
48623
|
const cancelled = callSignal();
|
|
@@ -48448,7 +48667,7 @@ class GodotServer {
|
|
|
48448
48667
|
reported.push(one.answer);
|
|
48449
48668
|
}
|
|
48450
48669
|
}
|
|
48451
|
-
return this.jsonTextResponse(
|
|
48670
|
+
return this.jsonTextResponse({
|
|
48452
48671
|
clean: errors === 0 && unread === 0,
|
|
48453
48672
|
scriptsChecked: scripts.value.length,
|
|
48454
48673
|
errors,
|
|
@@ -48457,7 +48676,7 @@ class GodotServer {
|
|
|
48457
48676
|
scripts: reported,
|
|
48458
48677
|
nothingReported,
|
|
48459
48678
|
...byEngine === null ? {} : engineChecked(byEngine)
|
|
48460
|
-
})
|
|
48679
|
+
});
|
|
48461
48680
|
}
|
|
48462
48681
|
async compileScripts(projectPath, paths) {
|
|
48463
48682
|
const payload = { checked: [], failed: [], scripts: [] };
|
|
@@ -48514,13 +48733,6 @@ class GodotServer {
|
|
|
48514
48733
|
})
|
|
48515
48734
|
};
|
|
48516
48735
|
}
|
|
48517
|
-
markedIfStale(answer) {
|
|
48518
|
-
const status = this.godotBridge.getStatus();
|
|
48519
|
-
if (!hasSaidWhoItIs(status)) {
|
|
48520
|
-
return answer;
|
|
48521
|
-
}
|
|
48522
|
-
return markIfStale(answer, status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
|
|
48523
|
-
}
|
|
48524
48736
|
async diagnosticsOf(args) {
|
|
48525
48737
|
const waited = await this.waitForTheEditorOpenedHere(args, []);
|
|
48526
48738
|
if (waited !== null) {
|
|
@@ -48566,11 +48778,11 @@ class GodotServer {
|
|
|
48566
48778
|
const cached = cachedClasses(projectPath);
|
|
48567
48779
|
const sourceOf = (resourcePath) => {
|
|
48568
48780
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
48569
|
-
if (!contained.ok || !
|
|
48781
|
+
if (!contained.ok || !existsSync26(contained.absolutePath)) {
|
|
48570
48782
|
return null;
|
|
48571
48783
|
}
|
|
48572
48784
|
try {
|
|
48573
|
-
return
|
|
48785
|
+
return readFileSync28(contained.absolutePath, "utf8");
|
|
48574
48786
|
} catch {
|
|
48575
48787
|
return null;
|
|
48576
48788
|
}
|
|
@@ -48660,11 +48872,11 @@ class GodotServer {
|
|
|
48660
48872
|
}
|
|
48661
48873
|
sourceOfClass(projectPath, resourcePath) {
|
|
48662
48874
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
48663
|
-
if (!contained.ok || !
|
|
48875
|
+
if (!contained.ok || !existsSync26(contained.absolutePath)) {
|
|
48664
48876
|
return null;
|
|
48665
48877
|
}
|
|
48666
48878
|
try {
|
|
48667
|
-
return
|
|
48879
|
+
return readFileSync28(contained.absolutePath, "utf8");
|
|
48668
48880
|
} catch {
|
|
48669
48881
|
return null;
|
|
48670
48882
|
}
|
|
@@ -48787,12 +48999,11 @@ class GodotServer {
|
|
|
48787
48999
|
return client;
|
|
48788
49000
|
}
|
|
48789
49001
|
breakpointsAtRisk() {
|
|
48790
|
-
|
|
48791
|
-
if (!hasSaidWhoItIs(status) || status.syncsBreakpoints === true) {
|
|
49002
|
+
if (this.godotBridge.getStatus().syncsBreakpoints !== false) {
|
|
48792
49003
|
return {};
|
|
48793
49004
|
}
|
|
48794
49005
|
return {
|
|
48795
|
-
breakpointsAtRisk:
|
|
49006
|
+
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."
|
|
48796
49007
|
};
|
|
48797
49008
|
}
|
|
48798
49009
|
breakpointsForAGame(projectPath) {
|
|
@@ -48822,7 +49033,7 @@ class GodotServer {
|
|
|
48822
49033
|
...Array.isArray(stack) ? {
|
|
48823
49034
|
stack: stack.map((frame) => ({
|
|
48824
49035
|
...frame,
|
|
48825
|
-
...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path:
|
|
49036
|
+
...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join33(projectPath, frame["script"].slice("res://".length)) } : {}
|
|
48826
49037
|
}))
|
|
48827
49038
|
} : {}
|
|
48828
49039
|
});
|
|
@@ -48886,13 +49097,13 @@ class GodotServer {
|
|
|
48886
49097
|
const arriving = status.connected ? [] : arrivingFound;
|
|
48887
49098
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
48888
49099
|
const greeted = hasSaidWhoItIs(status);
|
|
48889
|
-
const
|
|
49100
|
+
const staleNote = this.godotBridge.anotherRelease();
|
|
48890
49101
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
48891
49102
|
const cannotConnect = status.connected || this.ownProject === null ? null : editorAddonMissing(this.ownProject);
|
|
48892
49103
|
return {
|
|
48893
49104
|
...status,
|
|
48894
49105
|
serverVersion: SERVER_VERSION,
|
|
48895
|
-
addonIsStale: greeted ?
|
|
49106
|
+
addonIsStale: greeted ? staleNote !== undefined : undefined,
|
|
48896
49107
|
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,
|
|
48897
49108
|
bridgeAvailable: this.bridgeStartupError === null && status.port !== null,
|
|
48898
49109
|
mayYetConnect: status.connected ? undefined : cannotConnect !== null ? false : arriving.length > 0 ? true : unfinished !== null ? unfinished.underway : this.closedHere !== null ? false : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
@@ -48926,8 +49137,8 @@ class GodotServer {
|
|
|
48926
49137
|
port: this.godotBridge.configuredPort,
|
|
48927
49138
|
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.`
|
|
48928
49139
|
},
|
|
48929
|
-
staleNote
|
|
48930
|
-
addonNote: greeted ? sameCodeNote(status.addonVersion
|
|
49140
|
+
staleNote,
|
|
49141
|
+
addonNote: greeted && staleNote === undefined ? sameCodeNote(status.addonVersion ?? "", SERVER_VERSION) : undefined,
|
|
48931
49142
|
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,
|
|
48932
49143
|
...this.breakpointsAtRisk(),
|
|
48933
49144
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
@@ -49012,7 +49223,7 @@ class GodotServer {
|
|
|
49012
49223
|
project: endpoint.project,
|
|
49013
49224
|
...this.ownProject === null ? {} : { ofThisProject: isSameDirectory(endpoint.project.path, this.ownProject) },
|
|
49014
49225
|
editorPid: endpoint.editorPid,
|
|
49015
|
-
addonVersion: endpoint.addon
|
|
49226
|
+
addonVersion: endpoint.addon.version === "" ? null : endpoint.addon.version,
|
|
49016
49227
|
...unknown ? {} : { addonIsStale: staleNote !== undefined },
|
|
49017
49228
|
...staleNote === undefined ? {} : { staleNote },
|
|
49018
49229
|
reachable: reply.ok,
|
|
@@ -49101,7 +49312,7 @@ class GodotServer {
|
|
|
49101
49312
|
return null;
|
|
49102
49313
|
}
|
|
49103
49314
|
try {
|
|
49104
|
-
return settingKeys(
|
|
49315
|
+
return settingKeys(readFileSync28(join33(projectPath, "project.godot"), "utf8"));
|
|
49105
49316
|
} catch {
|
|
49106
49317
|
return null;
|
|
49107
49318
|
}
|
|
@@ -49125,7 +49336,7 @@ class GodotServer {
|
|
|
49125
49336
|
]);
|
|
49126
49337
|
}
|
|
49127
49338
|
const settingsBefore = this.settingKeysOf(before.projectPath);
|
|
49128
|
-
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, args["hidden"] === true) : await this.handleViaBridge("restart_editor", {});
|
|
49339
|
+
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, args["hidden"] === true, resolveSilent(args["silent"])) : await this.handleViaBridge("restart_editor", {});
|
|
49129
49340
|
if (asked.isError === true) {
|
|
49130
49341
|
return asked;
|
|
49131
49342
|
}
|
|
@@ -49147,10 +49358,11 @@ class GodotServer {
|
|
|
49147
49358
|
editorPid: now.editorPid,
|
|
49148
49359
|
addonVersion: now.addonVersion,
|
|
49149
49360
|
serverVersion: SERVER_VERSION,
|
|
49150
|
-
addonIsStale:
|
|
49151
|
-
staleNote:
|
|
49361
|
+
addonIsStale: this.godotBridge.anotherRelease() !== undefined,
|
|
49362
|
+
staleNote: this.godotBridge.anotherRelease(),
|
|
49152
49363
|
...this.whatTheEditorDropped(settingsBefore, after),
|
|
49153
49364
|
...servers,
|
|
49365
|
+
...mine && this.launchedEditor !== null ? { arguments: this.launchedEditor.arguments } : {},
|
|
49154
49366
|
tookMs: Date.now() - began
|
|
49155
49367
|
});
|
|
49156
49368
|
}
|
|
@@ -49239,7 +49451,7 @@ class GodotServer {
|
|
|
49239
49451
|
whatTheEditorDropped(before, after) {
|
|
49240
49452
|
return settingsDroppedReport(before, after);
|
|
49241
49453
|
}
|
|
49242
|
-
async startItAgain(projectPath, editorPid, hidden) {
|
|
49454
|
+
async startItAgain(projectPath, editorPid, hidden, silent) {
|
|
49243
49455
|
const engine = await this.engine();
|
|
49244
49456
|
if (!engine.ok) {
|
|
49245
49457
|
return engine.response;
|
|
@@ -49268,7 +49480,7 @@ class GodotServer {
|
|
|
49268
49480
|
`editor_launch open starts one on ${projectPath} once it has gone`
|
|
49269
49481
|
]);
|
|
49270
49482
|
}
|
|
49271
|
-
const opened = await this.openAnEditor(engine.value, projectPath, ports, hidden);
|
|
49483
|
+
const opened = await this.openAnEditor(engine.value, projectPath, ports, hidden, silent);
|
|
49272
49484
|
restartSettled(projectPath);
|
|
49273
49485
|
if (opened.error !== null) {
|
|
49274
49486
|
return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
|
|
@@ -49318,7 +49530,7 @@ class GodotServer {
|
|
|
49318
49530
|
const ports = await this.portsForAnEditor();
|
|
49319
49531
|
const before = this.settingKeysOf(project.value.path);
|
|
49320
49532
|
const unfinished = restartOwed(project.value.path);
|
|
49321
|
-
const opened = await this.openAnEditor(engine.value, project.value.path, ports, args["hidden"] === true);
|
|
49533
|
+
const opened = await this.openAnEditor(engine.value, project.value.path, ports, args["hidden"] === true, resolveSilent(args["silent"]));
|
|
49322
49534
|
if (opened.error !== null) {
|
|
49323
49535
|
return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
|
|
49324
49536
|
}
|
|
@@ -49330,6 +49542,7 @@ class GodotServer {
|
|
|
49330
49542
|
projectPath: project.value.path,
|
|
49331
49543
|
lspPort: ports.lsp,
|
|
49332
49544
|
dapPort: ports.dap,
|
|
49545
|
+
arguments: opened.arguments,
|
|
49333
49546
|
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.`,
|
|
49334
49547
|
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."
|
|
49335
49548
|
});
|
|
@@ -49364,12 +49577,13 @@ class GodotServer {
|
|
|
49364
49577
|
}
|
|
49365
49578
|
return null;
|
|
49366
49579
|
}
|
|
49367
|
-
async openAnEditor(engine, projectPath, ports, hidden) {
|
|
49580
|
+
async openAnEditor(engine, projectPath, ports, hidden, silent) {
|
|
49368
49581
|
clearEditorLog(projectPath);
|
|
49369
49582
|
freeLibraryCopies(projectPath);
|
|
49583
|
+
const args = editorArguments(projectPath, ports, editorLogPath(projectPath), hidden, silent);
|
|
49370
49584
|
const launched = await launchOutsideTheTree({
|
|
49371
49585
|
command: engine,
|
|
49372
|
-
args
|
|
49586
|
+
args,
|
|
49373
49587
|
env: {
|
|
49374
49588
|
...process.env,
|
|
49375
49589
|
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
@@ -49380,16 +49594,17 @@ class GodotServer {
|
|
|
49380
49594
|
...hidden ? { desktop: HIDDEN_DESKTOP } : { noActivate: true }
|
|
49381
49595
|
});
|
|
49382
49596
|
if ("error" in launched) {
|
|
49383
|
-
return { pid: null, error: launched.error };
|
|
49597
|
+
return { pid: null, error: launched.error, arguments: args };
|
|
49384
49598
|
}
|
|
49385
49599
|
writeEditorLogNote(projectPath, launched.pid);
|
|
49386
49600
|
this.launchedEditor = {
|
|
49387
49601
|
pid: launched.pid,
|
|
49388
49602
|
startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null,
|
|
49389
|
-
projectPath
|
|
49603
|
+
projectPath,
|
|
49604
|
+
arguments: args
|
|
49390
49605
|
};
|
|
49391
49606
|
this.closedHere = null;
|
|
49392
|
-
return { pid: launched.pid, error: null };
|
|
49607
|
+
return { pid: launched.pid, error: null, arguments: args };
|
|
49393
49608
|
}
|
|
49394
49609
|
async portsForAnEditor() {
|
|
49395
49610
|
const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
|
|
@@ -49454,11 +49669,13 @@ class GodotServer {
|
|
|
49454
49669
|
if (!engine.ok) {
|
|
49455
49670
|
return engine.response;
|
|
49456
49671
|
}
|
|
49457
|
-
const
|
|
49672
|
+
const waited = await this.untilTheEditorsScanIsWritten(project.value.path);
|
|
49458
49673
|
const refreshed = await this.refreshStaleClasses(project.value.path);
|
|
49459
49674
|
if (!refreshed.ok) {
|
|
49460
49675
|
return refreshed.response;
|
|
49461
49676
|
}
|
|
49677
|
+
const imported = exported === null ? await this.importWhatChanged(project.value.path) : undefined;
|
|
49678
|
+
const scanned = await this.afterTheImport(project.value.path, waited, imported);
|
|
49462
49679
|
const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
|
|
49463
49680
|
if (op === "check") {
|
|
49464
49681
|
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, scanned, asked.value, flags.value);
|
|
@@ -49487,9 +49704,10 @@ class GodotServer {
|
|
|
49487
49704
|
});
|
|
49488
49705
|
const runtimeWaitMs = this.announceBudget(args, project.value.path, sceneArgument);
|
|
49489
49706
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
49490
|
-
const silent = resolveSilent(args["silent"]
|
|
49707
|
+
const silent = resolveSilent(args["silent"]) && !headless;
|
|
49708
|
+
const silentOnlyHere = silent && !editorPlaysHeadless(project.value.file);
|
|
49491
49709
|
const profiled = readBoolean(args, "profile") === true;
|
|
49492
|
-
if (exported === null && this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && flags.value.length === 0 && !needsItsOwnProcess(asked.value) && !
|
|
49710
|
+
if (exported === null && this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && flags.value.length === 0 && !needsItsOwnProcess(asked.value) && !silentOnlyHere) {
|
|
49493
49711
|
return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended, profiled);
|
|
49494
49712
|
}
|
|
49495
49713
|
const cmdArgs = runArguments({
|
|
@@ -49510,7 +49728,7 @@ class GodotServer {
|
|
|
49510
49728
|
started.announcedBefore = alreadyPlaying;
|
|
49511
49729
|
started.bootScene = runtimeWaitMs.scene;
|
|
49512
49730
|
this.activeProcess = started;
|
|
49513
|
-
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 &&
|
|
49731
|
+
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}` : "";
|
|
49514
49732
|
const endedForThis = endedToStartThis(ended);
|
|
49515
49733
|
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
49516
49734
|
const handed = this.dapHoldingBreakpointsOf(project.value.path).breakpointsHeld();
|
|
@@ -49539,7 +49757,7 @@ class GodotServer {
|
|
|
49539
49757
|
}
|
|
49540
49758
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
49541
49759
|
const budgetMs = budget.ms;
|
|
49542
|
-
if (!
|
|
49760
|
+
if (!existsSync26(join33(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
49543
49761
|
return runtimeVerdict(null, {
|
|
49544
49762
|
addon: false,
|
|
49545
49763
|
budgetMs,
|
|
@@ -49558,7 +49776,7 @@ class GodotServer {
|
|
|
49558
49776
|
const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
|
|
49559
49777
|
const fresh = await announcedSince(projectPath, before, {
|
|
49560
49778
|
budgetMs: Math.max(runBegan + budgetMs - waitBegan, 0),
|
|
49561
|
-
accept: (one) => this.isTheGameOf(this.activeProcess, one
|
|
49779
|
+
accept: (one) => this.isTheGameOf(this.activeProcess, one),
|
|
49562
49780
|
giveUp: () => {
|
|
49563
49781
|
if (this.dapClient?.isStopped() === true) {
|
|
49564
49782
|
return true;
|
|
@@ -49597,7 +49815,7 @@ class GodotServer {
|
|
|
49597
49815
|
if (going !== null && !running) {
|
|
49598
49816
|
this.drainTranscript(going);
|
|
49599
49817
|
}
|
|
49600
|
-
|
|
49818
|
+
const verdict = runtimeVerdict(endpoint, {
|
|
49601
49819
|
addon: true,
|
|
49602
49820
|
budgetMs,
|
|
49603
49821
|
...budget.sizedToMs === undefined ? {} : { sizedToMs: budget.sizedToMs },
|
|
@@ -49608,6 +49826,8 @@ class GodotServer {
|
|
|
49608
49826
|
...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {},
|
|
49609
49827
|
runtimeCameUp: going !== null && !running && going.log.all.some((entry) => entry.text.startsWith(RUNTIME_LISTENING_LINE))
|
|
49610
49828
|
});
|
|
49829
|
+
const staleNote = endpoint === null ? undefined : this.runtimeStaleNote(endpoint);
|
|
49830
|
+
return staleNote === undefined ? verdict : { ...verdict, addonIsStale: true, staleNote };
|
|
49611
49831
|
}
|
|
49612
49832
|
announceBudget(args, projectPath, scene) {
|
|
49613
49833
|
const key = scene ?? MAIN_SCENE;
|
|
@@ -49625,7 +49845,7 @@ class GodotServer {
|
|
|
49625
49845
|
}
|
|
49626
49846
|
let announcedAt;
|
|
49627
49847
|
try {
|
|
49628
|
-
announcedAt =
|
|
49848
|
+
announcedAt = statSync15(endpoint.file).mtimeMs;
|
|
49629
49849
|
} catch {
|
|
49630
49850
|
return;
|
|
49631
49851
|
}
|
|
@@ -49808,7 +50028,7 @@ class GodotServer {
|
|
|
49808
50028
|
return;
|
|
49809
50029
|
}
|
|
49810
50030
|
const before = run.announcedBefore ?? new Set;
|
|
49811
|
-
const theOne = this.announcedOfTheRunsProject(run, runtimesAnnounced().running).find((one) => one.pid === Number(named) && !before.has(one.pid) && this.isTheGameOf(run, one
|
|
50031
|
+
const theOne = this.announcedOfTheRunsProject(run, runtimesAnnounced().running).find((one) => one.pid === Number(named) && !before.has(one.pid) && this.isTheGameOf(run, one));
|
|
49812
50032
|
if (theOne !== undefined) {
|
|
49813
50033
|
this.tie(run, theOne);
|
|
49814
50034
|
}
|
|
@@ -49819,7 +50039,7 @@ class GodotServer {
|
|
|
49819
50039
|
return 0;
|
|
49820
50040
|
}
|
|
49821
50041
|
try {
|
|
49822
|
-
return reportAlreadyInTranscript(
|
|
50042
|
+
return reportAlreadyInTranscript(readFileSync28(game.transcript), readFileSync28(report));
|
|
49823
50043
|
} catch (error) {
|
|
49824
50044
|
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
49825
50045
|
return 0;
|
|
@@ -49902,16 +50122,16 @@ class GodotServer {
|
|
|
49902
50122
|
return ended;
|
|
49903
50123
|
}
|
|
49904
50124
|
}
|
|
49905
|
-
const
|
|
50125
|
+
const { startedBy, command, projectPath } = running;
|
|
50126
|
+
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({
|
|
49906
50127
|
pid: running.pid,
|
|
49907
50128
|
transcript: running.transcript ?? "",
|
|
49908
50129
|
startedAt: running.startedAt,
|
|
49909
|
-
|
|
49910
|
-
projectPath
|
|
50130
|
+
startedBy,
|
|
50131
|
+
projectPath,
|
|
49911
50132
|
arguments: [],
|
|
49912
|
-
|
|
49913
|
-
};
|
|
49914
|
-
const why = await whyNotStillTheRecordedRun(startedAs);
|
|
50133
|
+
command
|
|
50134
|
+
});
|
|
49915
50135
|
if (why !== null) {
|
|
49916
50136
|
clearTheRecord();
|
|
49917
50137
|
running.endedHere = null;
|
|
@@ -50196,7 +50416,7 @@ class GodotServer {
|
|
|
50196
50416
|
return run.announcedPid;
|
|
50197
50417
|
}
|
|
50198
50418
|
const before = run.announcedBefore ?? new Set;
|
|
50199
|
-
const candidates = ours.filter((one) => !before.has(one.pid) && this.isTheGameOf(run, one
|
|
50419
|
+
const candidates = ours.filter((one) => !before.has(one.pid) && this.isTheGameOf(run, one));
|
|
50200
50420
|
const theOne = candidates.length === 1 ? candidates[0] : undefined;
|
|
50201
50421
|
if (theOne !== undefined) {
|
|
50202
50422
|
this.tie(run, theOne);
|
|
@@ -50306,22 +50526,16 @@ class GodotServer {
|
|
|
50306
50526
|
run.announcedPid = endpoint.pid;
|
|
50307
50527
|
this.noteBootOf(run, endpoint);
|
|
50308
50528
|
}
|
|
50309
|
-
playedByOurEditor(endpoint
|
|
50529
|
+
playedByOurEditor(endpoint) {
|
|
50310
50530
|
const editor = this.godotBridge.getStatus().editorPid;
|
|
50311
|
-
|
|
50312
|
-
return true;
|
|
50313
|
-
}
|
|
50314
|
-
if (endpoint.editorPid !== undefined) {
|
|
50315
|
-
return endpoint.editorPid === editor;
|
|
50316
|
-
}
|
|
50317
|
-
return !runtimeAddonAnnouncesEditors(projectPath ?? this.ownProject);
|
|
50531
|
+
return editor === undefined || endpoint.editorPid === editor;
|
|
50318
50532
|
}
|
|
50319
|
-
isTheGameOf(run, endpoint
|
|
50533
|
+
isTheGameOf(run, endpoint) {
|
|
50320
50534
|
if (run === null) {
|
|
50321
50535
|
return false;
|
|
50322
50536
|
}
|
|
50323
50537
|
if (run.throughEditor) {
|
|
50324
|
-
return this.playedByOurEditor(endpoint
|
|
50538
|
+
return this.playedByOurEditor(endpoint);
|
|
50325
50539
|
}
|
|
50326
50540
|
const editor = this.godotBridge.getStatus().editorPid;
|
|
50327
50541
|
return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
|
|
@@ -50339,12 +50553,12 @@ class GodotServer {
|
|
|
50339
50553
|
log: new GameLog,
|
|
50340
50554
|
transcript: record.transcript,
|
|
50341
50555
|
readOffset: 0,
|
|
50342
|
-
projectPath: record.projectPath
|
|
50556
|
+
projectPath: record.projectPath,
|
|
50343
50557
|
startedAt: record.startedAt,
|
|
50344
|
-
|
|
50558
|
+
startedBy: record.startedBy,
|
|
50345
50559
|
...record.keeper === undefined ? {} : { keeper: record.keeper },
|
|
50346
50560
|
...record.profile === undefined ? {} : { profile: record.profile },
|
|
50347
|
-
|
|
50561
|
+
command: record.command,
|
|
50348
50562
|
exitCode: null,
|
|
50349
50563
|
exitSignal: null,
|
|
50350
50564
|
throughEditor: false,
|
|
@@ -50397,9 +50611,8 @@ class GodotServer {
|
|
|
50397
50611
|
if (record === null) {
|
|
50398
50612
|
return `No game is running.${noEditor} Start one with editor_run.`;
|
|
50399
50613
|
}
|
|
50400
|
-
const whose = record.projectPath === "" ? "a project it does not name" : record.projectPath;
|
|
50401
50614
|
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}`;
|
|
50402
|
-
return `No game of this server's is running.${noEditor} A run started from ${
|
|
50615
|
+
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.`;
|
|
50403
50616
|
}
|
|
50404
50617
|
aRuntimeOfOursIsAnswering() {
|
|
50405
50618
|
return this.announcedForOurProject(discoverRuntimes());
|
|
@@ -50521,7 +50734,7 @@ class GodotServer {
|
|
|
50521
50734
|
};
|
|
50522
50735
|
const going = await this.runStillGoing(run);
|
|
50523
50736
|
const projectPath = run.projectPath;
|
|
50524
|
-
const projectGone = projectPath !== null && !
|
|
50737
|
+
const projectGone = projectPath !== null && !existsSync26(join33(projectPath, "project.godot"));
|
|
50525
50738
|
if (projectGone && callersName !== undefined) {
|
|
50526
50739
|
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"]);
|
|
50527
50740
|
}
|
|
@@ -50868,7 +51081,7 @@ class GodotServer {
|
|
|
50868
51081
|
notes.push(endedWithoutACode({
|
|
50869
51082
|
throughEditor: run.throughEditor,
|
|
50870
51083
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
50871
|
-
couldAnnounce: run.projectPath !== null &&
|
|
51084
|
+
couldAnnounce: run.projectPath !== null && existsSync26(join33(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
50872
51085
|
endedHere: typeof run.endedHere === "string",
|
|
50873
51086
|
pickedUp: run.pickedUp === true
|
|
50874
51087
|
}));
|
|
@@ -51185,10 +51398,9 @@ class GodotServer {
|
|
|
51185
51398
|
idleSince = null;
|
|
51186
51399
|
continue;
|
|
51187
51400
|
}
|
|
51188
|
-
const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
|
|
51189
51401
|
const completed = readNumber(status, "scansCompleted");
|
|
51190
51402
|
const notYetOver = completedBefore !== undefined && completed !== undefined && completed <= completedBefore;
|
|
51191
|
-
busy = Boolean(status["scanning"]) || Boolean(status["importing"]) ||
|
|
51403
|
+
busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || notYetOver;
|
|
51192
51404
|
if (busy) {
|
|
51193
51405
|
idleSince = null;
|
|
51194
51406
|
} else {
|
|
@@ -51248,7 +51460,7 @@ class GodotServer {
|
|
|
51248
51460
|
constantsBefore: listed("heldConstantsBefore")["heldConstantsBefore"] ?? [],
|
|
51249
51461
|
constantsAfter: listed("constants")["constants"] ?? [],
|
|
51250
51462
|
names: projectPath === "" ? [] : this.classesAScriptNames(projectPath, script),
|
|
51251
|
-
restored:
|
|
51463
|
+
restored: answer["restored"] === true,
|
|
51252
51464
|
restoredAs: readString(answer, "restoredAs") ?? ""
|
|
51253
51465
|
})
|
|
51254
51466
|
};
|
|
@@ -51352,7 +51564,7 @@ class GodotServer {
|
|
|
51352
51564
|
}
|
|
51353
51565
|
const failed = answer["failed"];
|
|
51354
51566
|
if (typeof failed === "number" && failed !== 0) {
|
|
51355
|
-
const left = answer["restored"] === true ? "" :
|
|
51567
|
+
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";
|
|
51356
51568
|
notReloaded.push({
|
|
51357
51569
|
scriptPath,
|
|
51358
51570
|
problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}${left}`
|
|
@@ -51498,7 +51710,7 @@ class GodotServer {
|
|
|
51498
51710
|
if (!scriptPath.ok) {
|
|
51499
51711
|
return this.createErrorResponse(scriptPath.reason);
|
|
51500
51712
|
}
|
|
51501
|
-
if (!
|
|
51713
|
+
if (!existsSync26(scriptPath.absolutePath)) {
|
|
51502
51714
|
return this.createErrorResponse(`Script file does not exist: ${scriptPath.absolutePath}`);
|
|
51503
51715
|
}
|
|
51504
51716
|
const asked = readNonEmptyString(args, "newScriptPath");
|
|
@@ -51912,7 +52124,7 @@ class GodotServer {
|
|
|
51912
52124
|
}
|
|
51913
52125
|
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
51914
52126
|
const declared = new RegExp(`^\\s*(?:@\\w+(?:\\([^)]*\\))?\\s+)*class_name\\s+${escaped}\\b`, "m");
|
|
51915
|
-
if (declared.test(
|
|
52127
|
+
if (declared.test(readFileSync28(scriptPath, "utf8"))) {
|
|
51916
52128
|
return null;
|
|
51917
52129
|
}
|
|
51918
52130
|
const checked = await this.classesTheEditorCannotSee(args);
|
|
@@ -52011,20 +52223,18 @@ class GodotServer {
|
|
|
52011
52223
|
]);
|
|
52012
52224
|
}
|
|
52013
52225
|
try {
|
|
52014
|
-
return this.jsonTextResponse(
|
|
52226
|
+
return this.jsonTextResponse(await this.godotBridge.invokeTool(toolName, args));
|
|
52015
52227
|
} catch (error) {
|
|
52016
52228
|
if (error instanceof BridgeTimeout) {
|
|
52017
52229
|
return error;
|
|
52018
52230
|
}
|
|
52231
|
+
if (error instanceof AddonOfAnotherRelease) {
|
|
52232
|
+
return this.createErrorResponse(error.message, ["editor_status says which addon the editor holds"]);
|
|
52233
|
+
}
|
|
52019
52234
|
const message = errorMessage(error);
|
|
52020
52235
|
if (message.includes(`Unknown tool: ${toolName}`)) {
|
|
52021
|
-
const
|
|
52022
|
-
|
|
52023
|
-
if (remedy === undefined) {
|
|
52024
|
-
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`;
|
|
52025
|
-
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"]);
|
|
52026
|
-
}
|
|
52027
|
-
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"]);
|
|
52236
|
+
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`;
|
|
52237
|
+
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"]);
|
|
52028
52238
|
}
|
|
52029
52239
|
return this.createErrorResponse(`The editor answered ${named} with an error: ${message}`);
|
|
52030
52240
|
}
|
|
@@ -52080,7 +52290,7 @@ class GodotServer {
|
|
|
52080
52290
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
52081
52291
|
const run = await this.currentRun();
|
|
52082
52292
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
52083
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
52293
|
+
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)) {
|
|
52084
52294
|
return null;
|
|
52085
52295
|
}
|
|
52086
52296
|
const already = runtimesAnnounced().running;
|
|
@@ -52089,12 +52299,12 @@ class GodotServer {
|
|
|
52089
52299
|
return !run.announcedBefore.has(one.pid);
|
|
52090
52300
|
}
|
|
52091
52301
|
try {
|
|
52092
|
-
return
|
|
52302
|
+
return statSync15(one.file).mtimeMs >= run.startedAt;
|
|
52093
52303
|
} catch {
|
|
52094
52304
|
return false;
|
|
52095
52305
|
}
|
|
52096
52306
|
};
|
|
52097
|
-
if (already.some((one) => announcedSinceTheStart(one) && this.isTheGameOf(run, one
|
|
52307
|
+
if (already.some((one) => announcedSinceTheStart(one) && this.isTheGameOf(run, one))) {
|
|
52098
52308
|
return null;
|
|
52099
52309
|
}
|
|
52100
52310
|
const which = run.throughEditor ? "the game the editor is playing for it" : `pid ${run.pid ?? "unknown"}`;
|
|
@@ -52106,7 +52316,7 @@ class GodotServer {
|
|
|
52106
52316
|
}
|
|
52107
52317
|
const fresh = await announcedSince(project, new Set(already.map((one) => one.pid)), {
|
|
52108
52318
|
budgetMs: Math.min(budgetMs, remaining),
|
|
52109
|
-
accept: (one) => this.isTheGameOf(this.activeProcess, one
|
|
52319
|
+
accept: (one) => this.isTheGameOf(this.activeProcess, one),
|
|
52110
52320
|
giveUp: () => this.activeProcess !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
|
|
52111
52321
|
});
|
|
52112
52322
|
const endpoint = await this.theRunsAmong(run, fresh);
|
|
@@ -52169,13 +52379,13 @@ class GodotServer {
|
|
|
52169
52379
|
return this.createErrorResponse(refusal);
|
|
52170
52380
|
}
|
|
52171
52381
|
}
|
|
52172
|
-
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
52173
|
-
const screenshotPath = screenshotDir ? join32(screenshotDir, "capture.png") : null;
|
|
52174
52382
|
const staleNote = this.runtimeStaleNote(choice.endpoint);
|
|
52175
|
-
|
|
52176
|
-
|
|
52177
|
-
|
|
52178
|
-
|
|
52383
|
+
if (staleNote !== undefined) {
|
|
52384
|
+
return this.createErrorResponse(staleNote);
|
|
52385
|
+
}
|
|
52386
|
+
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
52387
|
+
const screenshotPath = screenshotDir ? join33(screenshotDir, "capture.png") : null;
|
|
52388
|
+
const refused = (message) => this.createErrorResponse(message);
|
|
52179
52389
|
const onGame = {
|
|
52180
52390
|
pid: choice.endpoint.pid,
|
|
52181
52391
|
what: `${asked.tool} ${asked.op}`,
|
|
@@ -52201,7 +52411,6 @@ ${staleNote}`);
|
|
|
52201
52411
|
waitedMs: timeoutMs,
|
|
52202
52412
|
note: `${reply.message} ${longerWaitNote(asked.tool, asked.op, timeoutMs)}`,
|
|
52203
52413
|
...answeredBy,
|
|
52204
|
-
...stale,
|
|
52205
52414
|
...alongside()
|
|
52206
52415
|
});
|
|
52207
52416
|
}
|
|
@@ -52210,7 +52419,7 @@ ${staleNote}`);
|
|
|
52210
52419
|
}
|
|
52211
52420
|
const { id: _id, ...payload } = reply.payload;
|
|
52212
52421
|
if (!expectsScreenshot) {
|
|
52213
|
-
return this.jsonTextResponse({ ...payload, ...answeredBy, ...
|
|
52422
|
+
return this.jsonTextResponse({ ...payload, ...answeredBy, ...alongside() });
|
|
52214
52423
|
}
|
|
52215
52424
|
const returnedPath = readString(payload, "path");
|
|
52216
52425
|
if (readString(payload, "type") !== "screenshot_file" || !returnedPath || !screenshotPath) {
|
|
@@ -52234,11 +52443,9 @@ ${staleNote}`);
|
|
|
52234
52443
|
type: "text",
|
|
52235
52444
|
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${ranWith.length === 0 ? "" : `
|
|
52236
52445
|
|
|
52237
|
-
Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}
|
|
52238
|
-
|
|
52239
|
-
${staleNote}`}`
|
|
52446
|
+
Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}`
|
|
52240
52447
|
},
|
|
52241
|
-
{ type: "image", data:
|
|
52448
|
+
{ type: "image", data: readFileSync28(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
52242
52449
|
]
|
|
52243
52450
|
};
|
|
52244
52451
|
} finally {
|
|
@@ -52355,7 +52562,7 @@ async function runGodotServer() {
|
|
|
52355
52562
|
const server = new GodotServer;
|
|
52356
52563
|
await server.run();
|
|
52357
52564
|
}
|
|
52358
|
-
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,
|
|
52565
|
+
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;
|
|
52359
52566
|
var init_server = __esm(() => {
|
|
52360
52567
|
init_dist();
|
|
52361
52568
|
init_stdio();
|
|
@@ -52384,6 +52591,7 @@ var init_server = __esm(() => {
|
|
|
52384
52591
|
init_godot_path();
|
|
52385
52592
|
init_headless();
|
|
52386
52593
|
init_headless_operations();
|
|
52594
|
+
init_import_freshness();
|
|
52387
52595
|
init_issues();
|
|
52388
52596
|
init_junit();
|
|
52389
52597
|
init_keeper_channel();
|
|
@@ -52476,8 +52684,8 @@ var init_server = __esm(() => {
|
|
|
52476
52684
|
init_bridge_announce();
|
|
52477
52685
|
init_errors();
|
|
52478
52686
|
init_godot_path();
|
|
52479
|
-
import { existsSync as
|
|
52480
|
-
import { join as
|
|
52687
|
+
import { existsSync as existsSync27 } from "node:fs";
|
|
52688
|
+
import { join as join34, resolve as resolve8 } from "node:path";
|
|
52481
52689
|
|
|
52482
52690
|
// src/harnesses.ts
|
|
52483
52691
|
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";
|
|
@@ -53472,10 +53680,10 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
53472
53680
|
- \`editor_status\` says whether an editor is connected and whether its addon matches the server.
|
|
53473
53681
|
\`addonIsStale\` means the editor is running different addon code from what the server ships
|
|
53474
53682
|
(an older \`addonVersion\` on the same code is not stale), and \`staleNote\` says which half is behind: an editor that
|
|
53475
|
-
needs restarting,
|
|
53476
|
-
|
|
53477
|
-
|
|
53478
|
-
|
|
53683
|
+
needs restarting, a project that needs the upgrade command, or a server that needs reconnecting
|
|
53684
|
+
in your harness. A stale editor is asked nothing but to quit, and every other call to it is
|
|
53685
|
+
refused with the same note. A game's runtime addon is judged the same way, under each entry in
|
|
53686
|
+
\`runtimes\`, and a \`runtime_*\` call to a stale game is refused with its note.
|
|
53479
53687
|
- Start the game with \`editor_run start\`, never by spawning an engine. The editor plays it, so
|
|
53480
53688
|
its debugger holds it, which is what gives the \`debug_*\` tools something to talk to. Its answer
|
|
53481
53689
|
says under \`runtime\` whether the game can be talked to yet; when it cannot, \`mayYetAnnounce\`
|
|
@@ -53761,7 +53969,7 @@ class UsageError extends Refusal {
|
|
|
53761
53969
|
function projectArgument(at) {
|
|
53762
53970
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
53763
53971
|
const projectPath = resolve8(named ?? ".");
|
|
53764
|
-
if (!
|
|
53972
|
+
if (!existsSync27(join34(projectPath, "project.godot"))) {
|
|
53765
53973
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
53766
53974
|
}
|
|
53767
53975
|
return projectPath;
|
|
@@ -53769,11 +53977,11 @@ function projectArgument(at) {
|
|
|
53769
53977
|
async function engine(projectPath) {
|
|
53770
53978
|
const located = await new GodotLocator().find();
|
|
53771
53979
|
if (located.ok) {
|
|
53772
|
-
return { godotPath: located.path, script: shippedOperationsScript(), debug:
|
|
53980
|
+
return { godotPath: located.path, script: shippedOperationsScript(), debug: DEBUG_MODE };
|
|
53773
53981
|
}
|
|
53774
53982
|
const recorded = located.named === null && projectPath !== undefined ? recordedEnginePath(projectPath) : null;
|
|
53775
53983
|
if (recorded !== null) {
|
|
53776
|
-
return { godotPath: recorded, script: shippedOperationsScript(), debug:
|
|
53984
|
+
return { godotPath: recorded, script: shippedOperationsScript(), debug: DEBUG_MODE };
|
|
53777
53985
|
}
|
|
53778
53986
|
const main = located.named === null && projectPath !== undefined ? mainCheckoutOf(projectPath) : null;
|
|
53779
53987
|
const theirs = main === null ? null : recordedEnginePath(main);
|
|
@@ -53782,7 +53990,7 @@ async function engine(projectPath) {
|
|
|
53782
53990
|
throw new UsageError(`${refusal.message} ${refusal.advice.join(". ")}.`);
|
|
53783
53991
|
}
|
|
53784
53992
|
console.error(`engine: ${theirs}, which the harness config of ${main} names; this project is a git worktree of it`);
|
|
53785
|
-
return { godotPath: theirs, script: shippedOperationsScript(), debug:
|
|
53993
|
+
return { godotPath: theirs, script: shippedOperationsScript(), debug: DEBUG_MODE };
|
|
53786
53994
|
}
|
|
53787
53995
|
function said2(outcome, what) {
|
|
53788
53996
|
if (!outcome.ok) {
|
|
@@ -53806,7 +54014,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
53806
54014
|
return;
|
|
53807
54015
|
}
|
|
53808
54016
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
53809
|
-
if (!
|
|
54017
|
+
if (!existsSync27(join34(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
53810
54018
|
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}`);
|
|
53811
54019
|
}
|
|
53812
54020
|
}
|
|
@@ -53938,7 +54146,7 @@ async function setup() {
|
|
|
53938
54146
|
console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
|
|
53939
54147
|
}
|
|
53940
54148
|
if (!args.includes("--no-skill")) {
|
|
53941
|
-
const directories = skillDirectories(chosen.harnesses, projectPath,
|
|
54149
|
+
const directories = skillDirectories(chosen.harnesses, projectPath, existsSync27);
|
|
53942
54150
|
for (const written of writeSkill(directories, getLocalVersion())) {
|
|
53943
54151
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
53944
54152
|
}
|
|
@@ -54052,14 +54260,14 @@ async function upgrade() {
|
|
|
54052
54260
|
moved.push(took.from);
|
|
54053
54261
|
}
|
|
54054
54262
|
}
|
|
54055
|
-
for (const written of writeSkill(skillDirectories(already, projectPath,
|
|
54263
|
+
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync27), version)) {
|
|
54056
54264
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
54057
54265
|
}
|
|
54058
54266
|
console.log(`
|
|
54059
54267
|
On ${version}. What is left for you:`);
|
|
54060
54268
|
console.log(digestBefore !== null && digestBefore === installedEditorDigest(projectPath) ? ` 1. Nothing for the editor: this upgrade left the editor addons the same code as before,
|
|
54061
54269
|
` + ` so it gives an open editor nothing to restart for. editor_status says addonIsStale
|
|
54062
|
-
` + " if the editor is
|
|
54270
|
+
` + " if the editor is running other code." : ` 1. The open editor is still running the addons it loaded at startup. Restart it with the
|
|
54063
54271
|
` + " editor_launch restart tool, which closes and reopens the window.");
|
|
54064
54272
|
const serving = await serverServing(projectPath);
|
|
54065
54273
|
console.log(harnessNote({ moved, written, byHand }, [launch.command, ...launch.args].join(" "), serving === null ? null : { server: serving, isThisVersion: serving.version === version }));
|