gdharness 1.2.35 → 1.2.37
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 +478 -100
- package/build/godot/addons/gdharness_editor/headless_check.gd +374 -0
- package/build/godot/addons/gdharness_editor/headless_check.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +3 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +345 -10
- package/build/godot/addons/gdharness_editor/serialisation.gd +103 -5
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +68 -18
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +24 -13
- package/build/godot/addons/gdharness_runtime/call_errors.gd +7 -2
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +9 -5
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +33 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +21 -4
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +7 -5
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +116 -3
- package/build/godot/addons/gdharness_runtime/serialisation.gd +103 -5
- package/build/godot/operations/classdb_queries.gd +22 -0
- package/build/godot/operations/dependencies.gd +90 -20
- package/build/godot/operations/editor_tools.gd +57 -0
- package/build/godot/operations/gdscript_analysis.gd +45 -2
- package/build/godot/operations/gdscript_authoring.gd +5 -2
- package/build/godot/operations/godot_operations.gd +53 -2
- package/build/godot/operations/property_values.gd +3 -0
- package/build/godot/operations/rename_checks.gd +0 -19
- package/build/godot/operations/script_check.gd +98 -0
- package/build/godot/operations/serialisation.gd +103 -5
- package/build/index.js +466 -90
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -3373,7 +3373,8 @@ import {
|
|
|
3373
3373
|
readFileSync as readFileSync24,
|
|
3374
3374
|
readSync as readSync2,
|
|
3375
3375
|
rmdirSync as rmdirSync2,
|
|
3376
|
-
statSync as statSync12
|
|
3376
|
+
statSync as statSync12,
|
|
3377
|
+
writeFileSync as writeFileSync16
|
|
3377
3378
|
} from "node:fs";
|
|
3378
3379
|
import { basename as basename7, dirname as dirname18, extname as extname4, isAbsolute as isAbsolute4, join as join26, normalize as normalize2, relative as relative5 } from "node:path";
|
|
3379
3380
|
import { setTimeout as delay5 } from "node:timers/promises";
|
|
@@ -32974,6 +32975,7 @@ var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
|
|
|
32974
32975
|
var waiting = new Set;
|
|
32975
32976
|
var atExit = new Set;
|
|
32976
32977
|
var KINDS = [
|
|
32978
|
+
"answers",
|
|
32977
32979
|
"desktop",
|
|
32978
32980
|
"export",
|
|
32979
32981
|
"import",
|
|
@@ -33078,8 +33080,14 @@ function snakeCased(params) {
|
|
|
33078
33080
|
}
|
|
33079
33081
|
function engineExtras(outcome) {
|
|
33080
33082
|
const after = outcome.afterAnswerMessages ?? [];
|
|
33083
|
+
const before = outcome.beforeOperationMessages ?? [];
|
|
33081
33084
|
return {
|
|
33082
33085
|
...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
|
|
33086
|
+
...before.length > 0 ? {
|
|
33087
|
+
engine_messages_before_operation: before,
|
|
33088
|
+
engine_messages_before_operation_note: BEFORE_OPERATION_NOTE
|
|
33089
|
+
} : {},
|
|
33090
|
+
...outcome.beforeOperationOmitted === undefined ? {} : { engine_messages_before_operation_omitted: outcome.beforeOperationOmitted },
|
|
33083
33091
|
...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
|
|
33084
33092
|
...after.length > 0 ? { engine_messages_after_answer: after, engine_messages_after_answer_note: AFTER_ANSWER_NOTE } : {},
|
|
33085
33093
|
...outcome.afterAnswerOmitted === undefined ? {} : { engine_messages_after_answer_omitted: outcome.afterAnswerOmitted },
|
|
@@ -33087,8 +33095,11 @@ function engineExtras(outcome) {
|
|
|
33087
33095
|
};
|
|
33088
33096
|
}
|
|
33089
33097
|
var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, while the engine ran the project's autoloads and shut down: Godot starts a project's autoloads in every script run and cannot be told not to. An error raised in a project script here is that autoload's own, and none of these changes the answer.";
|
|
33098
|
+
var BEFORE_OPERATION_NOTE = "Printed while the engine started the project's autoloads, before this operation ran: an operation that compiles the project's scripts waits for them, because the engine knows an autoload's name only once it is in the tree. An error raised in a project script here is that autoload's own.";
|
|
33090
33099
|
var OWN_ERROR = "[ERROR] ";
|
|
33091
33100
|
var ANSWERED = "[gdharness] answered";
|
|
33101
|
+
var WAITING = "[gdharness] waiting for the autoloads";
|
|
33102
|
+
var BEGUN = "[gdharness] begun";
|
|
33092
33103
|
var MESSAGES_KEPT = 200;
|
|
33093
33104
|
var ERRORS_NAMED = 3;
|
|
33094
33105
|
function problemsIn(log) {
|
|
@@ -33229,12 +33240,22 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
33229
33240
|
discard(paramsDir);
|
|
33230
33241
|
}
|
|
33231
33242
|
const everything = ran.log.everything();
|
|
33232
|
-
const
|
|
33233
|
-
const
|
|
33243
|
+
const marked = (text) => everything.findIndex((entry) => entry.source === "stderr" && entry.text === text);
|
|
33244
|
+
const answered = marked(ANSWERED);
|
|
33245
|
+
const waiting = marked(WAITING);
|
|
33246
|
+
const begun = marked(BEGUN);
|
|
33247
|
+
const ended = answered === -1 ? everything.length : answered;
|
|
33248
|
+
const problems = waiting === -1 ? problemsAmong(everything.slice(0, ended)) : problemsAmong([
|
|
33249
|
+
...everything.slice(0, waiting),
|
|
33250
|
+
...begun === -1 ? [] : everything.slice(begun + 1, ended)
|
|
33251
|
+
]);
|
|
33252
|
+
const startup = waiting === -1 ? [] : problemsAmong(everything.slice(waiting + 1, begun === -1 ? ended : begun));
|
|
33234
33253
|
const after = answered === -1 ? [] : problemsAmong(everything.slice(answered + 1));
|
|
33235
33254
|
const afterAnswered = {
|
|
33236
33255
|
...after.length === 0 ? {} : { afterAnswerMessages: after.slice(-MESSAGES_KEPT) },
|
|
33237
|
-
...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {}
|
|
33256
|
+
...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {},
|
|
33257
|
+
...startup.length === 0 ? {} : { beforeOperationMessages: startup.slice(-MESSAGES_KEPT) },
|
|
33258
|
+
...startup.length > MESSAGES_KEPT ? { beforeOperationOmitted: startup.length - MESSAGES_KEPT } : {}
|
|
33238
33259
|
};
|
|
33239
33260
|
if (answer.kind === "unreadable") {
|
|
33240
33261
|
return {
|
|
@@ -33245,6 +33266,14 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
33245
33266
|
};
|
|
33246
33267
|
}
|
|
33247
33268
|
if (answer.kind === "none") {
|
|
33269
|
+
if (waiting !== -1 && begun === -1) {
|
|
33270
|
+
return {
|
|
33271
|
+
ok: false,
|
|
33272
|
+
message: `${operation} never ran: the engine ended (${howItEnded(ran)}) while it started the project's ` + `autoloads, which this operation waits for. The last it reported: ${reason(ran.log, startup)}`,
|
|
33273
|
+
...reported2(problems),
|
|
33274
|
+
...afterAnswered
|
|
33275
|
+
};
|
|
33276
|
+
}
|
|
33248
33277
|
const refusals = refusalsIn(ran.log);
|
|
33249
33278
|
if (refusals.length > 0) {
|
|
33250
33279
|
const said = new Set(refusals.map((entry) => entry.index));
|
|
@@ -33686,6 +33715,31 @@ ${note}`;
|
|
|
33686
33715
|
return detail === entry.detail ? entry : { ...entry, detail };
|
|
33687
33716
|
});
|
|
33688
33717
|
}
|
|
33718
|
+
var MOST_REPORTS_SHOWN = 20;
|
|
33719
|
+
var LONGEST_DETAIL = 20000;
|
|
33720
|
+
function shortened(detail) {
|
|
33721
|
+
if (detail === null || detail.length <= LONGEST_DETAIL) {
|
|
33722
|
+
return detail;
|
|
33723
|
+
}
|
|
33724
|
+
return `${detail.slice(0, LONGEST_DETAIL)}
|
|
33725
|
+
[${detail.length - LONGEST_DETAIL} more characters left out]`;
|
|
33726
|
+
}
|
|
33727
|
+
function foldedReports(reports) {
|
|
33728
|
+
const first = new Map;
|
|
33729
|
+
for (const report of reports) {
|
|
33730
|
+
const key = JSON.stringify([report.kind, report.message, report.detail]);
|
|
33731
|
+
const seen = first.get(key);
|
|
33732
|
+
if (seen === undefined) {
|
|
33733
|
+
first.set(key, { report, times: 1 });
|
|
33734
|
+
} else {
|
|
33735
|
+
seen.times += 1;
|
|
33736
|
+
}
|
|
33737
|
+
}
|
|
33738
|
+
return [...first.values()].map(({ report, times }) => {
|
|
33739
|
+
const kept = { ...report, detail: shortened(report.detail) };
|
|
33740
|
+
return times === 1 ? kept : { ...kept, times };
|
|
33741
|
+
});
|
|
33742
|
+
}
|
|
33689
33743
|
function failedCases(report, printed) {
|
|
33690
33744
|
const cases = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ entry, path: suite.path })));
|
|
33691
33745
|
const read = withActualsPrinted(cases.flatMap(({ entry }) => entry.reports), printed);
|
|
@@ -33693,6 +33747,8 @@ function failedCases(report, printed) {
|
|
|
33693
33747
|
return cases.map(({ entry, path }) => {
|
|
33694
33748
|
const reports = read.slice(next, next + entry.reports.length);
|
|
33695
33749
|
next += entry.reports.length;
|
|
33750
|
+
const folded = foldedReports(reports);
|
|
33751
|
+
const shown = folded.slice(0, MOST_REPORTS_SHOWN);
|
|
33696
33752
|
return {
|
|
33697
33753
|
suite: entry.suite,
|
|
33698
33754
|
name: entry.name,
|
|
@@ -33700,11 +33756,34 @@ function failedCases(report, printed) {
|
|
|
33700
33756
|
time: entry.time,
|
|
33701
33757
|
path,
|
|
33702
33758
|
message: reports[0]?.message ?? null,
|
|
33703
|
-
detail: reports[0]?.detail ?? null,
|
|
33704
|
-
...reports.length > 1 ? { reports } : {}
|
|
33759
|
+
detail: shortened(reports[0]?.detail ?? null),
|
|
33760
|
+
...reports.length > 1 ? { reports: shown, reportCount: reports.length } : {},
|
|
33761
|
+
...folded.length > shown.length ? { reportsLeftOut: folded.length - shown.length } : {}
|
|
33705
33762
|
};
|
|
33706
33763
|
});
|
|
33707
33764
|
}
|
|
33765
|
+
function failedWithin(failed, budget) {
|
|
33766
|
+
const sizes = failed.map((entry) => JSON.stringify(entry).length);
|
|
33767
|
+
const smallestFirst = sizes.map((_, index) => index).sort((a, b) => (sizes[a] ?? 0) - (sizes[b] ?? 0));
|
|
33768
|
+
const whole = new Set;
|
|
33769
|
+
let spent = 0;
|
|
33770
|
+
for (const index of smallestFirst) {
|
|
33771
|
+
const size = sizes[index] ?? 0;
|
|
33772
|
+
if (spent + size > budget) {
|
|
33773
|
+
break;
|
|
33774
|
+
}
|
|
33775
|
+
spent += size;
|
|
33776
|
+
whole.add(index);
|
|
33777
|
+
}
|
|
33778
|
+
const kept = failed.map((entry, index) => {
|
|
33779
|
+
if (whole.has(index)) {
|
|
33780
|
+
return entry;
|
|
33781
|
+
}
|
|
33782
|
+
const { detail: _detail, reports: _reports, reportsLeftOut: _left, ...named } = entry;
|
|
33783
|
+
return { ...named, message: entry.message?.slice(0, 500) ?? null, detail: null };
|
|
33784
|
+
});
|
|
33785
|
+
return { failed: kept, briefed: failed.length - whole.size };
|
|
33786
|
+
}
|
|
33708
33787
|
var MASKED_BRACKET = "[lb]";
|
|
33709
33788
|
function masked(expected, actual) {
|
|
33710
33789
|
const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
|
|
@@ -34899,6 +34978,8 @@ var SCENE_PATH = {
|
|
|
34899
34978
|
type: "string",
|
|
34900
34979
|
description: 'Scene file inside the project, such as "scenes/main.tscn" or "res://scenes/main.tscn".'
|
|
34901
34980
|
};
|
|
34981
|
+
var WITHOUT_AN_EDITOR = "Through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.";
|
|
34982
|
+
var WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.';
|
|
34902
34983
|
var SCRIPT_PATH = {
|
|
34903
34984
|
type: "string",
|
|
34904
34985
|
description: 'Script file inside the project, such as "scripts/player.gd".'
|
|
@@ -34911,7 +34992,7 @@ var NODE_PATH = {
|
|
|
34911
34992
|
};
|
|
34912
34993
|
var PROPERTIES = {
|
|
34913
34994
|
type: "object",
|
|
34914
|
-
description:
|
|
34995
|
+
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}`,
|
|
34915
34996
|
additionalProperties: true
|
|
34916
34997
|
};
|
|
34917
34998
|
var XY = {
|
|
@@ -34955,7 +35036,10 @@ var SCRIPT_MODIFICATIONS = {
|
|
|
34955
35036
|
name: { type: "string" },
|
|
34956
35037
|
params: { type: "string", description: 'Functions and signals: the parameter list, "delta: float".' },
|
|
34957
35038
|
returnType: { type: "string", description: "Functions: the return type." },
|
|
34958
|
-
body: {
|
|
35039
|
+
body: {
|
|
35040
|
+
type: "string",
|
|
35041
|
+
description: "Functions: the body, its lines separated by \\n. Indent shared by every line is taken off and one tab put on, so it can be written indented or not."
|
|
35042
|
+
},
|
|
34959
35043
|
varType: {
|
|
34960
35044
|
type: "string",
|
|
34961
35045
|
description: "Variables and constants: the type annotation; without one, the type the value evaluates to."
|
|
@@ -35058,7 +35142,7 @@ var TOOL_SPECS = [
|
|
|
35058
35142
|
{
|
|
35059
35143
|
name: "project_settings",
|
|
35060
35144
|
title: "Project settings",
|
|
35061
|
-
description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events and deadzone, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change
|
|
35145
|
+
description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events and deadzone, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change moves no other line; the save is the engine's, and like the editor's it writes Godot's header and config/features into a file that lacks them. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
|
|
35062
35146
|
parameters: {
|
|
35063
35147
|
projectPath: PROJECT_PATH2,
|
|
35064
35148
|
setting: {
|
|
@@ -35215,7 +35299,7 @@ var TOOL_SPECS = [
|
|
|
35215
35299
|
direction: {
|
|
35216
35300
|
type: "string",
|
|
35217
35301
|
enum: ["forward", "reverse"],
|
|
35218
|
-
description: "forward: what this resource loads, with cycles reported. reverse: every file that refers to it and how, 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, and
|
|
35302
|
+
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, and total_references counts the entries in the tree. reverse: every file that refers to it and how, 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."
|
|
35219
35303
|
},
|
|
35220
35304
|
depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
|
|
35221
35305
|
includeBuiltin: {
|
|
@@ -35236,24 +35320,32 @@ var TOOL_SPECS = [
|
|
|
35236
35320
|
description: "The import pipeline: what needs importing, how a resource is imported, reimports, UIDs, and the global class list the editor and the engine read.",
|
|
35237
35321
|
parameters: {
|
|
35238
35322
|
projectPath: PROJECT_PATH2,
|
|
35239
|
-
resourcePath: RESOURCE_PATH,
|
|
35323
|
+
resourcePath: { ...RESOURCE_PATH, ops: ["status", "options", "set_options", "uid", "reimport"] },
|
|
35240
35324
|
includeUpToDate: {
|
|
35241
35325
|
type: "boolean",
|
|
35326
|
+
ops: ["status"],
|
|
35242
35327
|
description: "status: list resources that are current as well. Default false."
|
|
35243
35328
|
},
|
|
35244
35329
|
options: {
|
|
35245
35330
|
type: "object",
|
|
35331
|
+
ops: ["set_options"],
|
|
35246
35332
|
description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads.`,
|
|
35247
35333
|
additionalProperties: true
|
|
35248
35334
|
},
|
|
35249
35335
|
reimport: {
|
|
35250
35336
|
type: "boolean",
|
|
35337
|
+
ops: ["set_options"],
|
|
35251
35338
|
description: "set_options: reimport afterwards, answered under reimport as project_import reimport answers. Default true."
|
|
35252
35339
|
},
|
|
35253
|
-
force: {
|
|
35340
|
+
force: {
|
|
35341
|
+
type: "boolean",
|
|
35342
|
+
ops: ["reimport"],
|
|
35343
|
+
description: "reimport: reimport even what is current. Default false."
|
|
35344
|
+
},
|
|
35254
35345
|
timeoutMs: {
|
|
35255
35346
|
type: "integer",
|
|
35256
35347
|
minimum: 1,
|
|
35348
|
+
ops: ["reimport", "set_options"],
|
|
35257
35349
|
description: "reimport, set_options: how long to wait for the editor to finish reimporting. Default 300000."
|
|
35258
35350
|
}
|
|
35259
35351
|
},
|
|
@@ -35331,7 +35423,7 @@ var TOOL_SPECS = [
|
|
|
35331
35423
|
name: "project_test",
|
|
35332
35424
|
title: "Run tests",
|
|
35333
35425
|
effect: "additive",
|
|
35334
|
-
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,
|
|
35426
|
+
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 rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; 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, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
35335
35427
|
parameters: {
|
|
35336
35428
|
projectPath: PROJECT_PATH2,
|
|
35337
35429
|
path: {
|
|
@@ -35368,7 +35460,7 @@ var TOOL_SPECS = [
|
|
|
35368
35460
|
{
|
|
35369
35461
|
name: "scene_create",
|
|
35370
35462
|
title: "Create or save a scene",
|
|
35371
|
-
description:
|
|
35463
|
+
description: `Creates a scene file where none is, saves one, or saves a copy under a new path. A save of a scene open in the editor saves the editor's copy, unsaved changes included. ${WITHOUT_AN_EDITOR}`,
|
|
35372
35464
|
parameters: {
|
|
35373
35465
|
projectPath: PROJECT_PATH2,
|
|
35374
35466
|
scenePath: SCENE_PATH,
|
|
@@ -35413,17 +35505,19 @@ var TOOL_SPECS = [
|
|
|
35413
35505
|
{
|
|
35414
35506
|
name: "scene_node",
|
|
35415
35507
|
title: "Scene nodes",
|
|
35416
|
-
description:
|
|
35508
|
+
description: `One node in a scene file: add, read, set, duplicate, reparent or delete it, or paint TileMap cells. Any ClassDB node type can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties, and another scene is instanced by an add with instancePath. A set with properties {"name": "Boss"} renames a node. A property holding a Resource takes the res:// path of one, so a texture, a material or a theme is a set like any other. A write is refused while the editor holds unsaved changes to the scene, and for a node that belongs to a scene this one instances or inherits. ${WITHOUT_AN_EDITOR}`,
|
|
35417
35509
|
parameters: {
|
|
35418
35510
|
projectPath: PROJECT_PATH2,
|
|
35419
35511
|
scenePath: SCENE_PATH,
|
|
35420
|
-
nodePath: NODE_PATH,
|
|
35512
|
+
nodePath: { ...NODE_PATH, ops: ["get", "set", "duplicate", "reparent", "delete", "set_tilemap_cells"] },
|
|
35421
35513
|
parentNodePath: {
|
|
35422
35514
|
type: "string",
|
|
35515
|
+
ops: ["add", "duplicate"],
|
|
35423
35516
|
description: "add: where the node goes. Default the root. duplicate: where the copy goes."
|
|
35424
35517
|
},
|
|
35425
35518
|
nodeType: {
|
|
35426
35519
|
type: "string",
|
|
35520
|
+
ops: ["add"],
|
|
35427
35521
|
description: 'add: the node class, such as "CharacterBody2D". Give this or instancePath.'
|
|
35428
35522
|
},
|
|
35429
35523
|
instancePath: {
|
|
@@ -35431,11 +35525,12 @@ var TOOL_SPECS = [
|
|
|
35431
35525
|
ops: ["add"],
|
|
35432
35526
|
description: `add: a scene to instance as the new node instead of a class, such as "ui/panel.tscn". It is saved as an instance of that file, the way the editor's Instantiate Child Scene saves one, so changes to that scene reach it; properties set its root. A scene that is the target or instances it is refused.`
|
|
35433
35527
|
},
|
|
35434
|
-
nodeName: { type: "string", description: "add: the new node's name." },
|
|
35435
|
-
properties: PROPERTIES,
|
|
35436
|
-
newName: { type: "string", description: "duplicate: the copy's name." },
|
|
35528
|
+
nodeName: { type: "string", ops: ["add"], description: "add: the new node's name." },
|
|
35529
|
+
properties: { ...PROPERTIES, ops: ["add", "set"] },
|
|
35530
|
+
newName: { type: "string", ops: ["duplicate"], description: "duplicate: the copy's name." },
|
|
35437
35531
|
newParentPath: {
|
|
35438
35532
|
type: "string",
|
|
35533
|
+
ops: ["reparent"],
|
|
35439
35534
|
description: "reparent: the new parent, which may be the one it has, to move it among its siblings."
|
|
35440
35535
|
},
|
|
35441
35536
|
index: {
|
|
@@ -35446,13 +35541,15 @@ var TOOL_SPECS = [
|
|
|
35446
35541
|
},
|
|
35447
35542
|
includeDefaults: {
|
|
35448
35543
|
type: "boolean",
|
|
35544
|
+
ops: ["get"],
|
|
35449
35545
|
description: "get: include properties still at their default. Default false."
|
|
35450
35546
|
},
|
|
35451
35547
|
layer: {
|
|
35452
35548
|
type: "number",
|
|
35549
|
+
ops: ["set_tilemap_cells"],
|
|
35453
35550
|
description: "set_tilemap_cells: the layer of a TileMap. Default 0. A TileMapLayer is one layer."
|
|
35454
35551
|
},
|
|
35455
|
-
cells: TILEMAP_CELLS
|
|
35552
|
+
cells: { ...TILEMAP_CELLS, ops: ["set_tilemap_cells"] }
|
|
35456
35553
|
},
|
|
35457
35554
|
requires: ["projectPath", "scenePath"],
|
|
35458
35555
|
operations: {
|
|
@@ -35492,16 +35589,32 @@ var TOOL_SPECS = [
|
|
|
35492
35589
|
{
|
|
35493
35590
|
name: "scene_signal",
|
|
35494
35591
|
title: "Scene signals",
|
|
35495
|
-
description:
|
|
35592
|
+
description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connect of a pair already connected says alreadyConnected, with flagsBefore when it changed the flags. ${WITHOUT_AN_EDITOR}`,
|
|
35496
35593
|
parameters: {
|
|
35497
35594
|
projectPath: PROJECT_PATH2,
|
|
35498
35595
|
scenePath: SCENE_PATH,
|
|
35499
|
-
sourceNodePath: { type: "string", description: "The node that emits." },
|
|
35500
|
-
signalName: {
|
|
35501
|
-
|
|
35502
|
-
|
|
35503
|
-
|
|
35504
|
-
|
|
35596
|
+
sourceNodePath: { type: "string", ops: ["connect", "disconnect"], description: "The node that emits." },
|
|
35597
|
+
signalName: {
|
|
35598
|
+
type: "string",
|
|
35599
|
+
ops: ["connect", "disconnect"],
|
|
35600
|
+
description: 'The signal it emits, such as "pressed".'
|
|
35601
|
+
},
|
|
35602
|
+
targetNodePath: {
|
|
35603
|
+
type: "string",
|
|
35604
|
+
ops: ["connect", "disconnect"],
|
|
35605
|
+
description: "The node whose method is called."
|
|
35606
|
+
},
|
|
35607
|
+
methodName: {
|
|
35608
|
+
type: "string",
|
|
35609
|
+
ops: ["connect", "disconnect"],
|
|
35610
|
+
description: "The method on that node, called when it emits."
|
|
35611
|
+
},
|
|
35612
|
+
flags: {
|
|
35613
|
+
type: "number",
|
|
35614
|
+
ops: ["connect"],
|
|
35615
|
+
description: "connect: Object.ConnectFlags, such as 1 for deferred."
|
|
35616
|
+
},
|
|
35617
|
+
nodePath: { type: "string", ops: ["list"], description: "list: only connections involving this node." }
|
|
35505
35618
|
},
|
|
35506
35619
|
requires: ["projectPath", "scenePath"],
|
|
35507
35620
|
operations: {
|
|
@@ -35626,18 +35739,28 @@ var TOOL_SPECS = [
|
|
|
35626
35739
|
parameters: {
|
|
35627
35740
|
projectPath: PROJECT_PATH2,
|
|
35628
35741
|
scriptPath: SCRIPT_PATH,
|
|
35629
|
-
className: {
|
|
35630
|
-
|
|
35742
|
+
className: {
|
|
35743
|
+
type: "string",
|
|
35744
|
+
ops: ["create"],
|
|
35745
|
+
description: "create: a class_name for the script. Not with content."
|
|
35746
|
+
},
|
|
35747
|
+
extends: {
|
|
35748
|
+
type: "string",
|
|
35749
|
+
ops: ["create"],
|
|
35750
|
+
description: "create: the base class. Default Node. Not with content."
|
|
35751
|
+
},
|
|
35631
35752
|
content: {
|
|
35632
35753
|
type: "string",
|
|
35754
|
+
ops: ["create"],
|
|
35633
35755
|
description: "create: the whole file, written as given, instead of a template, className and extends."
|
|
35634
35756
|
},
|
|
35635
35757
|
template: {
|
|
35636
35758
|
type: "string",
|
|
35637
35759
|
enum: ["singleton", "state_machine", "component", "resource"],
|
|
35760
|
+
ops: ["create"],
|
|
35638
35761
|
description: "create: a starting shape."
|
|
35639
35762
|
},
|
|
35640
|
-
modifications: SCRIPT_MODIFICATIONS,
|
|
35763
|
+
modifications: { ...SCRIPT_MODIFICATIONS, ops: ["modify"] },
|
|
35641
35764
|
symbol: {
|
|
35642
35765
|
type: "string",
|
|
35643
35766
|
ops: ["rename"],
|
|
@@ -35675,16 +35798,19 @@ var TOOL_SPECS = [
|
|
|
35675
35798
|
scriptPath: SCRIPT_PATH,
|
|
35676
35799
|
includeInherited: {
|
|
35677
35800
|
type: "boolean",
|
|
35801
|
+
ops: ["structure"],
|
|
35678
35802
|
description: "structure: also list what the script inherits, walking extends through the project's other scripts. Each such member carries inherited_from, the file declaring it, and the answer gains inherits_from, the chain that was walked. A name the script overrides is listed at both its lines rather than once. A native base is not walked, since it declares nothing in a file: editor_classes info answers for those. Default false."
|
|
35679
35803
|
},
|
|
35680
35804
|
line: {
|
|
35681
35805
|
type: "integer",
|
|
35682
35806
|
minimum: 1,
|
|
35807
|
+
ops: ["completion", "hover"],
|
|
35683
35808
|
description: "completion, hover: the line, counted from 1 as the editor shows it."
|
|
35684
35809
|
},
|
|
35685
35810
|
character: {
|
|
35686
35811
|
type: "integer",
|
|
35687
35812
|
minimum: 1,
|
|
35813
|
+
ops: ["completion", "hover"],
|
|
35688
35814
|
description: "completion, hover: the column, counted from 1 as the editor shows it, with a tab counted as one. symbols gives each name at the line and column hover is asked at."
|
|
35689
35815
|
}
|
|
35690
35816
|
},
|
|
@@ -35692,7 +35818,7 @@ var TOOL_SPECS = [
|
|
|
35692
35818
|
operations: {
|
|
35693
35819
|
structure: {
|
|
35694
35820
|
effect: "read",
|
|
35695
|
-
summary: "functions, variables, signals, class_name and extends, read from the file",
|
|
35821
|
+
summary: "functions, variables, signals, class_name and extends, 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",
|
|
35696
35822
|
requires: []
|
|
35697
35823
|
},
|
|
35698
35824
|
symbols: {
|
|
@@ -35713,7 +35839,7 @@ var TOOL_SPECS = [
|
|
|
35713
35839
|
name: "script_diagnostics",
|
|
35714
35840
|
title: "Script diagnostics",
|
|
35715
35841
|
effect: "read",
|
|
35716
|
-
description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean.
|
|
35842
|
+
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.",
|
|
35717
35843
|
parameters: {
|
|
35718
35844
|
projectPath: PROJECT_PATH2,
|
|
35719
35845
|
scriptPath: {
|
|
@@ -35727,7 +35853,7 @@ var TOOL_SPECS = [
|
|
|
35727
35853
|
{
|
|
35728
35854
|
name: "resource_edit",
|
|
35729
35855
|
title: "Edit resources",
|
|
35730
|
-
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
|
|
35856
|
+
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.',
|
|
35731
35857
|
parameters: {
|
|
35732
35858
|
projectPath: PROJECT_PATH2,
|
|
35733
35859
|
resourcePath: {
|
|
@@ -36078,9 +36204,10 @@ var TOOL_SPECS = [
|
|
|
36078
36204
|
description: "The engine's ClassDB: find classes, read one in full, or walk an inheritance tree.",
|
|
36079
36205
|
parameters: {
|
|
36080
36206
|
projectPath: PROJECT_PATH2,
|
|
36081
|
-
filter: { type: "string", description: "query: a substring of the class name." },
|
|
36207
|
+
filter: { type: "string", ops: ["query"], description: "query: a substring of the class name." },
|
|
36082
36208
|
category: {
|
|
36083
36209
|
type: "string",
|
|
36210
|
+
ops: ["query"],
|
|
36084
36211
|
enum: [
|
|
36085
36212
|
"node",
|
|
36086
36213
|
"node2d",
|
|
@@ -36097,9 +36224,21 @@ var TOOL_SPECS = [
|
|
|
36097
36224
|
],
|
|
36098
36225
|
description: "query: limit to one family."
|
|
36099
36226
|
},
|
|
36100
|
-
instantiableOnly: {
|
|
36101
|
-
|
|
36102
|
-
|
|
36227
|
+
instantiableOnly: {
|
|
36228
|
+
type: "boolean",
|
|
36229
|
+
ops: ["query"],
|
|
36230
|
+
description: "query: leave out abstract classes. Default false."
|
|
36231
|
+
},
|
|
36232
|
+
className: {
|
|
36233
|
+
type: "string",
|
|
36234
|
+
ops: ["info", "inheritance"],
|
|
36235
|
+
description: "info, inheritance: the class."
|
|
36236
|
+
},
|
|
36237
|
+
includeInherited: {
|
|
36238
|
+
type: "boolean",
|
|
36239
|
+
ops: ["info"],
|
|
36240
|
+
description: "info: include inherited members. Default false."
|
|
36241
|
+
},
|
|
36103
36242
|
member: {
|
|
36104
36243
|
type: "string",
|
|
36105
36244
|
ops: ["info"],
|
|
@@ -36171,7 +36310,7 @@ var TOOL_SPECS = [
|
|
|
36171
36310
|
says: {
|
|
36172
36311
|
type: "string",
|
|
36173
36312
|
ops: ["find", "accessibility"],
|
|
36174
|
-
description: 'accessibility: only the elements whose name or
|
|
36313
|
+
description: 'accessibility: only the elements whose name, value or value_text says these words, matched as find matches them, with what they hold. find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively and with a typed space matching any space the game draws (a non-breaking one included), which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Alternatives separated by | match when any one of them does, each by these rules ("*won*|*lost*"); write \\| for a bar that is part of the words.'
|
|
36175
36314
|
},
|
|
36176
36315
|
limit: {
|
|
36177
36316
|
type: "integer",
|
|
@@ -36202,7 +36341,7 @@ var TOOL_SPECS = [
|
|
|
36202
36341
|
tree: { effect: "read", summary: "the live scene tree", requires: [] },
|
|
36203
36342
|
text: {
|
|
36204
36343
|
effect: "read",
|
|
36205
|
-
summary: "every line of text under nodePath, the values in its fields included, in
|
|
36344
|
+
summary: "every line of text under nodePath, the values in its fields included, in reading order, leaving out what is hidden and everything under it. Every line reads as drawn: rich text without its tags, text only as far as it has been typed out, a translation rather than its key, an upper-case label in capitals and a secret field as its mask. Tab titles, menu bar titles, list items, a tree's column titles and rows, and an open menu's items and the titles of its separators read a line each",
|
|
36206
36345
|
requires: []
|
|
36207
36346
|
},
|
|
36208
36347
|
find: {
|
|
@@ -36222,12 +36361,12 @@ var TOOL_SPECS = [
|
|
|
36222
36361
|
},
|
|
36223
36362
|
metrics: {
|
|
36224
36363
|
effect: "read",
|
|
36225
|
-
summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game
|
|
36364
|
+
summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. physics_steps_per_second is the physics steps taken a second over those frames, fewer than physics_ticks_per_second when a frame needs more than max_physics_steps_per_frame or the editor's Game view slows it. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. Memory, object and render counts are read as asked. memory_static is what the engine allocated on the CPU side, in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are what the renderer has on the GPU, in bytes, which memory_static does not count",
|
|
36226
36365
|
requires: []
|
|
36227
36366
|
},
|
|
36228
36367
|
accessibility: {
|
|
36229
36368
|
effect: "read",
|
|
36230
|
-
summary:
|
|
36369
|
+
summary: `what the game tells a screen reader, read through UI Automation as a Windows screen reader does, under windows: one tree per window, each element with its role (text for static text, button, check_box, group for a control holding others), name, value, value_text (a range's spoken words, such as "75%"), description, placeholder, bounds in the window's pixels, the actions a reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the focused one. node is the node that made the element; a part a node made of itself has part_of, the node drawn around it that holds its words, or part_of_candidates where several are, and part: announcement marks the text a window announces through. An empty name reads as nothing: static text takes it from accessibility_update_set_value, and _note counts the text elements on screen with none. tooltip is the node's tooltip_text, which a Windows reader is not told. With nothing focused, the engine calls GdharnessRuntime focused. Needs a windowed Windows run started with engineArgs ["--accessibility", "always"] or accessibility_support at Always Active, since the engine builds the tree only when asked; reading takes a second or two`,
|
|
36231
36370
|
requires: []
|
|
36232
36371
|
}
|
|
36233
36372
|
},
|
|
@@ -36236,7 +36375,7 @@ var TOOL_SPECS = [
|
|
|
36236
36375
|
{
|
|
36237
36376
|
name: "runtime_invoke",
|
|
36238
36377
|
title: "Set or call in the running game",
|
|
36239
|
-
description: "Sets a property or calls a method on a node, or on one of the engine singletons nodePath names, in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. Errors
|
|
36378
|
+
description: "Sets a property or calls a method on a node, or on one of the engine singletons nodePath names, in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. Errors the engine reported while a called method ran come back under errors and warnings under warnings, the first 20 of each, with errors_not_shown and warnings_not_shown counting the rest. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
|
|
36240
36379
|
parameters: {
|
|
36241
36380
|
projectPath: RUNNING_PROJECT_PATH,
|
|
36242
36381
|
pid: RUNNING_PID,
|
|
@@ -36253,7 +36392,7 @@ var TOOL_SPECS = [
|
|
|
36253
36392
|
value: {
|
|
36254
36393
|
ops: ["set"],
|
|
36255
36394
|
blank: true,
|
|
36256
|
-
description: `set: the value, fitted to the property's type. 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.`
|
|
36395
|
+
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.`
|
|
36257
36396
|
},
|
|
36258
36397
|
method: {
|
|
36259
36398
|
type: "string",
|
|
@@ -36263,7 +36402,7 @@ var TOOL_SPECS = [
|
|
|
36263
36402
|
args: {
|
|
36264
36403
|
type: "array",
|
|
36265
36404
|
ops: ["call"],
|
|
36266
|
-
description: `call: the arguments, fitted to the method's parameter types. A number a parameter would have to cut is refused rather than truncated: a fraction for an int, a number other than 0 or 1 for 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}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
|
|
36405
|
+
description: `call: the arguments, fitted to the method's parameter types. An RID is passed as the {"_type": "RID", "id": ...} a read answers it with, such as a viewport's for RenderingServer's render timing. ${WHOLE_NUMBERS} A number a parameter would have to cut is refused rather than truncated: a fraction for an int, a number other than 0 or 1 for 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}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
|
|
36267
36406
|
},
|
|
36268
36407
|
properties: {
|
|
36269
36408
|
type: ["boolean", "array"],
|
|
@@ -36356,7 +36495,7 @@ var TOOL_SPECS = [
|
|
|
36356
36495
|
{
|
|
36357
36496
|
name: "runtime_input",
|
|
36358
36497
|
title: "Send input to the game",
|
|
36359
|
-
description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button, mouse motion or drag. All of it works headless, where the
|
|
36498
|
+
description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button, mouse motion or drag. All of it works headless, where the viewport is the project's window size and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
|
|
36360
36499
|
answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again, for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
|
|
36361
36500
|
parameters: {
|
|
36362
36501
|
projectPath: RUNNING_PROJECT_PATH,
|
|
@@ -36553,10 +36692,16 @@ var TOOL_SPECS = [
|
|
|
36553
36692
|
},
|
|
36554
36693
|
nodePath: {
|
|
36555
36694
|
type: "string",
|
|
36556
|
-
ops: ["signal", "until"],
|
|
36557
|
-
description: "signal, until: the node. Needed for a signal
|
|
36695
|
+
ops: ["signal", "watch", "until"],
|
|
36696
|
+
description: "signal, watch, until: the node. Needed for a signal without a watch, a watch and a property; for says it defaults to /root, the whole screen."
|
|
36697
|
+
},
|
|
36698
|
+
signal: { type: "string", ops: ["signal", "watch"], description: "signal, watch: the signal name." },
|
|
36699
|
+
watch: {
|
|
36700
|
+
type: "integer",
|
|
36701
|
+
minimum: 1,
|
|
36702
|
+
ops: ["signal"],
|
|
36703
|
+
description: "signal: the watch an earlier watch answered, in place of nodePath and signal. Answers at once, with fired_before_wait, when the signal fired after the watch was armed, and otherwise waits for it; times counts how often it fired, and args is what the first carried. The watch goes with the answer."
|
|
36558
36704
|
},
|
|
36559
|
-
signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
|
|
36560
36705
|
property: {
|
|
36561
36706
|
type: "string",
|
|
36562
36707
|
ops: ["until"],
|
|
@@ -36596,6 +36741,11 @@ var TOOL_SPECS = [
|
|
|
36596
36741
|
signal: {
|
|
36597
36742
|
effect: "read",
|
|
36598
36743
|
summary: "wait for a signal and answer with what it carried",
|
|
36744
|
+
requires: []
|
|
36745
|
+
},
|
|
36746
|
+
watch: {
|
|
36747
|
+
effect: "read",
|
|
36748
|
+
summary: "start recording a signal and answer at once with a watch; arm it before the call meant to set the signal off, then collect it with signal and the watch. A client can run a call sent beside a wait only after the wait answers, so a wait sent with the call misses it",
|
|
36599
36749
|
requires: ["nodePath", "signal"]
|
|
36600
36750
|
},
|
|
36601
36751
|
until: {
|
|
@@ -36981,6 +37131,8 @@ function argumentShape(schema) {
|
|
|
36981
37131
|
// src/server.ts
|
|
36982
37132
|
var UPDATE_NOTICE_EVERY = 500;
|
|
36983
37133
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
37134
|
+
var FAILED_BUDGET = 1e6;
|
|
37135
|
+
var ANSWER_BUDGET = 4000000;
|
|
36984
37136
|
var __dirname2 = dirname18(fileURLToPath4(import.meta.url));
|
|
36985
37137
|
var PARENT_ASKED_AFTER_MS = 5000;
|
|
36986
37138
|
function editorRestartTimeoutMs() {
|
|
@@ -37249,6 +37401,12 @@ function endedWithoutACode(run) {
|
|
|
37249
37401
|
}
|
|
37250
37402
|
return `The editor stopped playing this run. ${kept} What it printed is below.`;
|
|
37251
37403
|
}
|
|
37404
|
+
function ourRunEnded(run, now) {
|
|
37405
|
+
const which = `${run.throughEditor ? "played in the editor" : "started"}${run.pid === null ? "" : ` (pid ${run.pid})`}`;
|
|
37406
|
+
const when = run.endedAt === undefined ? "" : ` ${Math.max(0, Math.round((now - run.endedAt) / 1000))}s ago`;
|
|
37407
|
+
const how = run.endedThrough === "close" ? "this server asked it to close" : run.endedThrough === "keeper" ? "this server ended it through the keeper holding it" : run.endedThrough === "signal" ? "this server signalled it" : run.endedThrough === "editor" ? "this server asked the editor to stop it" : run.exitCode !== null ? `it exited by itself with code ${run.exitCode}` : run.exitSignal === null ? "it is no longer running" : `it was ended by ${run.exitSignal}`;
|
|
37408
|
+
return `No game with the runtime addon is running: the game this server ${which} ended${when}, and ${how}. editor_output has what it printed, and editor_run start runs it again.`;
|
|
37409
|
+
}
|
|
37252
37410
|
function stopVerdict(stop) {
|
|
37253
37411
|
const notClosed = stop.notClosedBecause === undefined ? "" : ` It was asked to close first and was not ended by that: ${stop.notClosedBecause}.`;
|
|
37254
37412
|
if (!stop.wasRunning) {
|
|
@@ -38252,6 +38410,13 @@ function scriptsToDiagnose(projectPath, given) {
|
|
|
38252
38410
|
}
|
|
38253
38411
|
return { ok: true, value: unique };
|
|
38254
38412
|
}
|
|
38413
|
+
function engineChecked(why) {
|
|
38414
|
+
return {
|
|
38415
|
+
checkedBy: "engine",
|
|
38416
|
+
languageServer: why,
|
|
38417
|
+
note: "No editor of this project is open to ask, so a headless engine compiled the scripts the way loading them does. It reports every error that stops a script compiling, at its line and without a column, and no warnings, which the engine tells only an editor: warnings is null rather than 0. editor_launch with hidden: true opens this project's editor without a window, and its language server reports both."
|
|
38418
|
+
};
|
|
38419
|
+
}
|
|
38255
38420
|
var EXTERNAL_MEMBER = 'Could not resolve external class member "';
|
|
38256
38421
|
function isDirectory(path) {
|
|
38257
38422
|
try {
|
|
@@ -38340,6 +38505,7 @@ class GodotServer {
|
|
|
38340
38505
|
inFlight = new Map;
|
|
38341
38506
|
callsOnGames = new Set;
|
|
38342
38507
|
callsTaken = 0;
|
|
38508
|
+
answersDirectory;
|
|
38343
38509
|
clientNoted = null;
|
|
38344
38510
|
parentCommand = null;
|
|
38345
38511
|
endNoted = false;
|
|
@@ -38738,12 +38904,66 @@ class GodotServer {
|
|
|
38738
38904
|
this.inFlight.set(call, { tool: spec.name, op: checked.op ?? "", startedAt: Date.now() });
|
|
38739
38905
|
try {
|
|
38740
38906
|
const answer = await withCallSignal(ctx.mcpReq.signal, () => this.answered(spec.name, checked.op ?? "", args), progress);
|
|
38741
|
-
return this.withFeedbackNotice(this.withUpdateNotice(answer));
|
|
38907
|
+
return this.withinTheClientsLimit(spec.name, this.withFeedbackNotice(this.withUpdateNotice(answer)));
|
|
38742
38908
|
} finally {
|
|
38743
38909
|
this.inFlight.delete(call);
|
|
38744
38910
|
}
|
|
38745
38911
|
});
|
|
38746
38912
|
}
|
|
38913
|
+
withinTheClientsLimit(tool, answer) {
|
|
38914
|
+
const sizeOf = (item) => item.type === "text" ? Buffer.byteLength(item.text) : item.data.length;
|
|
38915
|
+
const bytes = answer.content.reduce((sum, item) => sum + sizeOf(item), 0);
|
|
38916
|
+
if (bytes <= ANSWER_BUDGET) {
|
|
38917
|
+
return answer;
|
|
38918
|
+
}
|
|
38919
|
+
this.answersDirectory ??= scratchDirectory("answers");
|
|
38920
|
+
const directory = this.answersDirectory;
|
|
38921
|
+
const stem = `${tool}-${Date.now()}`;
|
|
38922
|
+
let savedAt;
|
|
38923
|
+
let notSaved;
|
|
38924
|
+
try {
|
|
38925
|
+
savedAt = answer.content.map((item, index) => {
|
|
38926
|
+
const file = join26(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
|
|
38927
|
+
writeFileSync16(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
|
|
38928
|
+
return file;
|
|
38929
|
+
});
|
|
38930
|
+
} catch (error) {
|
|
38931
|
+
notSaved = errorMessage(error);
|
|
38932
|
+
}
|
|
38933
|
+
const first = answer.content[0];
|
|
38934
|
+
let parsed = null;
|
|
38935
|
+
try {
|
|
38936
|
+
parsed = first?.type === "text" ? JSON.parse(first.text) : null;
|
|
38937
|
+
} catch {}
|
|
38938
|
+
const small = {};
|
|
38939
|
+
const leftOut = [];
|
|
38940
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
38941
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
38942
|
+
if (JSON.stringify(value).length <= 2000) {
|
|
38943
|
+
small[key] = value;
|
|
38944
|
+
} else {
|
|
38945
|
+
leftOut.push(key);
|
|
38946
|
+
}
|
|
38947
|
+
}
|
|
38948
|
+
}
|
|
38949
|
+
return {
|
|
38950
|
+
...answer,
|
|
38951
|
+
content: [
|
|
38952
|
+
{
|
|
38953
|
+
type: "text",
|
|
38954
|
+
text: answerJson({
|
|
38955
|
+
...small,
|
|
38956
|
+
answerTooLarge: {
|
|
38957
|
+
bytes,
|
|
38958
|
+
savedAt,
|
|
38959
|
+
leftOut: leftOut.length > 0 ? leftOut : undefined,
|
|
38960
|
+
note: savedAt === undefined ? `The answer was ${bytes} bytes, past the ${ANSWER_BUDGET} this server sends in one message (Claude Code closes the connection past 16 MB), and writing it to a file failed: ${notSaved ?? "no reason given"}. The fields of its top level that are short are kept here.` : `The answer was ${bytes} bytes, past the ${ANSWER_BUDGET} this server sends in one message (Claude Code closes the connection past 16 MB), so it was written whole to savedAt; read it from there. The fields of its top level that are short are kept here. The files are removed once this server has stopped.`
|
|
38961
|
+
}
|
|
38962
|
+
})
|
|
38963
|
+
}
|
|
38964
|
+
]
|
|
38965
|
+
};
|
|
38966
|
+
}
|
|
38747
38967
|
async answered(tool, op, args) {
|
|
38748
38968
|
try {
|
|
38749
38969
|
return await this.dispatch(tool, op, args);
|
|
@@ -38995,6 +39215,7 @@ class GodotServer {
|
|
|
38995
39215
|
const contained = this.containProjectFiles({ ...arguments_, ...extra });
|
|
38996
39216
|
return contained.ok ? await this.handleViaBridge(command, contained.value, op === "" ? tool : `${tool} ${op}`) : contained.response;
|
|
38997
39217
|
};
|
|
39218
|
+
const editorOrEngine = async (command, effect, extra = {}) => await this.editorOrEngine(command, { ...arguments_, ...extra }, `${tool} ${op}`, effect);
|
|
38998
39219
|
switch (tool) {
|
|
38999
39220
|
case "project_info":
|
|
39000
39221
|
return await this.handleProjectInfo(args);
|
|
@@ -39007,7 +39228,7 @@ class GodotServer {
|
|
|
39007
39228
|
case "project_test":
|
|
39008
39229
|
return await this.handleRunTests(args);
|
|
39009
39230
|
case "scene_create":
|
|
39010
|
-
return op === "create" ? await
|
|
39231
|
+
return op === "create" ? await editorOrEngine("create_scene", "writes") : await editorOrEngine("save_scene", "writes");
|
|
39011
39232
|
case "scene_tree":
|
|
39012
39233
|
return await this.readFromFile(args, "scenePath", "read_scene_tree", (file) => ({
|
|
39013
39234
|
scenePath: file,
|
|
@@ -39017,28 +39238,28 @@ class GodotServer {
|
|
|
39017
39238
|
case "scene_node":
|
|
39018
39239
|
switch (op) {
|
|
39019
39240
|
case "add":
|
|
39020
|
-
return await
|
|
39241
|
+
return await editorOrEngine("add_node", "writes");
|
|
39021
39242
|
case "get":
|
|
39022
|
-
return await
|
|
39243
|
+
return await editorOrEngine("get_node_properties", "reads");
|
|
39023
39244
|
case "set":
|
|
39024
|
-
return await
|
|
39245
|
+
return await editorOrEngine("set_node_properties", "writes");
|
|
39025
39246
|
case "duplicate":
|
|
39026
|
-
return await
|
|
39247
|
+
return await editorOrEngine("duplicate_node", "writes", { parentPath: args["parentNodePath"] });
|
|
39027
39248
|
case "reparent":
|
|
39028
|
-
return await
|
|
39249
|
+
return await editorOrEngine("reparent_node", "writes");
|
|
39029
39250
|
case "delete":
|
|
39030
|
-
return await
|
|
39251
|
+
return await editorOrEngine("delete_node", "writes");
|
|
39031
39252
|
default:
|
|
39032
|
-
return await
|
|
39253
|
+
return await editorOrEngine("set_tilemap_cells", "writes", { tilemapNodePath: args["nodePath"] });
|
|
39033
39254
|
}
|
|
39034
39255
|
case "scene_signal":
|
|
39035
39256
|
switch (op) {
|
|
39036
39257
|
case "connect":
|
|
39037
|
-
return await
|
|
39258
|
+
return await editorOrEngine("connect_signal", "writes");
|
|
39038
39259
|
case "disconnect":
|
|
39039
|
-
return await
|
|
39260
|
+
return await editorOrEngine("disconnect_signal", "writes");
|
|
39040
39261
|
default:
|
|
39041
|
-
return await
|
|
39262
|
+
return await editorOrEngine("list_connections", "reads");
|
|
39042
39263
|
}
|
|
39043
39264
|
case "scene_animation":
|
|
39044
39265
|
switch (op) {
|
|
@@ -39089,17 +39310,17 @@ class GodotServer {
|
|
|
39089
39310
|
}), async () => await bridge("read_resource", kept));
|
|
39090
39311
|
}
|
|
39091
39312
|
case "create":
|
|
39092
|
-
return await
|
|
39313
|
+
return await editorOrEngine("create_resource", "writes");
|
|
39093
39314
|
case "modify":
|
|
39094
|
-
return await
|
|
39315
|
+
return await editorOrEngine("modify_resource", "writes");
|
|
39095
39316
|
case "create_shader":
|
|
39096
|
-
return await
|
|
39317
|
+
return await editorOrEngine("create_shader", "writes", { shaderPath: args["resourcePath"] });
|
|
39097
39318
|
case "create_tileset":
|
|
39098
|
-
return await
|
|
39319
|
+
return await editorOrEngine("create_tileset", "writes", { tilesetPath: args["resourcePath"] });
|
|
39099
39320
|
case "set_theme_color":
|
|
39100
|
-
return await
|
|
39321
|
+
return await editorOrEngine("set_theme_color", "writes", { themePath: args["resourcePath"] });
|
|
39101
39322
|
default:
|
|
39102
|
-
return await
|
|
39323
|
+
return await editorOrEngine("set_theme_font_size", "writes", { themePath: args["resourcePath"] });
|
|
39103
39324
|
}
|
|
39104
39325
|
case "editor_launch":
|
|
39105
39326
|
if (op === "close") {
|
|
@@ -39965,6 +40186,10 @@ class GodotServer {
|
|
|
39965
40186
|
if (unknown.length > 0) {
|
|
39966
40187
|
return this.createErrorResponse(`project_info cannot include ${unknown.join(", ")}. Sections: ${Object.keys(PROJECT_INFO_SECTIONS2).join(", ")}.`);
|
|
39967
40188
|
}
|
|
40189
|
+
const forValidation = ["preset", "detail"].filter((name) => args[name] !== undefined);
|
|
40190
|
+
if (forValidation.length > 0 && !include.includes("validation")) {
|
|
40191
|
+
return this.createErrorResponse(`${forValidation.join(" and ")} ${forValidation.length === 1 ? "is" : "are"} read only by the validation section, which this call does not include: add "validation" to include, or leave ${forValidation.length === 1 ? "it" : "them"} out.`);
|
|
40192
|
+
}
|
|
39968
40193
|
const project = this.project(args);
|
|
39969
40194
|
if (!project.ok) {
|
|
39970
40195
|
return project.response;
|
|
@@ -40349,7 +40574,7 @@ class GodotServer {
|
|
|
40349
40574
|
isError: true
|
|
40350
40575
|
};
|
|
40351
40576
|
}
|
|
40352
|
-
const failed = failedCases(report, Buffer.concat(console2).toString("utf8"));
|
|
40577
|
+
const { failed, briefed } = failedWithin(failedCases(report, Buffer.concat(console2).toString("utf8")), FAILED_BUDGET);
|
|
40353
40578
|
const orphans = orphansPrinted(said);
|
|
40354
40579
|
const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
|
|
40355
40580
|
const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
|
|
@@ -40389,6 +40614,7 @@ class GodotServer {
|
|
|
40389
40614
|
skipped: report.skipped,
|
|
40390
40615
|
time: report.time,
|
|
40391
40616
|
failed,
|
|
40617
|
+
failedWithoutDetail: briefed > 0 ? briefed : undefined,
|
|
40392
40618
|
hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
|
|
40393
40619
|
uncaughtScriptErrors: uncaughtFolded.length > 0 ? uncaughtFolded : undefined,
|
|
40394
40620
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
@@ -40400,6 +40626,7 @@ class GodotServer {
|
|
|
40400
40626
|
...scanned,
|
|
40401
40627
|
note: [
|
|
40402
40628
|
missingNote,
|
|
40629
|
+
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,
|
|
40403
40630
|
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,
|
|
40404
40631
|
uncaughtNote(uncaught),
|
|
40405
40632
|
ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
|
|
@@ -40442,6 +40669,10 @@ class GodotServer {
|
|
|
40442
40669
|
}
|
|
40443
40670
|
if (scripts?.value == null) {
|
|
40444
40671
|
const one = await this.diagnosticsOf(args);
|
|
40672
|
+
if ("noLanguageServer" in one) {
|
|
40673
|
+
const checked = project.ok ? await this.engineDiagnostics(project.value.path, [readString(args, "scriptPath") ?? ""], one.noLanguageServer) : { refusal: project.response };
|
|
40674
|
+
return "refusal" in checked ? checked.refusal : this.jsonTextResponse({ ...checked.answers[0], ...engineChecked(one.noLanguageServer.reason) });
|
|
40675
|
+
}
|
|
40445
40676
|
return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
|
|
40446
40677
|
}
|
|
40447
40678
|
const progress = callProgress();
|
|
@@ -40451,12 +40682,31 @@ class GodotServer {
|
|
|
40451
40682
|
let errors = 0;
|
|
40452
40683
|
let warnings = 0;
|
|
40453
40684
|
let unread = 0;
|
|
40685
|
+
let byEngine = null;
|
|
40454
40686
|
for (const [at, scriptPath] of scripts.value.entries()) {
|
|
40455
40687
|
if (cancelled?.aborted === true) {
|
|
40456
40688
|
return this.createErrorResponse(`Cancelled after ${at} of ${scripts.value.length} scripts.`);
|
|
40457
40689
|
}
|
|
40458
40690
|
progress?.(at + 1, `${at + 1} of ${scripts.value.length}: ${scriptPath}`);
|
|
40459
40691
|
const one = await this.diagnosticsOf({ ...args, scriptPath });
|
|
40692
|
+
if ("noLanguageServer" in one) {
|
|
40693
|
+
const rest = scripts.value.slice(at);
|
|
40694
|
+
progress?.(at + 1, `${rest.length} script(s) left, compiled by a headless engine`);
|
|
40695
|
+
const checked = await this.engineDiagnostics(project.ok ? project.value.path : "", rest, one.noLanguageServer);
|
|
40696
|
+
if ("refusal" in checked) {
|
|
40697
|
+
return checked.refusal;
|
|
40698
|
+
}
|
|
40699
|
+
for (const answer of checked.answers) {
|
|
40700
|
+
errors += Number(answer["errors"]);
|
|
40701
|
+
if (readArray(answer, "diagnostics")?.length === 0) {
|
|
40702
|
+
nothingReported.push(String(answer["scriptPath"]));
|
|
40703
|
+
} else {
|
|
40704
|
+
reported.push(answer);
|
|
40705
|
+
}
|
|
40706
|
+
}
|
|
40707
|
+
byEngine = one.noLanguageServer.reason;
|
|
40708
|
+
break;
|
|
40709
|
+
}
|
|
40460
40710
|
if ("refusal" in one) {
|
|
40461
40711
|
if (!one.perScript) {
|
|
40462
40712
|
return one.refusal;
|
|
@@ -40477,14 +40727,74 @@ class GodotServer {
|
|
|
40477
40727
|
clean: errors === 0 && unread === 0,
|
|
40478
40728
|
scriptsChecked: scripts.value.length,
|
|
40479
40729
|
errors,
|
|
40480
|
-
warnings,
|
|
40730
|
+
warnings: byEngine === null ? warnings : null,
|
|
40481
40731
|
...unread > 0 ? { scriptsNotRead: unread } : {},
|
|
40482
40732
|
scripts: reported,
|
|
40483
|
-
nothingReported
|
|
40733
|
+
nothingReported,
|
|
40734
|
+
...byEngine === null ? {} : engineChecked(byEngine)
|
|
40484
40735
|
}));
|
|
40485
40736
|
}
|
|
40737
|
+
async compileScripts(projectPath, paths) {
|
|
40738
|
+
const payload = { checked: [], failed: [], scripts: [] };
|
|
40739
|
+
const messages = [];
|
|
40740
|
+
let left = [...paths];
|
|
40741
|
+
while (left.length > 0) {
|
|
40742
|
+
const ran = await this.operation("check_scripts", { paths: left }, projectPath);
|
|
40743
|
+
if (!ran.ok) {
|
|
40744
|
+
return { ok: false, message: ran.message };
|
|
40745
|
+
}
|
|
40746
|
+
const checked = (readArray(ran.payload, "checked") ?? []).map(String);
|
|
40747
|
+
if (checked.length === 0) {
|
|
40748
|
+
return { ok: false, message: `check_scripts read none of ${left.length} script(s)` };
|
|
40749
|
+
}
|
|
40750
|
+
payload.checked.push(...checked);
|
|
40751
|
+
payload.failed.push(...(readArray(ran.payload, "failed") ?? []).map(String));
|
|
40752
|
+
payload.scripts.push(...readArray(ran.payload, "scripts") ?? []);
|
|
40753
|
+
messages.push(...ran.messages);
|
|
40754
|
+
left = left.slice(checked.length);
|
|
40755
|
+
}
|
|
40756
|
+
return { ok: true, payload, extras: engineExtras({ messages }) };
|
|
40757
|
+
}
|
|
40758
|
+
async engineDiagnostics(projectPath, scripts, why) {
|
|
40759
|
+
const named = scripts.map((script) => {
|
|
40760
|
+
const contained = resolveWithinProject(projectPath, script);
|
|
40761
|
+
return contained.ok ? `res://${contained.relativePath}` : script;
|
|
40762
|
+
});
|
|
40763
|
+
const checked = await this.compileScripts(projectPath, named);
|
|
40764
|
+
if (!checked.ok) {
|
|
40765
|
+
return {
|
|
40766
|
+
refusal: this.createErrorResponse(`Diagnostics unavailable: ${why.reason.replace(/\.?$/, ".")} No headless engine could compile the scripts instead: ${checked.message}`, why.solutions)
|
|
40767
|
+
};
|
|
40768
|
+
}
|
|
40769
|
+
const failures = new Map((readArray(checked.payload, "scripts") ?? []).map((entry) => [
|
|
40770
|
+
String(asParams(entry)["path"]),
|
|
40771
|
+
asParams(entry)
|
|
40772
|
+
]));
|
|
40773
|
+
return {
|
|
40774
|
+
answers: scripts.map((scriptPath, at) => {
|
|
40775
|
+
const failure = failures.get(named[at] ?? "");
|
|
40776
|
+
const diagnostics = (readArray(failure ?? {}, "errors") ?? []).map((entry) => {
|
|
40777
|
+
const { line, message } = asParams(entry);
|
|
40778
|
+
return { severity: "error", message, ...typeof line === "number" && line > 0 ? { line } : {} };
|
|
40779
|
+
});
|
|
40780
|
+
const dependencies = readArray(failure ?? {}, "dependencies_with_errors");
|
|
40781
|
+
return {
|
|
40782
|
+
scriptPath,
|
|
40783
|
+
clean: diagnostics.length === 0,
|
|
40784
|
+
errors: diagnostics.length,
|
|
40785
|
+
warnings: null,
|
|
40786
|
+
diagnostics,
|
|
40787
|
+
...dependencies === undefined ? {} : { dependenciesWithErrors: dependencies }
|
|
40788
|
+
};
|
|
40789
|
+
})
|
|
40790
|
+
};
|
|
40791
|
+
}
|
|
40486
40792
|
markedIfStale(answer) {
|
|
40487
|
-
|
|
40793
|
+
const status = this.godotBridge.getStatus();
|
|
40794
|
+
if (!hasSaidWhoItIs(status)) {
|
|
40795
|
+
return answer;
|
|
40796
|
+
}
|
|
40797
|
+
return markIfStale(answer, status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
|
|
40488
40798
|
}
|
|
40489
40799
|
async diagnosticsOf(args) {
|
|
40490
40800
|
const waited = await this.waitForTheEditorOpenedHere(args, []);
|
|
@@ -40494,6 +40804,16 @@ class GodotServer {
|
|
|
40494
40804
|
const answer = await this.handleLSP("lsp_get_diagnostics", args);
|
|
40495
40805
|
const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
40496
40806
|
const refused = this.languageServerRefusal(payload, "Diagnostics");
|
|
40807
|
+
const project = this.project(args);
|
|
40808
|
+
const ownEditor = project.ok && this.editorHasOpen(project.value.path);
|
|
40809
|
+
if (refused !== null && !refused.perScript && payload["stillReading"] !== true && !ownEditor) {
|
|
40810
|
+
return {
|
|
40811
|
+
noLanguageServer: {
|
|
40812
|
+
reason: typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]),
|
|
40813
|
+
solutions: this.languageServerSolutions(payload)
|
|
40814
|
+
}
|
|
40815
|
+
};
|
|
40816
|
+
}
|
|
40497
40817
|
if (refused !== null) {
|
|
40498
40818
|
return refused;
|
|
40499
40819
|
}
|
|
@@ -40653,24 +40973,24 @@ class GodotServer {
|
|
|
40653
40973
|
if (payload["stillReading"] === true) {
|
|
40654
40974
|
return { refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`), perScript: false };
|
|
40655
40975
|
}
|
|
40656
|
-
if (typeof payload["serves"] === "string") {
|
|
40657
|
-
return {
|
|
40658
|
-
refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
|
|
40659
|
-
"editor_status names the editor on the bridge and the port it says it serves",
|
|
40660
|
-
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
40661
|
-
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
40662
|
-
]),
|
|
40663
|
-
perScript: false
|
|
40664
|
-
};
|
|
40665
|
-
}
|
|
40666
40976
|
return {
|
|
40667
|
-
refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`,
|
|
40668
|
-
|
|
40669
|
-
"GDHARNESS_LSP_PORT points this server at another one"
|
|
40670
|
-
]),
|
|
40671
|
-
perScript: payload["aboutThisFile"] === true
|
|
40977
|
+
refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, this.languageServerSolutions(payload)),
|
|
40978
|
+
perScript: typeof payload["serves"] !== "string" && payload["aboutThisFile"] === true
|
|
40672
40979
|
};
|
|
40673
40980
|
}
|
|
40981
|
+
languageServerSolutions(payload) {
|
|
40982
|
+
if (typeof payload["serves"] === "string") {
|
|
40983
|
+
return [
|
|
40984
|
+
...this.godotBridge.getStatus().connected ? ["editor_status names the editor on the bridge and the port it says it serves"] : [],
|
|
40985
|
+
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
40986
|
+
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
40987
|
+
];
|
|
40988
|
+
}
|
|
40989
|
+
return [
|
|
40990
|
+
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
40991
|
+
"GDHARNESS_LSP_PORT points this server at another one"
|
|
40992
|
+
];
|
|
40993
|
+
}
|
|
40674
40994
|
async lsp() {
|
|
40675
40995
|
const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
|
|
40676
40996
|
if (this.lspClient !== null && this.lspClient.port !== port) {
|
|
@@ -41295,7 +41615,7 @@ class GodotServer {
|
|
|
41295
41615
|
return;
|
|
41296
41616
|
}
|
|
41297
41617
|
const connected = this.godotBridge.isConnected() ? this.godotBridge.getStatus().editorPid : undefined;
|
|
41298
|
-
const pids = editorsOfProjectIn(tree, projectPath).filter((pid) => pid !== connected);
|
|
41618
|
+
const pids = editorsOfProjectIn(tree, projectPath).filter((pid) => pid !== connected && alive(pid));
|
|
41299
41619
|
const started = await startTimesOf(pids);
|
|
41300
41620
|
return pids.map((pid) => {
|
|
41301
41621
|
const at = started.get(pid) ?? tree.get(pid)?.startedAt;
|
|
@@ -43534,6 +43854,45 @@ class GodotServer {
|
|
|
43534
43854
|
...engineExtras(outcome)
|
|
43535
43855
|
});
|
|
43536
43856
|
}
|
|
43857
|
+
async editorOrEngine(command, args, askedAs, effect) {
|
|
43858
|
+
const contained = this.containProjectFiles(args);
|
|
43859
|
+
if (!contained.ok) {
|
|
43860
|
+
return contained.response;
|
|
43861
|
+
}
|
|
43862
|
+
const project = this.project(contained.value);
|
|
43863
|
+
if (!project.ok) {
|
|
43864
|
+
return project.response;
|
|
43865
|
+
}
|
|
43866
|
+
const projectPath = project.value.path;
|
|
43867
|
+
const waited = await this.waitForTheEditorOpenedHere(contained.value, []);
|
|
43868
|
+
if (this.editorHasOpen(projectPath)) {
|
|
43869
|
+
return await this.handleViaBridge(command, contained.value, askedAs);
|
|
43870
|
+
}
|
|
43871
|
+
if (effect === "writes") {
|
|
43872
|
+
if (waited !== null) {
|
|
43873
|
+
return waited;
|
|
43874
|
+
}
|
|
43875
|
+
const arriving = await this.editorsNotYetConnected(projectPath);
|
|
43876
|
+
if (arriving !== undefined && arriving.length > 0) {
|
|
43877
|
+
const named = arriving.map((one) => `pid ${one.pid}`).join(", ");
|
|
43878
|
+
return this.createErrorResponse(`${askedAs} was not run: an editor of this project is running (${named}) and has not reached this server, and saving a scene or resource it holds would put back what a write made around it.`, [
|
|
43879
|
+
"editor_status names it under editorsNotYetConnected and says when it has connected; the write then goes through it",
|
|
43880
|
+
`If it never connects it is not reaching this server: end ${arriving.length === 1 ? "that pid" : "those pids"} and ask again`
|
|
43881
|
+
]);
|
|
43882
|
+
}
|
|
43883
|
+
}
|
|
43884
|
+
const outcome = await this.operation("run_editor_tool", { command, arguments: contained.value }, projectPath);
|
|
43885
|
+
if (!outcome.ok) {
|
|
43886
|
+
return this.answer(outcome);
|
|
43887
|
+
}
|
|
43888
|
+
return this.jsonTextResponse({
|
|
43889
|
+
...outcome.payload,
|
|
43890
|
+
...effect === "reads" ? {
|
|
43891
|
+
readBy: "a headless engine, since no editor serves this project; unsaved changes in an editor are not seen"
|
|
43892
|
+
} : { writtenBy: "a headless engine, since no editor serves this project" },
|
|
43893
|
+
...engineExtras(outcome)
|
|
43894
|
+
});
|
|
43895
|
+
}
|
|
43537
43896
|
async handleMoveFile(given) {
|
|
43538
43897
|
const project = this.project(given);
|
|
43539
43898
|
if (!project.ok) {
|
|
@@ -43724,9 +44083,9 @@ class GodotServer {
|
|
|
43724
44083
|
ok &&= rebuilt.ok;
|
|
43725
44084
|
}
|
|
43726
44085
|
if (changed.length > 0) {
|
|
43727
|
-
const compiled = await this.
|
|
43728
|
-
report["compiled"] = compiled.ok ? { ...compiled.payload, ...
|
|
43729
|
-
ok &&= compiled.ok &&
|
|
44086
|
+
const compiled = await this.compileScripts(projectPath, [...changed]);
|
|
44087
|
+
report["compiled"] = compiled.ok ? { ...compiled.payload, ...compiled.extras } : { problem: compiled.message };
|
|
44088
|
+
ok &&= compiled.ok && compiled.payload.failed.length === 0;
|
|
43730
44089
|
}
|
|
43731
44090
|
return { ok, report };
|
|
43732
44091
|
}
|
|
@@ -43992,6 +44351,11 @@ class GodotServer {
|
|
|
43992
44351
|
const own = typeof pid === "number" ? undefined : await this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
|
|
43993
44352
|
const choice = chooseRuntime(announced.running, wanted, announced.unspoken, typeof pid === "number" ? pid : own);
|
|
43994
44353
|
if ("problem" in choice) {
|
|
44354
|
+
const ended = this.activeProcess;
|
|
44355
|
+
const nothingAnnounced = typeof pid !== "number" && announced.unspoken.length === 0 && !announced.running.some((one) => wanted === undefined || isSameDirectory(one.project.path, wanted));
|
|
44356
|
+
if (nothingAnnounced && ended !== null && !stillRunning(ended) && (wanted === undefined || ended.projectPath === null || isSameDirectory(ended.projectPath, wanted))) {
|
|
44357
|
+
return this.createErrorResponse(ourRunEnded({ ...ended, pid: ended.pid ?? ended.announcedPid ?? null }, Date.now()));
|
|
44358
|
+
}
|
|
43995
44359
|
return this.createErrorResponse(choice.problem);
|
|
43996
44360
|
}
|
|
43997
44361
|
const ofProject = announced.running.filter((one) => wanted === undefined || isSameDirectory(one.project.path, wanted));
|
|
@@ -44157,7 +44521,18 @@ ${staleNote}`}`
|
|
|
44157
44521
|
const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
|
|
44158
44522
|
return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, { tool: "runtime_wait", op, patience: waited });
|
|
44159
44523
|
}
|
|
44524
|
+
if (op === "watch") {
|
|
44525
|
+
return await this.handleRuntimeCommand("watch_signal", {
|
|
44526
|
+
...whichGame(args),
|
|
44527
|
+
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
44528
|
+
signal: readString(args, "signal") ?? ""
|
|
44529
|
+
}, { tool: "runtime_wait", op });
|
|
44530
|
+
}
|
|
44160
44531
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
|
|
44532
|
+
const watch = readPositiveNumber(args, "watch");
|
|
44533
|
+
if (op === "signal" && watch === undefined && (readNonEmptyString(args, "nodePath") === undefined || readNonEmptyString(args, "signal") === undefined)) {
|
|
44534
|
+
return this.createErrorResponse("runtime_wait signal needs nodePath and signal, or the watch an earlier runtime_wait watch answered.");
|
|
44535
|
+
}
|
|
44161
44536
|
const asked = readNonEmptyString(args, "nodePath");
|
|
44162
44537
|
if (asked === undefined && op === "until" && readNonEmptyString(args, "says") === undefined) {
|
|
44163
44538
|
return this.createErrorResponse("runtime_wait until needs nodePath to wait for a property: the property belongs to that node.", ["With says instead of property, nodePath can be left out and the whole game is searched"]);
|
|
@@ -44168,7 +44543,8 @@ ${staleNote}`}`
|
|
|
44168
44543
|
...whichGame(args),
|
|
44169
44544
|
path: nodePath,
|
|
44170
44545
|
signal: readString(args, "signal") ?? "",
|
|
44171
|
-
timeout_ms: timeoutMs
|
|
44546
|
+
timeout_ms: timeoutMs,
|
|
44547
|
+
...watch === undefined ? {} : { watch }
|
|
44172
44548
|
}, { tool: "runtime_wait", op, patience }) : await this.handleRuntimeCommand("wait_until", {
|
|
44173
44549
|
...whichGame(args),
|
|
44174
44550
|
path: nodePath,
|