gdharness 1.3.0 → 1.3.2
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 +322 -96
- package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -19
- package/build/godot/addons/gdharness_editor/bridge_client.gd +8 -8
- package/build/godot/addons/gdharness_editor/declared_values.gd +116 -0
- package/build/godot/addons/gdharness_editor/declared_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +65 -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 +222 -59
- package/build/godot/addons/gdharness_editor/reading.gd +65 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +28 -23
- package/build/godot/addons/gdharness_editor/serialisation.gd +49 -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 +175 -74
- 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 +88 -84
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +4 -4
- package/build/godot/addons/gdharness_editor/value_shapes.gd +118 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +65 -0
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +18 -18
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +29 -29
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +21 -18
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +86 -80
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +65 -64
- 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 +6 -6
- 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 +52 -25
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +56 -50
- package/build/godot/addons/gdharness_runtime/serialisation.gd +49 -94
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +118 -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 +25 -15
- package/build/godot/operations/declared_values.gd +116 -0
- package/build/godot/operations/dependencies.gd +141 -33
- package/build/godot/operations/editor_tools.gd +12 -12
- package/build/godot/operations/export_presets.gd +14 -14
- package/build/godot/operations/file_reads.gd +65 -25
- package/build/godot/operations/file_walk.gd +34 -0
- package/build/godot/operations/gdscript_analysis.gd +27 -22
- package/build/godot/operations/gdscript_authoring.gd +122 -17
- package/build/godot/operations/godot_operations.gd +66 -63
- package/build/godot/operations/import_pipeline.gd +59 -20
- package/build/godot/operations/input_actions.gd +90 -18
- package/build/godot/operations/logger.gd +1 -1
- package/build/godot/operations/plugins.gd +8 -8
- package/build/godot/operations/project_config.gd +83 -20
- package/build/godot/operations/project_diagnostics.gd +36 -11
- package/build/godot/operations/property_values.gd +222 -59
- package/build/godot/operations/reading.gd +65 -0
- package/build/godot/operations/rename_checks.gd +3 -3
- package/build/godot/operations/resource_files.gd +9 -6
- package/build/godot/operations/script_check.gd +23 -7
- package/build/godot/operations/serialisation.gd +49 -94
- package/build/godot/operations/value_shapes.gd +118 -0
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7864,7 +7864,8 @@ function resolveWithinProject(projectPath, candidatePath, options = {}) {
|
|
|
7864
7864
|
if (trimmed.includes("\x00")) {
|
|
7865
7865
|
return { ok: false, reason: "Path contains a null byte." };
|
|
7866
7866
|
}
|
|
7867
|
-
const
|
|
7867
|
+
const root = resolve(projectPath);
|
|
7868
|
+
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : isAbsolute(trimmed) && isWithinRoot(root, trimmed) ? relative(root, resolve(trimmed)).split(sep).join("/") : trimmed;
|
|
7868
7869
|
if (withoutScheme.startsWith("uid://")) {
|
|
7869
7870
|
return {
|
|
7870
7871
|
ok: false,
|
|
@@ -7881,7 +7882,7 @@ function resolveWithinProject(projectPath, candidatePath, options = {}) {
|
|
|
7881
7882
|
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
7882
7883
|
return {
|
|
7883
7884
|
ok: false,
|
|
7884
|
-
reason: `Path '${trimmed}' is absolute; give a path inside
|
|
7885
|
+
reason: `Path '${trimmed}' is absolute and outside the project; give a path inside it, such as 'scenes/main.tscn'.`
|
|
7885
7886
|
};
|
|
7886
7887
|
}
|
|
7887
7888
|
const kept = keptInside(trimmed);
|
|
@@ -7897,7 +7898,6 @@ function resolveWithinProject(projectPath, candidatePath, options = {}) {
|
|
|
7897
7898
|
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
7898
7899
|
};
|
|
7899
7900
|
}
|
|
7900
|
-
const root = resolve(projectPath);
|
|
7901
7901
|
const absolutePath = resolve(root, withoutScheme);
|
|
7902
7902
|
if (!isWithinRoot(root, absolutePath)) {
|
|
7903
7903
|
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
@@ -10678,6 +10678,14 @@ function unloadedTypes(messages, declared, cached) {
|
|
|
10678
10678
|
}
|
|
10679
10679
|
return found;
|
|
10680
10680
|
}
|
|
10681
|
+
function uncachedFor(projectPath, messagesPer) {
|
|
10682
|
+
if (!messagesPer.some((messages) => messages.some((message) => unknownTypeIn(message) !== null))) {
|
|
10683
|
+
return messagesPer.map(() => []);
|
|
10684
|
+
}
|
|
10685
|
+
const declared = declaredClasses(projectPath);
|
|
10686
|
+
const cached = cachedClasses(projectPath);
|
|
10687
|
+
return messagesPer.map((messages) => unloadedTypes(messages, declared, cached).filter((type) => !type.inTheClassCache));
|
|
10688
|
+
}
|
|
10681
10689
|
function classesNamedIn(source, known) {
|
|
10682
10690
|
const named = new Set;
|
|
10683
10691
|
for (const [word] of source.matchAll(/\b[A-Z][A-Za-z0-9_]*\b/g)) {
|
|
@@ -10818,6 +10826,16 @@ function uncachedClassNote(types) {
|
|
|
10818
10826
|
const one = types.length === 1;
|
|
10819
10827
|
return `${types.join(", ")} ${one ? "is declared" : "are declared"} in this project and missing from ` + `.godot/global_script_class_cache.cfg, so a game launched now would not resolve ${one ? "it" : "them"} ` + "either. Try editor_rescan first: it asks the editor already running and starts nothing, and it " + "cleared this in 314ms in a second project with a 31-worker fan-out still importing. It is not " + "always enough, because a file another engine has already imported reads as settled and the " + "walk does not look inside it. project_import refresh_classes rewrites the cache from the " + "declarations on disk and does reach that case, at the cost of a short headless engine, which " + "is worth knowing when something else is already running against this project.";
|
|
10820
10828
|
}
|
|
10829
|
+
function uncachedForTheEngineNote(projectPath, types) {
|
|
10830
|
+
if (types.length === 0) {
|
|
10831
|
+
return "";
|
|
10832
|
+
}
|
|
10833
|
+
const one = types.length === 1;
|
|
10834
|
+
const named = types.map((type) => `${type.type} (${type.declaredIn})`).join(", ");
|
|
10835
|
+
const cacheExists = existsSync11(join12(projectPath, ".godot", "global_script_class_cache.cfg"));
|
|
10836
|
+
const state = cacheExists ? `missing from .godot/global_script_class_cache.cfg, which is where the engine that compiled these scripts resolves a class name from, so the error is the cache being behind the files rather than the script.` : `the engine that compiled these scripts resolves a class name from .godot/global_script_class_cache.cfg, which has not been written yet, as in a project no editor has opened, so the error is the missing cache rather than the script.`;
|
|
10837
|
+
return `${named} ${one ? "is" : "are"} declared in this project${cacheExists ? " and" : ", but"} ${state} ` + "project_import refresh_classes writes the cache from the declarations on disk; editor_run start " + "writes it before a game, and an editor opening the project writes it when it scans.";
|
|
10838
|
+
}
|
|
10821
10839
|
function cacheWrittenAt(projectPath) {
|
|
10822
10840
|
const cache = join12(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
10823
10841
|
try {
|
|
@@ -11046,6 +11064,17 @@ function ignoredTexts(projectPath) {
|
|
|
11046
11064
|
visit(projectPath, "", false);
|
|
11047
11065
|
return found;
|
|
11048
11066
|
}
|
|
11067
|
+
function plainMatch(line, needle, caseSensitive) {
|
|
11068
|
+
if (caseSensitive) {
|
|
11069
|
+
return line.includes(needle) ? [needle] : null;
|
|
11070
|
+
}
|
|
11071
|
+
const lowered = line.toLowerCase();
|
|
11072
|
+
const at = lowered.indexOf(needle);
|
|
11073
|
+
if (at < 0) {
|
|
11074
|
+
return null;
|
|
11075
|
+
}
|
|
11076
|
+
return [lowered.length === line.length ? line.slice(at, at + needle.length) : needle];
|
|
11077
|
+
}
|
|
11049
11078
|
function searchProject(projectPath, options) {
|
|
11050
11079
|
const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
|
|
11051
11080
|
const result = {
|
|
@@ -11083,7 +11112,7 @@ function searchProject(projectPath, options) {
|
|
|
11083
11112
|
result.summary.files_searched += 1;
|
|
11084
11113
|
const matches = [];
|
|
11085
11114
|
for (const [index, line] of bytes.toString("utf8").split(/\r?\n/).entries()) {
|
|
11086
|
-
const found = regex ? regex.exec(line) : (
|
|
11115
|
+
const found = regex ? regex.exec(line) : plainMatch(line, needle, options.caseSensitive);
|
|
11087
11116
|
if (found === null) {
|
|
11088
11117
|
continue;
|
|
11089
11118
|
}
|
|
@@ -11091,7 +11120,7 @@ function searchProject(projectPath, options) {
|
|
|
11091
11120
|
result.summary.truncated = true;
|
|
11092
11121
|
break;
|
|
11093
11122
|
}
|
|
11094
|
-
matches.push({ line: index + 1, content: line.trim(), match: found[0]
|
|
11123
|
+
matches.push({ line: index + 1, content: line.trim(), match: found[0] });
|
|
11095
11124
|
result.summary.total_matches += 1;
|
|
11096
11125
|
}
|
|
11097
11126
|
if (matches.length > 0) {
|
|
@@ -11879,6 +11908,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11879
11908
|
init_launch();
|
|
11880
11909
|
PROJECT_INFO_SECTIONS = [
|
|
11881
11910
|
"autoloads",
|
|
11911
|
+
"input_actions",
|
|
11882
11912
|
"plugins",
|
|
11883
11913
|
"export_presets",
|
|
11884
11914
|
"audio_buses",
|
|
@@ -11912,7 +11942,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11912
11942
|
};
|
|
11913
11943
|
PROPERTIES = {
|
|
11914
11944
|
type: "object",
|
|
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. 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}`,
|
|
11945
|
+
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. An enum property takes the name of a value, as the script spells it, or its number; a value past an export range, or a number no value of an enum has, is written as given and named under outsideDeclared. ${WHOLE_NUMBERS}`,
|
|
11916
11946
|
additionalProperties: true
|
|
11917
11947
|
};
|
|
11918
11948
|
XY = {
|
|
@@ -12005,7 +12035,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12005
12035
|
};
|
|
12006
12036
|
TILESET_SOURCES = {
|
|
12007
12037
|
type: "array",
|
|
12008
|
-
description: "Atlas sources, one per texture.",
|
|
12038
|
+
description: "Atlas sources, one per texture. A wholly transparent cell gets no tile, and the answer lists those cells as leftBlank; a strip at the right or bottom that no whole tile reaches is described in notInAnyTile. A path that already exists is refused, as create_tileset would replace the whole TileSet.",
|
|
12009
12039
|
items: {
|
|
12010
12040
|
type: "object",
|
|
12011
12041
|
properties: {
|
|
@@ -12025,17 +12055,17 @@ var init_tool_definitions = __esm(() => {
|
|
|
12025
12055
|
type: "object",
|
|
12026
12056
|
properties: {
|
|
12027
12057
|
coords: XY,
|
|
12028
|
-
sourceId: { type: "number", description: "TileSet source
|
|
12029
|
-
atlasCoords: XY,
|
|
12058
|
+
sourceId: { type: "number", description: "TileSet source id, or -1 to erase the cell." },
|
|
12059
|
+
atlasCoords: { ...XY, description: "The tile in the source. Needed by every cell but an erased one." },
|
|
12030
12060
|
alternativeTile: { type: "number" }
|
|
12031
12061
|
},
|
|
12032
|
-
required: ["coords", "sourceId"
|
|
12062
|
+
required: ["coords", "sourceId"],
|
|
12033
12063
|
additionalProperties: false
|
|
12034
12064
|
}
|
|
12035
12065
|
};
|
|
12036
12066
|
COLOUR2 = {
|
|
12037
|
-
type: "object",
|
|
12038
|
-
description:
|
|
12067
|
+
type: ["object", "string"],
|
|
12068
|
+
description: 'set_theme_color: r, g and b from 0 to 1, with a optional and opaque by default, or an html code such as "#ff8000" or "#ff800080".',
|
|
12039
12069
|
properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" }, a: { type: "number" } },
|
|
12040
12070
|
required: ["r", "g", "b"],
|
|
12041
12071
|
additionalProperties: false
|
|
@@ -12051,7 +12081,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12051
12081
|
include: {
|
|
12052
12082
|
type: "array",
|
|
12053
12083
|
items: { type: "string", enum: [...PROJECT_INFO_SECTIONS] },
|
|
12054
|
-
description: "Extra sections: registered autoloads, addons and whether each is enabled, export presets, the audio bus layout, a health report, or export validation."
|
|
12084
|
+
description: "Extra sections: registered autoloads, the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets, the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, and each file a scene, resource or script refers to that does not exist."
|
|
12055
12085
|
},
|
|
12056
12086
|
preset: { type: "string", description: "For validation: the export preset to validate against." },
|
|
12057
12087
|
detail: {
|
|
@@ -12087,7 +12117,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12087
12117
|
value: {
|
|
12088
12118
|
blank: true,
|
|
12089
12119
|
ops: ["set"],
|
|
12090
|
-
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.'
|
|
12120
|
+
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. A setting the engine does not declare is written as given, a whole number as an int, and when one in its section is named like it, note names the nearest, as it may be a misspelt one.'
|
|
12091
12121
|
},
|
|
12092
12122
|
name: { type: "string", ops: ["add_autoload", "remove_autoload"], description: "Autoload name." },
|
|
12093
12123
|
path: {
|
|
@@ -12222,7 +12252,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12222
12252
|
direction: {
|
|
12223
12253
|
type: "string",
|
|
12224
12254
|
enum: ["forward", "reverse"],
|
|
12225
|
-
description: "forward: what this resource loads, with cycles reported, a script's dependencies including every script whose class_name it uses, marked by_class with the name. summary.total_dependencies counts each file once however many places load it,
|
|
12255
|
+
description: "forward: what this resource loads, with cycles reported, a script's dependencies including every script whose class_name it uses, marked by_class with the name. summary.total_dependencies counts each file once however many places load it, total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well, answered with exists: false, a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Each reference carries a kind: ext_resource, extends, preload, load or path for a reference by path; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about. Two kinds are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
|
|
12226
12256
|
},
|
|
12227
12257
|
depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
|
|
12228
12258
|
includeBuiltin: {
|
|
@@ -12346,7 +12376,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12346
12376
|
name: "project_test",
|
|
12347
12377
|
title: "Run tests",
|
|
12348
12378
|
effect: "additive",
|
|
12349
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. A run that found nothing to run is never called a pass
|
|
12379
|
+
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
12380
|
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,
|
|
12351
12381
|
parameters: {
|
|
12352
12382
|
projectPath: PROJECT_PATH,
|
|
@@ -12360,6 +12390,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
12360
12390
|
items: { type: "string" },
|
|
12361
12391
|
description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name". A bare test name is read as a suite name and leaves out nothing; an entry that named a case which ran all the same comes back under ignoreNotApplied with the form that would have left it out.'
|
|
12362
12392
|
},
|
|
12393
|
+
tests: {
|
|
12394
|
+
type: "array",
|
|
12395
|
+
items: { type: "string" },
|
|
12396
|
+
description: 'Run only these cases of the suites under path, each "test_name" or "suite_name:test_name", the suite by its file name without .gd or its res:// path. Only the suites holding them run, with their other cases left out. A name no suite there declares as a top-level func is refused before the engine starts, with the nearest names; a case asked for that gdUnit4 did not report comes back under testsNotRun, and the run is not called passed.'
|
|
12397
|
+
},
|
|
12363
12398
|
engineArgs: {
|
|
12364
12399
|
type: "array",
|
|
12365
12400
|
items: { type: "string" },
|
|
@@ -12437,7 +12472,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12437
12472
|
{
|
|
12438
12473
|
name: "scene_node",
|
|
12439
12474
|
title: "Scene nodes",
|
|
12440
|
-
description: `One node in a scene file: add, read, set, duplicate, reparent or delete it, or paint TileMap cells. Any ClassDB node type or script class can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties, and another scene is instanced by an add with instancePath. A set with properties {"name": "Boss"} renames a node, and answers nodePath as its new path. A rename or a reparent carries the node paths naming the node, as the editor's rename does: NodePath values on nodes (in arrays, dictionaries, built-in resources, %unique paths) and the tracks its players' animations hold, saving a library kept in a .tres of its own. Each rewrite is listed under retargeted ({scenePath, nodePath, property or animation and track, from, to}), and under notRetargeted, with a reason, each left: a track kept in another scene or an imported file, or a value another scene gives that the move left naming nothing. The move is followed into every other .tscn that instances or inherits this scene at any depth: the editable children, values, added nodes and connections named by the old path are rewritten, since Godot finds a moved node's values by unique_id but editable children only by path and drops the values kept below them on the next save, and that scene's node paths and tracks are carried as above. A reparent also drops the node ids saved along each rewritten path, which ran through the old parent; the next save writes them again. The scenes rewritten are named under followed, and under notFollowed any left as they were, with why. A delete rewrites no path and lists under stillNaming each node path and track, here and in the scenes instancing or inheriting this one, that named the deleted node and now names nothing. A property holding a Resource takes the res:// path of one, so a texture, a material or a theme is a set like any other. A write is refused while the editor holds unsaved changes to the scene. A node this scene inherits takes values of this scene's own, and so does a node inside an instance of another scene once that instance's editableChildren is true; neither can be moved, renamed, copied or deleted, since the other scene makes it again on load. ${WITHOUT_AN_EDITOR}`,
|
|
12475
|
+
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}`,
|
|
12441
12476
|
parameters: {
|
|
12442
12477
|
projectPath: PROJECT_PATH,
|
|
12443
12478
|
scenePath: SCENE_PATH,
|
|
@@ -12672,7 +12707,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12672
12707
|
{
|
|
12673
12708
|
name: "script_edit",
|
|
12674
12709
|
title: "Edit scripts",
|
|
12675
|
-
description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read, and a script declaring a class_name is put in the project's class list, with registered saying whether it is there. modify makes every addition or none, and answers with the line each one is on in the file written; with an editor serving the project it reloads the script there too, so the editor's answers read what was written, and editorReloaded says so (editorNote when it could not, or when a script tab holds unsaved edits that saving would write over the file). rename renames a class or a member wherever the engine uses it and writes every file or none. A new name already taken is refused with nothing written, a member's included when a class inheriting it already uses the name in code, as a local or a parameter. changed lists each file with its changed lines as they now read, and leftAlone lists every other whole-word occurrence of the old name with what the tokenizer makes of it (code, string, comment, doc, nodePath, scene, text, or ignored for a file under a .gdignore directory, which is never changed) and, where it is not plain, why: a string that is the old name whole, such as find_children's type or a call's method name, which nothing renames or compiles and which is listed first; a node named like the class; a call on a value whose type the analyser could not tell. A member rename answers under languageServer how many requests it made and how long they took. A comment, documentation line, string or text file line with nothing more to say is given under leftAloneInProse by file, kind and line instead, and code that is certainly another symbol of the same name, a bare name in a file where nothing inherits the member or one the language server resolved to another declaration, under leftAloneOtherSymbols as each why with the files and lines it covers. summary.leftAlone counts each of the three lists beside the total. When those two grouped lists run past 300 lines, they are written with each line's text to a file under the project's .godot directory instead, of which the newest ten are kept, and leftAloneDetail names the file and counts what is in it. Read both before calling the rename done; nothing in them was changed. Afterwards an open editor scans the changed files and reloads every changed script, which is also the compile check, and its answer comes back under editor; with no editor the class cache is rebuilt and the changed scripts are compiled in a fresh engine, under classCache and compiled. written is true once the files are written, and ok is false when what came after the write failed: the files are left changed, and note says so.",
|
|
12710
|
+
description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read, and a script declaring a class_name is put in the project's class list, with registered saying whether it is there; a class_name another script or an engine class holds, or an extends naming no class, is refused with nothing written. modify makes every addition or none, refuses with nothing changed additions that would stop a script that parses from parsing, giving the engine's reasons, and answers with the line each one is on in the file written; with an editor serving the project it reloads the script there too, so the editor's answers read what was written, and editorReloaded says so (editorNote when it could not, or when a script tab holds unsaved edits that saving would write over the file). rename renames a class or a member wherever the engine uses it and writes every file or none. A new name already taken is refused with nothing written, a member's included when a class inheriting it already uses the name in code, as a local or a parameter. changed lists each file with its changed lines as they now read, and leftAlone lists every other whole-word occurrence of the old name with what the tokenizer makes of it (code, string, comment, doc, nodePath, scene, text, or ignored for a file under a .gdignore directory, which is never changed) and, where it is not plain, why: a string that is the old name whole, such as find_children's type or a call's method name, which nothing renames or compiles and which is listed first; a node named like the class; a call on a value whose type the analyser could not tell. A member rename answers under languageServer how many requests it made and how long they took. A comment, documentation line, string or text file line with nothing more to say is given under leftAloneInProse by file, kind and line instead, and code that is certainly another symbol of the same name, a bare name in a file where nothing inherits the member or one the language server resolved to another declaration, under leftAloneOtherSymbols as each why with the files and lines it covers. summary.leftAlone counts each of the three lists beside the total. When those two grouped lists run past 300 lines, they are written with each line's text to a file under the project's .godot directory instead, of which the newest ten are kept, and leftAloneDetail names the file and counts what is in it. Read both before calling the rename done; nothing in them was changed. Afterwards an open editor scans the changed files and reloads every changed script, which is also the compile check, and its answer comes back under editor; with no editor the class cache is rebuilt and the changed scripts are compiled in a fresh engine, under classCache and compiled. written is true once the files are written, and ok is false when what came after the write failed: the files are left changed, and note says so.",
|
|
12676
12711
|
parameters: {
|
|
12677
12712
|
projectPath: PROJECT_PATH,
|
|
12678
12713
|
scriptPath: SCRIPT_PATH,
|
|
@@ -12755,7 +12790,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12755
12790
|
operations: {
|
|
12756
12791
|
structure: {
|
|
12757
12792
|
effect: "read",
|
|
12758
|
-
summary: "functions, variables, signals, class_name and
|
|
12793
|
+
summary: "functions, variables, signals, constants, enums, inner_classes, class_name, extends and line_count, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class; autoloads_used names each autoload the code uses by its global name, with the autoload's script, which the project loads rather than this script, so it is not under dependencies",
|
|
12759
12794
|
requires: []
|
|
12760
12795
|
},
|
|
12761
12796
|
symbols: {
|
|
@@ -12776,7 +12811,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12776
12811
|
name: "script_diagnostics",
|
|
12777
12812
|
title: "Script diagnostics",
|
|
12778
12813
|
effect: "read",
|
|
12779
|
-
description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. With no editor of this project open, so no language server of it to ask, a short headless engine compiles the scripts instead and the answer says checkedBy: engine: every error that stops a script compiling, with its line and no column, dependenciesWithErrors naming the other scripts that failed while it loaded, and warnings null, since the engine reports warnings only to an editor. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, or a project class it extends does, a method, property, constant, static function or enum member, comes back under contradictedByTheFile with the script declaring it, and so does one a diagnostic names alone as an external class member, read against the class it is looked up on in the line it is on; and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
12814
|
+
description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. With no editor of this project open, so no language server of it to ask, a short headless engine compiles the scripts instead and the answer says checkedBy: engine: every error that stops a script compiling, with its line and no column, dependenciesWithErrors naming the other scripts that failed while it loaded, typesMissingFromTheClassCache naming a class the project declares that the engine could not resolve because the class cache lacks it, with staleAnalysis saying what rewrites the cache, and warnings null, since the engine reports warnings only to an editor. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, or a project class it extends does, a method, property, constant, static function or enum member, comes back under contradictedByTheFile with the script declaring it, and so does one a diagnostic names alone as an external class member, read against the class it is looked up on in the line it is on; and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
12780
12815
|
parameters: {
|
|
12781
12816
|
projectPath: PROJECT_PATH,
|
|
12782
12817
|
scriptPath: {
|
|
@@ -12790,7 +12825,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12790
12825
|
{
|
|
12791
12826
|
name: "resource_edit",
|
|
12792
12827
|
title: "Edit resources",
|
|
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". 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.',
|
|
12828
|
+
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, with resourceType the engine class and scriptClass the class_name of its script when it declares one, 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. A property holding a resource takes its res:// path; a file never imported is refused with the reimport that imports it. 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.',
|
|
12794
12829
|
parameters: {
|
|
12795
12830
|
projectPath: PROJECT_PATH,
|
|
12796
12831
|
resourcePath: {
|
|
@@ -12925,7 +12960,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12925
12960
|
{
|
|
12926
12961
|
name: "editor_run",
|
|
12927
12962
|
title: "Run the game",
|
|
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. 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.",
|
|
12963
|
+
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.",
|
|
12929
12964
|
parameters: {
|
|
12930
12965
|
projectPath: {
|
|
12931
12966
|
...PROJECT_PATH,
|
|
@@ -13065,13 +13100,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
13065
13100
|
requires: []
|
|
13066
13101
|
}
|
|
13067
13102
|
},
|
|
13068
|
-
answer: "start: through is gdharness for a game this server started itself, and editor for one the editor plays. scene is the scene the game started in, the project's main scene when none was given. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. start and check: " + IMPORTED_FIRST + " An exported build carries its own imports and is not asked. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
|
|
13103
|
+
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.",
|
|
13069
13104
|
defaultOperation: "start"
|
|
13070
13105
|
},
|
|
13071
13106
|
{
|
|
13072
13107
|
name: "editor_output",
|
|
13073
13108
|
title: "Game and editor output",
|
|
13074
13109
|
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.",
|
|
13110
|
+
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.",
|
|
13075
13111
|
parameters: {
|
|
13076
13112
|
projectPath: {
|
|
13077
13113
|
type: "string",
|
|
@@ -13349,7 +13385,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13349
13385
|
value: {
|
|
13350
13386
|
ops: ["set"],
|
|
13351
13387
|
blank: true,
|
|
13352
|
-
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.`
|
|
13388
|
+
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.`
|
|
13353
13389
|
},
|
|
13354
13390
|
method: {
|
|
13355
13391
|
type: "string",
|
|
@@ -13402,7 +13438,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13402
13438
|
{
|
|
13403
13439
|
name: "runtime_capture",
|
|
13404
13440
|
title: "Capture the screen",
|
|
13405
|
-
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
|
|
13441
|
+
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.",
|
|
13406
13442
|
parameters: {
|
|
13407
13443
|
projectPath: RUNNING_PROJECT_PATH,
|
|
13408
13444
|
pid: RUNNING_PID,
|
|
@@ -13715,7 +13751,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13715
13751
|
{
|
|
13716
13752
|
name: "debug_breakpoint",
|
|
13717
13753
|
title: "Breakpoints",
|
|
13718
|
-
description: "Sets or removes a breakpoint. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it at once (runningGame says so). The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor where it was turned off again is said so under breakpointsAtRisk, here and in editor_status.",
|
|
13754
|
+
description: "Sets or removes a breakpoint. A set on a script that is not there, or past its last line, is refused, as nothing would ever stop there; a remove takes any. 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.",
|
|
13719
13755
|
parameters: {
|
|
13720
13756
|
projectPath: PROJECT_PATH,
|
|
13721
13757
|
scriptPath: SCRIPT_PATH,
|
|
@@ -39274,7 +39310,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
39274
39310
|
try {
|
|
39275
39311
|
scriptPath = await realpath(contained.absolutePath);
|
|
39276
39312
|
} catch {
|
|
39277
|
-
throw new ArgumentRefusal(`Script file does not exist:
|
|
39313
|
+
throw new ArgumentRefusal(`Script file does not exist: res://${contained.relativePath}`);
|
|
39278
39314
|
}
|
|
39279
39315
|
if (!isWithinRoot(projectPath, scriptPath)) {
|
|
39280
39316
|
throw new ArgumentRefusal("scriptPath resolves outside the project root boundary.");
|
|
@@ -39360,7 +39396,7 @@ var init_lsp_client = __esm(() => {
|
|
|
39360
39396
|
AnotherProjectsServer = class AnotherProjectsServer extends Refusal {
|
|
39361
39397
|
serves;
|
|
39362
39398
|
constructor(port, serves, asked) {
|
|
39363
|
-
super(`The language server on port ${port} belongs to the editor of ${serves}
|
|
39399
|
+
super(`The language server on port ${port} belongs to the editor of ${serves}; no editor of ${asked} answers there.`);
|
|
39364
39400
|
this.serves = serves;
|
|
39365
39401
|
}
|
|
39366
39402
|
};
|
|
@@ -44088,6 +44124,46 @@ var init_lifecycle_log = __esm(() => {
|
|
|
44088
44124
|
init_server_version();
|
|
44089
44125
|
});
|
|
44090
44126
|
|
|
44127
|
+
// src/nearest.ts
|
|
44128
|
+
function similarity(one, other) {
|
|
44129
|
+
if (one === other) {
|
|
44130
|
+
return 1;
|
|
44131
|
+
}
|
|
44132
|
+
if (one.length < 2 || other.length < 2) {
|
|
44133
|
+
return 0;
|
|
44134
|
+
}
|
|
44135
|
+
const pairs = (text) => Array.from({ length: text.length - 1 }, (_, at) => text.slice(at, at + 2));
|
|
44136
|
+
const ones = pairs(one);
|
|
44137
|
+
const others = pairs(other);
|
|
44138
|
+
const shared = ones.filter((pair) => others.includes(pair)).length;
|
|
44139
|
+
return 2 * shared / (ones.length + others.length);
|
|
44140
|
+
}
|
|
44141
|
+
function withNearestFiles(said, wanted, files) {
|
|
44142
|
+
const extension = /\.([^./]+)$/.exec(wanted)?.[1];
|
|
44143
|
+
if (extension === undefined) {
|
|
44144
|
+
return said;
|
|
44145
|
+
}
|
|
44146
|
+
if (files.length === 0) {
|
|
44147
|
+
return `${said}. The project holds no .${extension} file.`;
|
|
44148
|
+
}
|
|
44149
|
+
const stem = (path) => (path.split("/").pop() ?? path).replace(/\.[^.]*$/, "");
|
|
44150
|
+
const close = nearest(stem(wanted), files.map((file) => stem(file)));
|
|
44151
|
+
const offered = files.filter((file) => close.includes(stem(file)));
|
|
44152
|
+
if (offered.length > 0) {
|
|
44153
|
+
return `${said}. The nearest .${extension} ${offered.length === 1 ? "file is" : "files are"} ${offered.join(", ")}.`;
|
|
44154
|
+
}
|
|
44155
|
+
if (files.length <= LISTED_FILES) {
|
|
44156
|
+
return `${said}. The project's .${extension} files are ${files.join(", ")}.`;
|
|
44157
|
+
}
|
|
44158
|
+
return `${said}. None of the project's ${files.length} .${extension} files is named like it.`;
|
|
44159
|
+
}
|
|
44160
|
+
function nearest(word, names) {
|
|
44161
|
+
const wanted = word.toLowerCase();
|
|
44162
|
+
const score = (name) => similarity(name.toLowerCase(), wanted);
|
|
44163
|
+
return names.filter((name) => score(name) >= 0.5).sort((one, other) => score(other) - score(one)).slice(0, 3);
|
|
44164
|
+
}
|
|
44165
|
+
var LISTED_FILES = 12;
|
|
44166
|
+
|
|
44091
44167
|
// src/outside.ts
|
|
44092
44168
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
44093
44169
|
function changesFrom(env, base) {
|
|
@@ -45021,6 +45097,70 @@ var init_server_instructions = __esm(() => {
|
|
|
45021
45097
|
init_tool_definitions();
|
|
45022
45098
|
});
|
|
45023
45099
|
|
|
45100
|
+
// src/test-selection.ts
|
|
45101
|
+
import { existsSync as existsSync26, readFileSync as readFileSync28 } from "node:fs";
|
|
45102
|
+
import { basename as basename10, join as join33 } from "node:path";
|
|
45103
|
+
function casesIn(projectPath, suite) {
|
|
45104
|
+
const text = readFileSync28(join33(projectPath, suite.slice("res://".length)), "utf8");
|
|
45105
|
+
return [...text.matchAll(/^func\s+(test_\w*)\s*\(/gm)].map((match) => match[1] ?? "");
|
|
45106
|
+
}
|
|
45107
|
+
function suitesUnder(projectPath, asked) {
|
|
45108
|
+
const scripts = projectFiles(projectPath, [".gd"]).map((path) => `res://${path}`);
|
|
45109
|
+
return [
|
|
45110
|
+
...new Set(asked.flatMap((path) => path.endsWith(".gd") ? existsSync26(join33(projectPath, path.slice("res://".length))) ? [path] : [] : scripts.filter((script) => script.startsWith(`${path}/`))))
|
|
45111
|
+
];
|
|
45112
|
+
}
|
|
45113
|
+
function selectTests(projectPath, asked, given) {
|
|
45114
|
+
if (given === undefined || given === null) {
|
|
45115
|
+
return { ok: true, value: null };
|
|
45116
|
+
}
|
|
45117
|
+
const entries = Array.isArray(given) ? given : [given];
|
|
45118
|
+
if (entries.length === 0) {
|
|
45119
|
+
return { ok: false, reason: "tests is an empty list: name a case, or leave tests out to run every one." };
|
|
45120
|
+
}
|
|
45121
|
+
const declared = suitesUnder(projectPath, asked).flatMap((suite) => casesIn(projectPath, suite).map((test) => ({ suite, test })));
|
|
45122
|
+
const nameOf = (suite) => basename10(suite, ".gd");
|
|
45123
|
+
const selected = [];
|
|
45124
|
+
for (const [index, entry] of entries.entries()) {
|
|
45125
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
45126
|
+
return {
|
|
45127
|
+
ok: false,
|
|
45128
|
+
reason: `tests[${index}] is not a case name: give "test_name" or "suite:test_name".`
|
|
45129
|
+
};
|
|
45130
|
+
}
|
|
45131
|
+
const spelled = entry.trim();
|
|
45132
|
+
const at = spelled.lastIndexOf(":");
|
|
45133
|
+
const suiteNamed = at === -1 || spelled.slice(at).startsWith("://") ? null : spelled.slice(0, at);
|
|
45134
|
+
const test = suiteNamed === null ? spelled : spelled.slice(at + 1);
|
|
45135
|
+
const suitePath = suiteNamed?.startsWith("res://") === true ? suiteNamed : null;
|
|
45136
|
+
const matches = declared.filter((one) => one.test === test && (suiteNamed === null || one.suite === suitePath || nameOf(one.suite) === suiteNamed));
|
|
45137
|
+
if (matches.length === 0) {
|
|
45138
|
+
const said = `No suite under ${asked.join(", ")} has a case ${spelled}`;
|
|
45139
|
+
if (declared.length === 0) {
|
|
45140
|
+
return { ok: false, reason: `${said}: none of them declares a func test_ at the top level.` };
|
|
45141
|
+
}
|
|
45142
|
+
const written = declared.map((one) => `${nameOf(one.suite)}:${one.test}`);
|
|
45143
|
+
const close = nearest(suiteNamed === null ? test : `${nameOf(suiteNamed)}:${test}`, [
|
|
45144
|
+
...new Set(suiteNamed === null ? declared.map((one) => one.test) : written)
|
|
45145
|
+
]);
|
|
45146
|
+
return {
|
|
45147
|
+
ok: false,
|
|
45148
|
+
reason: close.length > 0 ? `${said}. The nearest ${close.length === 1 ? "is" : "are"} ${close.join(", ")}.` : `${said}. The cases there are ${written.slice(0, 12).join(", ")}${written.length > 12 ? `, and ${written.length - 12} more` : ""}.`
|
|
45149
|
+
};
|
|
45150
|
+
}
|
|
45151
|
+
for (const one of matches) {
|
|
45152
|
+
selected.push({ entry: spelled, ...one });
|
|
45153
|
+
}
|
|
45154
|
+
}
|
|
45155
|
+
const suites = [...new Set(selected.map((one) => one.suite))];
|
|
45156
|
+
const chosen = new Set(selected.map((one) => `${one.suite}:${one.test}`));
|
|
45157
|
+
const ignore = declared.filter((one) => suites.includes(one.suite) && !chosen.has(`${one.suite}:${one.test}`)).map((one) => `${one.suite}:${one.test}`);
|
|
45158
|
+
return { ok: true, value: { suites, ignore, selected } };
|
|
45159
|
+
}
|
|
45160
|
+
var init_test_selection = __esm(() => {
|
|
45161
|
+
init_project_scan();
|
|
45162
|
+
});
|
|
45163
|
+
|
|
45024
45164
|
// src/server.ts
|
|
45025
45165
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
45026
45166
|
import {
|
|
@@ -45028,18 +45168,18 @@ import {
|
|
|
45028
45168
|
closeSync as closeSync3,
|
|
45029
45169
|
constants,
|
|
45030
45170
|
copyFileSync,
|
|
45031
|
-
existsSync as
|
|
45171
|
+
existsSync as existsSync27,
|
|
45032
45172
|
fstatSync,
|
|
45033
45173
|
mkdirSync as mkdirSync17,
|
|
45034
45174
|
openSync as openSync4,
|
|
45035
45175
|
readdirSync as readdirSync15,
|
|
45036
|
-
readFileSync as
|
|
45176
|
+
readFileSync as readFileSync29,
|
|
45037
45177
|
readSync as readSync2,
|
|
45038
45178
|
rmdirSync as rmdirSync2,
|
|
45039
45179
|
statSync as statSync15,
|
|
45040
45180
|
writeFileSync as writeFileSync18
|
|
45041
45181
|
} from "node:fs";
|
|
45042
|
-
import { basename as
|
|
45182
|
+
import { basename as basename11, dirname as dirname23, extname as extname4, isAbsolute as isAbsolute5, join as join34, normalize as normalize2, relative as relative6 } from "node:path";
|
|
45043
45183
|
import { setTimeout as delay5 } from "node:timers/promises";
|
|
45044
45184
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
45045
45185
|
function editorRestartTimeoutMs() {
|
|
@@ -45076,15 +45216,15 @@ function writtenFileRefusal(path, rule) {
|
|
|
45076
45216
|
return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
|
|
45077
45217
|
}
|
|
45078
45218
|
const directory = realPathOr(dirname23(path));
|
|
45079
|
-
if (!
|
|
45219
|
+
if (!existsSync27(directory)) {
|
|
45080
45220
|
return `${named}'s directory ${dirname23(path)} does not exist; ${what} creates no directories.`;
|
|
45081
45221
|
}
|
|
45082
|
-
const where =
|
|
45222
|
+
const where = join34(directory, basename11(path));
|
|
45083
45223
|
const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
45084
45224
|
if (inside !== undefined) {
|
|
45085
45225
|
return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
|
|
45086
45226
|
}
|
|
45087
|
-
const taken = rule.writes(where).find((file) =>
|
|
45227
|
+
const taken = rule.writes(where).find((file) => existsSync27(file));
|
|
45088
45228
|
if (taken !== undefined) {
|
|
45089
45229
|
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.`;
|
|
45090
45230
|
}
|
|
@@ -45468,6 +45608,16 @@ function endedToStartThis(ended) {
|
|
|
45468
45608
|
}
|
|
45469
45609
|
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
45470
45610
|
}
|
|
45611
|
+
function lineBeyondScript(absolutePath, relativePath, line) {
|
|
45612
|
+
const text = readFileSync29(absolutePath, "utf8");
|
|
45613
|
+
const lines = text === "" ? 0 : text.split(`
|
|
45614
|
+
`).length - (text.endsWith(`
|
|
45615
|
+
`) ? 1 : 0);
|
|
45616
|
+
if (line <= lines) {
|
|
45617
|
+
return null;
|
|
45618
|
+
}
|
|
45619
|
+
return `res://${relativePath} has ${lines} line${lines === 1 ? "" : "s"}, so there is no line ${line} to stop on.`;
|
|
45620
|
+
}
|
|
45471
45621
|
function runtimeVerdict(endpoint, after) {
|
|
45472
45622
|
if (endpoint !== null) {
|
|
45473
45623
|
return { listening: true, pid: endpoint.pid, port: endpoint.port };
|
|
@@ -45637,7 +45787,7 @@ function notRunNote(notRun, failFast) {
|
|
|
45637
45787
|
function exportPresetRefusal(projectPath, preset) {
|
|
45638
45788
|
let text;
|
|
45639
45789
|
try {
|
|
45640
|
-
text =
|
|
45790
|
+
text = readFileSync29(join34(projectPath, "export_presets.cfg"), "utf8");
|
|
45641
45791
|
} catch {
|
|
45642
45792
|
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.`;
|
|
45643
45793
|
}
|
|
@@ -45703,7 +45853,7 @@ function filesIn(folder) {
|
|
|
45703
45853
|
try {
|
|
45704
45854
|
for (const entry of readdirSync15(folder, { withFileTypes: true })) {
|
|
45705
45855
|
if (entry.isFile()) {
|
|
45706
|
-
found.set(entry.name, statSync15(
|
|
45856
|
+
found.set(entry.name, statSync15(join34(folder, entry.name)).mtimeMs);
|
|
45707
45857
|
}
|
|
45708
45858
|
}
|
|
45709
45859
|
} catch {}
|
|
@@ -45735,7 +45885,7 @@ function unsavedInEditorNote(scripts) {
|
|
|
45735
45885
|
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.`;
|
|
45736
45886
|
}
|
|
45737
45887
|
function editorAddonMissing(projectPath) {
|
|
45738
|
-
if (!
|
|
45888
|
+
if (!existsSync27(join34(projectPath, "project.godot"))) {
|
|
45739
45889
|
return null;
|
|
45740
45890
|
}
|
|
45741
45891
|
const report = inspectProject(projectPath);
|
|
@@ -46000,7 +46150,7 @@ function profileFrameFunctions(projectPath) {
|
|
|
46000
46150
|
let projectGodot = null;
|
|
46001
46151
|
if (projectPath !== null) {
|
|
46002
46152
|
try {
|
|
46003
|
-
projectGodot =
|
|
46153
|
+
projectGodot = readFileSync29(join34(projectPath, "project.godot"), "utf8");
|
|
46004
46154
|
} catch {}
|
|
46005
46155
|
}
|
|
46006
46156
|
return frameFunctionsFor(queuedMessagesOf(projectGodot));
|
|
@@ -46024,7 +46174,7 @@ function projectSourceOf(projectPath) {
|
|
|
46024
46174
|
let source = null;
|
|
46025
46175
|
if (inside.ok) {
|
|
46026
46176
|
try {
|
|
46027
|
-
source =
|
|
46177
|
+
source = readFileSync29(inside.absolutePath, "utf8");
|
|
46028
46178
|
} catch {}
|
|
46029
46179
|
}
|
|
46030
46180
|
read.set(script, source);
|
|
@@ -46179,7 +46329,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
|
|
|
46179
46329
|
}
|
|
46180
46330
|
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
46181
46331
|
try {
|
|
46182
|
-
return settingKeys(
|
|
46332
|
+
return settingKeys(readFileSync29(join34(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
46183
46333
|
} catch {
|
|
46184
46334
|
return true;
|
|
46185
46335
|
}
|
|
@@ -46194,7 +46344,7 @@ function uncaughtNote(uncaught) {
|
|
|
46194
46344
|
}
|
|
46195
46345
|
function elsewhereIn(projectPath, asked) {
|
|
46196
46346
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
46197
|
-
if (wanted === "" ||
|
|
46347
|
+
if (wanted === "" || existsSync27(join34(projectPath, wanted))) {
|
|
46198
46348
|
return "";
|
|
46199
46349
|
}
|
|
46200
46350
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -46297,7 +46447,7 @@ function isDirectory(path) {
|
|
|
46297
46447
|
}
|
|
46298
46448
|
}
|
|
46299
46449
|
function scriptsUnder(directory, prefix) {
|
|
46300
|
-
if (
|
|
46450
|
+
if (existsSync27(join34(directory, ".gdignore"))) {
|
|
46301
46451
|
return [];
|
|
46302
46452
|
}
|
|
46303
46453
|
const found = [];
|
|
@@ -46307,7 +46457,7 @@ function scriptsUnder(directory, prefix) {
|
|
|
46307
46457
|
}
|
|
46308
46458
|
const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
46309
46459
|
if (entry.isDirectory()) {
|
|
46310
|
-
found.push(...scriptsUnder(
|
|
46460
|
+
found.push(...scriptsUnder(join34(directory, entry.name), path));
|
|
46311
46461
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
46312
46462
|
found.push(path);
|
|
46313
46463
|
}
|
|
@@ -46315,11 +46465,11 @@ function scriptsUnder(directory, prefix) {
|
|
|
46315
46465
|
return found;
|
|
46316
46466
|
}
|
|
46317
46467
|
function mainSceneOf(projectFile) {
|
|
46318
|
-
const scene = parseProjectGodot(
|
|
46468
|
+
const scene = parseProjectGodot(readFileSync29(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
46319
46469
|
return typeof scene === "string" && scene !== "" ? scene : null;
|
|
46320
46470
|
}
|
|
46321
46471
|
function editorPlaysHeadless(projectFile) {
|
|
46322
|
-
const runArgs = parseProjectGodot(
|
|
46472
|
+
const runArgs = parseProjectGodot(readFileSync29(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
46323
46473
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
46324
46474
|
}
|
|
46325
46475
|
function ownArguments(params) {
|
|
@@ -46337,7 +46487,7 @@ function needsItsOwnProcess(asked) {
|
|
|
46337
46487
|
class GodotServer {
|
|
46338
46488
|
stdio = null;
|
|
46339
46489
|
locator = new GodotLocator;
|
|
46340
|
-
operationsScript =
|
|
46490
|
+
operationsScript = join34(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
46341
46491
|
godotBridge;
|
|
46342
46492
|
tools = buildToolDefinitions();
|
|
46343
46493
|
activeProcess = null;
|
|
@@ -46599,14 +46749,14 @@ class GodotServer {
|
|
|
46599
46749
|
}
|
|
46600
46750
|
noteTheStart() {
|
|
46601
46751
|
const cwd = process.cwd();
|
|
46602
|
-
const mcpJson =
|
|
46752
|
+
const mcpJson = join34(cwd, ".mcp.json");
|
|
46603
46753
|
const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
|
|
46604
46754
|
const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
|
|
46605
46755
|
writeLifecycle("started", {
|
|
46606
46756
|
argv: process.argv.slice(1),
|
|
46607
46757
|
cwd,
|
|
46608
46758
|
project: this.ownProject,
|
|
46609
|
-
mcpJson:
|
|
46759
|
+
mcpJson: existsSync27(mcpJson) ? mcpJson : null
|
|
46610
46760
|
});
|
|
46611
46761
|
if (predecessor !== null) {
|
|
46612
46762
|
writeLifecycle("replaced", { project: this.ownProject, predecessor });
|
|
@@ -46747,7 +46897,7 @@ class GodotServer {
|
|
|
46747
46897
|
return checked.response;
|
|
46748
46898
|
}
|
|
46749
46899
|
const named = args["projectPath"];
|
|
46750
|
-
if (typeof named === "string" &&
|
|
46900
|
+
if (typeof named === "string" && existsSync27(join34(named, "project.godot"))) {
|
|
46751
46901
|
this.lastProjectPath = named;
|
|
46752
46902
|
}
|
|
46753
46903
|
this.updates.refresh();
|
|
@@ -46783,7 +46933,7 @@ class GodotServer {
|
|
|
46783
46933
|
let notSaved;
|
|
46784
46934
|
try {
|
|
46785
46935
|
savedAt = answer.content.map((item, index) => {
|
|
46786
|
-
const file =
|
|
46936
|
+
const file = join34(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
|
|
46787
46937
|
writeFileSync18(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
|
|
46788
46938
|
return file;
|
|
46789
46939
|
});
|
|
@@ -47010,7 +47160,10 @@ class GodotServer {
|
|
|
47010
47160
|
});
|
|
47011
47161
|
if (missing.length > 0) {
|
|
47012
47162
|
const where = op !== null ? `${spec.name} ${op}` : spec.name;
|
|
47013
|
-
|
|
47163
|
+
const blank = missing.filter((field) => typeof args[field] === "string");
|
|
47164
|
+
const named = missing.map((field) => blank.includes(field) ? `${field}, which was given empty` : field);
|
|
47165
|
+
const joined = named.join(blank.length > 0 ? "; " : ", ");
|
|
47166
|
+
return { ok: false, response: this.createErrorResponse(`${where} needs ${joined}.`) };
|
|
47014
47167
|
}
|
|
47015
47168
|
return { ok: true, op };
|
|
47016
47169
|
}
|
|
@@ -47318,6 +47471,15 @@ class GodotServer {
|
|
|
47318
47471
|
if (line === undefined) {
|
|
47319
47472
|
return this.createErrorResponse("line is required: the one-based line to stop on.");
|
|
47320
47473
|
}
|
|
47474
|
+
if (op === "set") {
|
|
47475
|
+
if (!existsSync27(located.absolutePath)) {
|
|
47476
|
+
return this.missingScript(project.value.path, located.relativePath);
|
|
47477
|
+
}
|
|
47478
|
+
const beyond = lineBeyondScript(located.absolutePath, located.relativePath, line);
|
|
47479
|
+
if (beyond !== null) {
|
|
47480
|
+
return this.createErrorResponse(beyond);
|
|
47481
|
+
}
|
|
47482
|
+
}
|
|
47321
47483
|
const editor = this.godotBridge.isConnected();
|
|
47322
47484
|
let answer;
|
|
47323
47485
|
if (editor) {
|
|
@@ -47400,8 +47562,8 @@ class GodotServer {
|
|
|
47400
47562
|
if (path === undefined) {
|
|
47401
47563
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
47402
47564
|
}
|
|
47403
|
-
const file =
|
|
47404
|
-
if (!
|
|
47565
|
+
const file = join34(path, "project.godot");
|
|
47566
|
+
if (!existsSync27(file)) {
|
|
47405
47567
|
return {
|
|
47406
47568
|
ok: false,
|
|
47407
47569
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -48101,27 +48263,51 @@ class GodotServer {
|
|
|
48101
48263
|
if (!engine.ok) {
|
|
48102
48264
|
return engine.response;
|
|
48103
48265
|
}
|
|
48104
|
-
const application = parseProjectGodot(
|
|
48266
|
+
const application = parseProjectGodot(readFileSync29(project.value.file, "utf8"))["application"] ?? {};
|
|
48105
48267
|
const name = application["config/name"];
|
|
48106
48268
|
const mainScene = application["run/main_scene"];
|
|
48107
48269
|
const info = {
|
|
48108
|
-
name: typeof name === "string" && name !== "" ? name :
|
|
48270
|
+
name: typeof name === "string" && name !== "" ? name : basename11(project.value.path),
|
|
48109
48271
|
path: project.value.path,
|
|
48110
48272
|
mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
|
|
48111
48273
|
godotVersion: await this.godotVersion(engine.value),
|
|
48112
48274
|
structure: projectStructure(project.value.path)
|
|
48113
48275
|
};
|
|
48114
48276
|
const detailed = args["detail"] === "full";
|
|
48277
|
+
const compiledScripts = include.includes("health") || include.includes("validation") ? await this.compiledForProjectInfo(project.value.path) : undefined;
|
|
48115
48278
|
for (const section of include) {
|
|
48116
48279
|
const spec = PROJECT_INFO_SECTIONS2[section];
|
|
48117
48280
|
if (spec === undefined) {
|
|
48118
48281
|
continue;
|
|
48119
48282
|
}
|
|
48120
|
-
const
|
|
48283
|
+
const params = {
|
|
48284
|
+
...spec.params(args, detailed),
|
|
48285
|
+
...compiledScripts !== undefined && (section === "health" || section === "validation") ? { compiled: compiledScripts } : {}
|
|
48286
|
+
};
|
|
48287
|
+
const outcome = await this.operation(spec.operation, params, project.value.path);
|
|
48121
48288
|
info[section] = outcome.ok ? { ...outcome.payload, ...engineExtras(outcome) } : { error: outcome.message, ...engineExtras(outcome) };
|
|
48122
48289
|
}
|
|
48123
48290
|
return this.jsonTextResponse(info);
|
|
48124
48291
|
}
|
|
48292
|
+
async compiledForProjectInfo(projectPath) {
|
|
48293
|
+
const scripts = projectFiles(projectPath, [".gd"]).map((script) => `res://${script}`);
|
|
48294
|
+
if (scripts.length === 0) {
|
|
48295
|
+
return { checked: 0, failures: [] };
|
|
48296
|
+
}
|
|
48297
|
+
const checked = await this.compileScripts(projectPath, scripts);
|
|
48298
|
+
if (!checked.ok) {
|
|
48299
|
+
return { not_checked: `no headless engine could compile the scripts: ${checked.message}` };
|
|
48300
|
+
}
|
|
48301
|
+
const failures = checked.payload.scripts.map(asParams);
|
|
48302
|
+
const uncached = uncachedFor(projectPath, failures.map((failure) => (readArray(failure, "errors") ?? []).map((entry) => String(asParams(entry)["message"]))));
|
|
48303
|
+
return {
|
|
48304
|
+
checked: checked.payload.checked.length,
|
|
48305
|
+
failures: failures.map((failure, at) => {
|
|
48306
|
+
const types = uncached[at] ?? [];
|
|
48307
|
+
return types.length === 0 ? failure : { ...failure, note: uncachedForTheEngineNote(projectPath, types) };
|
|
48308
|
+
})
|
|
48309
|
+
};
|
|
48310
|
+
}
|
|
48125
48311
|
fileTypes(args) {
|
|
48126
48312
|
const named = readStringArray(args, "fileTypes");
|
|
48127
48313
|
return named === undefined ? {} : { fileTypes: named };
|
|
@@ -48199,7 +48385,7 @@ class GodotServer {
|
|
|
48199
48385
|
const exportArgs = [
|
|
48200
48386
|
"--headless",
|
|
48201
48387
|
"--log-file",
|
|
48202
|
-
|
|
48388
|
+
join34(exportLogs, "engine.log"),
|
|
48203
48389
|
"--path",
|
|
48204
48390
|
project.value.path,
|
|
48205
48391
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -48207,7 +48393,7 @@ class GodotServer {
|
|
|
48207
48393
|
output.absolutePath
|
|
48208
48394
|
];
|
|
48209
48395
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
48210
|
-
const writtenAt = () =>
|
|
48396
|
+
const writtenAt = () => existsSync27(output.absolutePath) ? statSync15(output.absolutePath).mtimeMs : null;
|
|
48211
48397
|
const caches = editorCacheDirectory(engine.value);
|
|
48212
48398
|
const exportOnce = async () => {
|
|
48213
48399
|
const before = writtenAt();
|
|
@@ -48236,7 +48422,7 @@ class GodotServer {
|
|
|
48236
48422
|
if (ending.exitSignal === null && ending.exitCode === null) {
|
|
48237
48423
|
return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
|
|
48238
48424
|
}
|
|
48239
|
-
const beside = writtenBeside(listedBefore, filesIn(folder),
|
|
48425
|
+
const beside = writtenBeside(listedBefore, filesIn(folder), basename11(output.absolutePath), dirname23(output.relativePath).replaceAll("\\", "/"));
|
|
48240
48426
|
return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt(), beside, docCacheNote);
|
|
48241
48427
|
}
|
|
48242
48428
|
reportTestProgress(log) {
|
|
@@ -48290,10 +48476,10 @@ class GodotServer {
|
|
|
48290
48476
|
if (movie !== null) {
|
|
48291
48477
|
return this.createErrorResponse(movie);
|
|
48292
48478
|
}
|
|
48293
|
-
const missing = paths.value.filter((path) => !
|
|
48479
|
+
const missing = paths.value.filter((path) => !existsSync27(join34(project.value.path, path.slice("res://".length))));
|
|
48294
48480
|
const asked = paths.value.filter((path) => !missing.includes(path));
|
|
48295
48481
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
48296
|
-
const noRunner = !
|
|
48482
|
+
const noRunner = !existsSync27(join34(project.value.path, runner));
|
|
48297
48483
|
if (asked.length === 0) {
|
|
48298
48484
|
const verdict = `nothing at ${missing.join(" and ")}`;
|
|
48299
48485
|
const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
|
|
@@ -48321,6 +48507,10 @@ class GodotServer {
|
|
|
48321
48507
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
48322
48508
|
]);
|
|
48323
48509
|
}
|
|
48510
|
+
const selection = selectTests(project.value.path, asked, args["tests"]);
|
|
48511
|
+
if (!selection.ok) {
|
|
48512
|
+
return this.createErrorResponse(selection.reason);
|
|
48513
|
+
}
|
|
48324
48514
|
const engine = await this.engine();
|
|
48325
48515
|
if (!engine.ok) {
|
|
48326
48516
|
return engine.response;
|
|
@@ -48351,8 +48541,8 @@ class GodotServer {
|
|
|
48351
48541
|
`res://${runner}`,
|
|
48352
48542
|
"--ignoreHeadlessMode",
|
|
48353
48543
|
...readBoolean(args, "failFast") === true ? [] : ["-c"],
|
|
48354
|
-
...asked.flatMap((path) => ["-a", path]),
|
|
48355
|
-
...ignored.flatMap((entry) => ["-i", entry]),
|
|
48544
|
+
...(selection.value?.suites ?? asked).flatMap((path) => ["-a", path]),
|
|
48545
|
+
...[...ignored, ...selection.value?.ignore ?? []].flatMap((entry) => ["-i", entry]),
|
|
48356
48546
|
"-rd",
|
|
48357
48547
|
reports,
|
|
48358
48548
|
"-rc",
|
|
@@ -48409,14 +48599,14 @@ class GodotServer {
|
|
|
48409
48599
|
await run.settled;
|
|
48410
48600
|
const tookMs = Date.now() - began;
|
|
48411
48601
|
const notCompiled = stopped.for ?? run.log.all.map((entry) => autoloadNotCompiled(entry, autoloaded)).find((found) => found !== null) ?? null;
|
|
48412
|
-
const reportsDir =
|
|
48602
|
+
const reportsDir = join34(project.value.path, ".godot", ...ours.split("/"));
|
|
48413
48603
|
let report = null;
|
|
48414
48604
|
let reportProblem = null;
|
|
48415
48605
|
try {
|
|
48416
|
-
const written =
|
|
48606
|
+
const written = existsSync27(reportsDir) && notCompiled === null ? readdirSync15(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
48417
48607
|
const newest = written.at(-1);
|
|
48418
48608
|
if (newest !== undefined) {
|
|
48419
|
-
report = parseJUnit(
|
|
48609
|
+
report = parseJUnit(readFileSync29(join34(reportsDir, newest, "results.xml"), "utf8"));
|
|
48420
48610
|
}
|
|
48421
48611
|
} catch (error) {
|
|
48422
48612
|
reportProblem = errorMessage(error);
|
|
@@ -48543,10 +48733,14 @@ class GodotServer {
|
|
|
48543
48733
|
const uncaughtFolded = foldedRepeats(uncaught.map(aboveTheRunner));
|
|
48544
48734
|
const listedAsUncaught = new Set(uncaughtFolded.map((entry) => entry.index));
|
|
48545
48735
|
const notApplied = ignoredYetRan(report, ignored);
|
|
48736
|
+
const testsNotRun = [
|
|
48737
|
+
...new Set((selection.value?.selected ?? []).filter((one) => !report.suites.some((suite) => (suite.path === one.suite || suite.name === basename11(one.suite, ".gd")) && suite.cases.some((ran) => ran.name === one.test || new RegExp(`^${one.test}\\W`).test(ran.name)))).map((one) => `${basename11(one.suite, ".gd")}:${one.test}`))
|
|
48738
|
+
];
|
|
48546
48739
|
return this.jsonTextResponse({
|
|
48547
|
-
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0,
|
|
48548
|
-
verdict,
|
|
48740
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0 && testsNotRun.length === 0,
|
|
48741
|
+
verdict: testsNotRun.length === 0 ? verdict : `${verdict}; not run: ${testsNotRun.join(", ")}`,
|
|
48549
48742
|
...missing.length > 0 ? { missing } : {},
|
|
48743
|
+
...testsNotRun.length > 0 ? { testsNotRun } : {},
|
|
48550
48744
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
48551
48745
|
exitCode,
|
|
48552
48746
|
exitSignal: run.exitSignal ?? undefined,
|
|
@@ -48569,6 +48763,7 @@ class GodotServer {
|
|
|
48569
48763
|
...scanned,
|
|
48570
48764
|
note: [
|
|
48571
48765
|
missingNote,
|
|
48766
|
+
testsNotRun.length > 0 ? `${testsNotRun.join(", ")} ${testsNotRun.length === 1 ? "was" : "were"} asked for and not run, so the run is not called passed: gdUnit4 runs a test_ function only in a script that extends GdUnitTestSuite, and leaves out a suite whose script does not load.` : undefined,
|
|
48572
48767
|
briefed > 0 ? `${briefed} of the ${failed.length} failing cases, those with the largest reports, are named with their first message only, since their reports would have made the answer too large to deliver; pass one case's suite path to see its reports.` : undefined,
|
|
48573
48768
|
notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
|
|
48574
48769
|
uncaughtNote(uncaught),
|
|
@@ -48611,6 +48806,16 @@ class GodotServer {
|
|
|
48611
48806
|
return this.createErrorResponse(scripts.reason);
|
|
48612
48807
|
}
|
|
48613
48808
|
if (scripts?.value == null) {
|
|
48809
|
+
const asked = readString(args, "scriptPath");
|
|
48810
|
+
if (project.ok && asked !== undefined) {
|
|
48811
|
+
const location = resolveWithinProject(project.value.path, asked);
|
|
48812
|
+
if (!location.ok) {
|
|
48813
|
+
return this.createErrorResponse(location.reason, PATH_SOLUTIONS);
|
|
48814
|
+
}
|
|
48815
|
+
if (!existsSync27(location.absolutePath)) {
|
|
48816
|
+
return this.missingScript(project.value.path, location.relativePath);
|
|
48817
|
+
}
|
|
48818
|
+
}
|
|
48614
48819
|
const one = await this.diagnosticsOf(args);
|
|
48615
48820
|
if ("noLanguageServer" in one) {
|
|
48616
48821
|
const checked = project.ok ? await this.engineDiagnostics(project.value.path, [readString(args, "scriptPath") ?? ""], one.noLanguageServer) : { refusal: project.response };
|
|
@@ -48677,6 +48882,11 @@ class GodotServer {
|
|
|
48677
48882
|
...byEngine === null ? {} : engineChecked(byEngine)
|
|
48678
48883
|
});
|
|
48679
48884
|
}
|
|
48885
|
+
missingScript(projectPath, relativePath) {
|
|
48886
|
+
const scripts = projectFiles(projectPath, [".gd"]).map((script) => `res://${script}`);
|
|
48887
|
+
const wanted = `res://${relativePath}`;
|
|
48888
|
+
return this.createErrorResponse(withNearestFiles(`Script file does not exist: ${wanted}`, wanted, scripts));
|
|
48889
|
+
}
|
|
48680
48890
|
async compileScripts(projectPath, paths) {
|
|
48681
48891
|
const payload = { checked: [], failed: [], scripts: [] };
|
|
48682
48892
|
const messages = [];
|
|
@@ -48713,21 +48923,28 @@ class GodotServer {
|
|
|
48713
48923
|
String(asParams(entry)["path"]),
|
|
48714
48924
|
asParams(entry)
|
|
48715
48925
|
]));
|
|
48926
|
+
const errorsOf = scripts.map((_, at) => readArray(failures.get(named[at] ?? "") ?? {}, "errors") ?? []);
|
|
48927
|
+
const uncachedPer = uncachedFor(projectPath, errorsOf.map((errors) => errors.map((entry) => String(asParams(entry)["message"]))));
|
|
48716
48928
|
return {
|
|
48717
48929
|
answers: scripts.map((scriptPath, at) => {
|
|
48718
48930
|
const failure = failures.get(named[at] ?? "");
|
|
48719
|
-
const diagnostics = (
|
|
48931
|
+
const diagnostics = (errorsOf[at] ?? []).map((entry) => {
|
|
48720
48932
|
const { line, message } = asParams(entry);
|
|
48721
48933
|
return { severity: "error", message, ...typeof line === "number" && line > 0 ? { line } : {} };
|
|
48722
48934
|
});
|
|
48723
48935
|
const dependencies = readArray(failure ?? {}, "dependencies_with_errors");
|
|
48936
|
+
const uncached = uncachedPer[at] ?? [];
|
|
48724
48937
|
return {
|
|
48725
48938
|
scriptPath,
|
|
48726
48939
|
clean: diagnostics.length === 0,
|
|
48727
48940
|
errors: diagnostics.length,
|
|
48728
48941
|
warnings: null,
|
|
48729
48942
|
diagnostics,
|
|
48730
|
-
...dependencies === undefined ? {} : { dependenciesWithErrors: dependencies }
|
|
48943
|
+
...dependencies === undefined ? {} : { dependenciesWithErrors: dependencies },
|
|
48944
|
+
...uncached.length === 0 ? {} : {
|
|
48945
|
+
typesMissingFromTheClassCache: uncached.map(({ type, declaredIn }) => ({ type, declaredIn })),
|
|
48946
|
+
staleAnalysis: uncachedForTheEngineNote(projectPath, uncached)
|
|
48947
|
+
}
|
|
48731
48948
|
};
|
|
48732
48949
|
})
|
|
48733
48950
|
};
|
|
@@ -48777,11 +48994,11 @@ class GodotServer {
|
|
|
48777
48994
|
const cached = cachedClasses(projectPath);
|
|
48778
48995
|
const sourceOf = (resourcePath) => {
|
|
48779
48996
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
48780
|
-
if (!contained.ok || !
|
|
48997
|
+
if (!contained.ok || !existsSync27(contained.absolutePath)) {
|
|
48781
48998
|
return null;
|
|
48782
48999
|
}
|
|
48783
49000
|
try {
|
|
48784
|
-
return
|
|
49001
|
+
return readFileSync29(contained.absolutePath, "utf8");
|
|
48785
49002
|
} catch {
|
|
48786
49003
|
return null;
|
|
48787
49004
|
}
|
|
@@ -48871,11 +49088,11 @@ class GodotServer {
|
|
|
48871
49088
|
}
|
|
48872
49089
|
sourceOfClass(projectPath, resourcePath) {
|
|
48873
49090
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
48874
|
-
if (!contained.ok || !
|
|
49091
|
+
if (!contained.ok || !existsSync27(contained.absolutePath)) {
|
|
48875
49092
|
return null;
|
|
48876
49093
|
}
|
|
48877
49094
|
try {
|
|
48878
|
-
return
|
|
49095
|
+
return readFileSync29(contained.absolutePath, "utf8");
|
|
48879
49096
|
} catch {
|
|
48880
49097
|
return null;
|
|
48881
49098
|
}
|
|
@@ -49032,7 +49249,7 @@ class GodotServer {
|
|
|
49032
49249
|
...Array.isArray(stack) ? {
|
|
49033
49250
|
stack: stack.map((frame) => ({
|
|
49034
49251
|
...frame,
|
|
49035
|
-
...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path:
|
|
49252
|
+
...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join34(projectPath, frame["script"].slice("res://".length)) } : {}
|
|
49036
49253
|
}))
|
|
49037
49254
|
} : {}
|
|
49038
49255
|
});
|
|
@@ -49311,7 +49528,7 @@ class GodotServer {
|
|
|
49311
49528
|
return null;
|
|
49312
49529
|
}
|
|
49313
49530
|
try {
|
|
49314
|
-
return settingKeys(
|
|
49531
|
+
return settingKeys(readFileSync29(join34(projectPath, "project.godot"), "utf8"));
|
|
49315
49532
|
} catch {
|
|
49316
49533
|
return null;
|
|
49317
49534
|
}
|
|
@@ -49756,7 +49973,7 @@ class GodotServer {
|
|
|
49756
49973
|
}
|
|
49757
49974
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
49758
49975
|
const budgetMs = budget.ms;
|
|
49759
|
-
if (!
|
|
49976
|
+
if (!existsSync27(join34(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
49760
49977
|
return runtimeVerdict(null, {
|
|
49761
49978
|
addon: false,
|
|
49762
49979
|
budgetMs,
|
|
@@ -49814,7 +50031,7 @@ class GodotServer {
|
|
|
49814
50031
|
if (going !== null && !running) {
|
|
49815
50032
|
this.drainTranscript(going);
|
|
49816
50033
|
}
|
|
49817
|
-
|
|
50034
|
+
const verdict = runtimeVerdict(endpoint, {
|
|
49818
50035
|
addon: true,
|
|
49819
50036
|
budgetMs,
|
|
49820
50037
|
...budget.sizedToMs === undefined ? {} : { sizedToMs: budget.sizedToMs },
|
|
@@ -49825,6 +50042,8 @@ class GodotServer {
|
|
|
49825
50042
|
...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {},
|
|
49826
50043
|
runtimeCameUp: going !== null && !running && going.log.all.some((entry) => entry.text.startsWith(RUNTIME_LISTENING_LINE))
|
|
49827
50044
|
});
|
|
50045
|
+
const staleNote = endpoint === null ? undefined : this.runtimeStaleNote(endpoint);
|
|
50046
|
+
return staleNote === undefined ? verdict : { ...verdict, addonIsStale: true, staleNote };
|
|
49828
50047
|
}
|
|
49829
50048
|
announceBudget(args, projectPath, scene) {
|
|
49830
50049
|
const key = scene ?? MAIN_SCENE;
|
|
@@ -50036,7 +50255,7 @@ class GodotServer {
|
|
|
50036
50255
|
return 0;
|
|
50037
50256
|
}
|
|
50038
50257
|
try {
|
|
50039
|
-
return reportAlreadyInTranscript(
|
|
50258
|
+
return reportAlreadyInTranscript(readFileSync29(game.transcript), readFileSync29(report));
|
|
50040
50259
|
} catch (error) {
|
|
50041
50260
|
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
50042
50261
|
return 0;
|
|
@@ -50731,7 +50950,7 @@ class GodotServer {
|
|
|
50731
50950
|
};
|
|
50732
50951
|
const going = await this.runStillGoing(run);
|
|
50733
50952
|
const projectPath = run.projectPath;
|
|
50734
|
-
const projectGone = projectPath !== null && !
|
|
50953
|
+
const projectGone = projectPath !== null && !existsSync27(join34(projectPath, "project.godot"));
|
|
50735
50954
|
if (projectGone && callersName !== undefined) {
|
|
50736
50955
|
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"]);
|
|
50737
50956
|
}
|
|
@@ -51078,7 +51297,7 @@ class GodotServer {
|
|
|
51078
51297
|
notes.push(endedWithoutACode({
|
|
51079
51298
|
throughEditor: run.throughEditor,
|
|
51080
51299
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
51081
|
-
couldAnnounce: run.projectPath !== null &&
|
|
51300
|
+
couldAnnounce: run.projectPath !== null && existsSync27(join34(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
51082
51301
|
endedHere: typeof run.endedHere === "string",
|
|
51083
51302
|
pickedUp: run.pickedUp === true
|
|
51084
51303
|
}));
|
|
@@ -51707,8 +51926,8 @@ class GodotServer {
|
|
|
51707
51926
|
if (!scriptPath.ok) {
|
|
51708
51927
|
return this.createErrorResponse(scriptPath.reason);
|
|
51709
51928
|
}
|
|
51710
|
-
if (!
|
|
51711
|
-
return this.
|
|
51929
|
+
if (!existsSync27(scriptPath.absolutePath)) {
|
|
51930
|
+
return this.missingScript(projectPath, scriptPath.relativePath);
|
|
51712
51931
|
}
|
|
51713
51932
|
const asked = readNonEmptyString(args, "newScriptPath");
|
|
51714
51933
|
const moveTo = asked === undefined ? null : resolveWithinProject(projectPath, asked);
|
|
@@ -52121,7 +52340,7 @@ class GodotServer {
|
|
|
52121
52340
|
}
|
|
52122
52341
|
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
52123
52342
|
const declared = new RegExp(`^\\s*(?:@\\w+(?:\\([^)]*\\))?\\s+)*class_name\\s+${escaped}\\b`, "m");
|
|
52124
|
-
if (declared.test(
|
|
52343
|
+
if (declared.test(readFileSync29(scriptPath, "utf8"))) {
|
|
52125
52344
|
return null;
|
|
52126
52345
|
}
|
|
52127
52346
|
const checked = await this.classesTheEditorCannotSee(args);
|
|
@@ -52287,7 +52506,7 @@ class GodotServer {
|
|
|
52287
52506
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
52288
52507
|
const run = await this.currentRun();
|
|
52289
52508
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
52290
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
52509
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync27(join34(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
|
|
52291
52510
|
return null;
|
|
52292
52511
|
}
|
|
52293
52512
|
const already = runtimesAnnounced().running;
|
|
@@ -52378,12 +52597,10 @@ class GodotServer {
|
|
|
52378
52597
|
}
|
|
52379
52598
|
const staleNote = this.runtimeStaleNote(choice.endpoint);
|
|
52380
52599
|
if (staleNote !== undefined) {
|
|
52381
|
-
return this.createErrorResponse(staleNote
|
|
52382
|
-
"editor_status lists the game and the runtime addon it loaded"
|
|
52383
|
-
]);
|
|
52600
|
+
return this.createErrorResponse(staleNote);
|
|
52384
52601
|
}
|
|
52385
52602
|
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
52386
|
-
const screenshotPath = screenshotDir ?
|
|
52603
|
+
const screenshotPath = screenshotDir ? join34(screenshotDir, "capture.png") : null;
|
|
52387
52604
|
const refused = (message) => this.createErrorResponse(message);
|
|
52388
52605
|
const onGame = {
|
|
52389
52606
|
pid: choice.endpoint.pid,
|
|
@@ -52444,7 +52661,7 @@ class GodotServer {
|
|
|
52444
52661
|
|
|
52445
52662
|
Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}`
|
|
52446
52663
|
},
|
|
52447
|
-
{ type: "image", data:
|
|
52664
|
+
{ type: "image", data: readFileSync29(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
52448
52665
|
]
|
|
52449
52666
|
};
|
|
52450
52667
|
} finally {
|
|
@@ -52619,6 +52836,7 @@ var init_server = __esm(() => {
|
|
|
52619
52836
|
init_server_version();
|
|
52620
52837
|
init_setup();
|
|
52621
52838
|
init_spawn();
|
|
52839
|
+
init_test_selection();
|
|
52622
52840
|
init_tool_definitions();
|
|
52623
52841
|
init_tool_reference();
|
|
52624
52842
|
init_update_check();
|
|
@@ -52667,6 +52885,7 @@ var init_server = __esm(() => {
|
|
|
52667
52885
|
});
|
|
52668
52886
|
PROJECT_INFO_SECTIONS2 = dictionary({
|
|
52669
52887
|
autoloads: { operation: "list_autoloads", params: () => ({}) },
|
|
52888
|
+
input_actions: { operation: "list_input_actions", params: () => ({}) },
|
|
52670
52889
|
plugins: { operation: "list_plugins", params: () => ({}) },
|
|
52671
52890
|
export_presets: { operation: "list_export_presets", params: () => ({}) },
|
|
52672
52891
|
audio_buses: { operation: "get_audio_buses", params: () => ({}) },
|
|
@@ -52683,8 +52902,8 @@ var init_server = __esm(() => {
|
|
|
52683
52902
|
init_bridge_announce();
|
|
52684
52903
|
init_errors();
|
|
52685
52904
|
init_godot_path();
|
|
52686
|
-
import { existsSync as
|
|
52687
|
-
import { join as
|
|
52905
|
+
import { existsSync as existsSync28 } from "node:fs";
|
|
52906
|
+
import { join as join35, resolve as resolve8 } from "node:path";
|
|
52688
52907
|
|
|
52689
52908
|
// src/harnesses.ts
|
|
52690
52909
|
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";
|
|
@@ -53828,6 +54047,13 @@ declaration and the cache can both be right while the editor stays blind to it.
|
|
|
53828
54047
|
its own, which is the measured cure and needs no change to the declaring script; \`editor_launch
|
|
53829
54048
|
restart\` also does it and costs a restart of the editor.
|
|
53830
54049
|
|
|
54050
|
+
With no editor open, the headless engine that compiles for \`script_diagnostics\` and for
|
|
54051
|
+
\`project_info\`'s health and validation resolves class names only from that cache file. A
|
|
54052
|
+
project never opened, as a fresh clone is, has no such file, so every script typed by a project
|
|
54053
|
+
class fails there. \`script_diagnostics\` names the class under
|
|
54054
|
+
\`typesMissingFromTheClassCache\`, health and validation say so beside the failure, and
|
|
54055
|
+
\`project_import refresh_classes\` writes the cache from the declarations on disk.
|
|
54056
|
+
|
|
53831
54057
|
The scan writes \`.godot/global_script_class_cache.cfg\` from the list the editor is holding, so
|
|
53832
54058
|
a class it cannot resolve would go out of that file with it. The answer names that loss under
|
|
53833
54059
|
\`cacheLost\` and rebuilds the cache from the declarations on disk, naming what came back under
|
|
@@ -53968,7 +54194,7 @@ class UsageError extends Refusal {
|
|
|
53968
54194
|
function projectArgument(at) {
|
|
53969
54195
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
53970
54196
|
const projectPath = resolve8(named ?? ".");
|
|
53971
|
-
if (!
|
|
54197
|
+
if (!existsSync28(join35(projectPath, "project.godot"))) {
|
|
53972
54198
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
53973
54199
|
}
|
|
53974
54200
|
return projectPath;
|
|
@@ -54013,7 +54239,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
54013
54239
|
return;
|
|
54014
54240
|
}
|
|
54015
54241
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
54016
|
-
if (!
|
|
54242
|
+
if (!existsSync28(join35(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
54017
54243
|
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}`);
|
|
54018
54244
|
}
|
|
54019
54245
|
}
|
|
@@ -54145,7 +54371,7 @@ async function setup() {
|
|
|
54145
54371
|
console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
|
|
54146
54372
|
}
|
|
54147
54373
|
if (!args.includes("--no-skill")) {
|
|
54148
|
-
const directories = skillDirectories(chosen.harnesses, projectPath,
|
|
54374
|
+
const directories = skillDirectories(chosen.harnesses, projectPath, existsSync28);
|
|
54149
54375
|
for (const written of writeSkill(directories, getLocalVersion())) {
|
|
54150
54376
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
54151
54377
|
}
|
|
@@ -54259,7 +54485,7 @@ async function upgrade() {
|
|
|
54259
54485
|
moved.push(took.from);
|
|
54260
54486
|
}
|
|
54261
54487
|
}
|
|
54262
|
-
for (const written of writeSkill(skillDirectories(already, projectPath,
|
|
54488
|
+
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync28), version)) {
|
|
54263
54489
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
54264
54490
|
}
|
|
54265
54491
|
console.log(`
|