gdharness 1.3.62 → 1.3.64
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 +49 -9
- package/build/godot/addons/gdharness_editor/declared_values.gd +2 -1
- package/build/godot/addons/gdharness_editor/file_reads.gd +46 -0
- package/build/godot/addons/gdharness_editor/setting_reads.gd +3 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +6 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +43 -41
- package/build/godot/addons/gdharness_runtime/declared_values.gd +2 -1
- package/build/godot/operations/declared_values.gd +2 -1
- package/build/godot/operations/file_reads.gd +46 -0
- package/build/godot/operations/project_diagnostics.gd +2 -1
- package/build/godot/operations/setting_reads.gd +3 -3
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -18139,7 +18139,7 @@ function annotationsOf(spec) {
|
|
|
18139
18139
|
openWorldHint: false
|
|
18140
18140
|
};
|
|
18141
18141
|
}
|
|
18142
|
-
var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "The call goes through the editor when one serves the project. With none, a headless engine runs the editor's own tool
|
|
18142
|
+
var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "The call goes through the editor when one serves the project. With none, a headless engine runs the editor's own tool. The answer's writtenBy (readBy for a read) names the editor or the engine. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. Each file the written one refers to is named by its uid, as the editor names it, except a file the project's uid cache has no uid for yet (one made since the project was last scanned), which is named by path and listed under namedByPath with the call that scans it. A running editor of the project that has not yet reached this server is waited for.", ALSO_SAVED = "A scene is saved as the engine holds it once the file has loaded, and one property can set others as it loads, as anchors_preset gives a centred Control both its grow directions, so a scene not last saved by an editor can gain, change or lose lines the call did not ask for, on any node the file held, the one it names among them: alsoSaved lists each as {node, property, was, saved}, was left out where the file set nothing and saved where it now sets nothing. What a set itself moved on its node is under alsoChanged rather than here, and neither a node the call adds nor the format lines (header uid, load_steps, unique_id) are listed. The scene loads as it did. Only its text changed.", IMPORTED_FIRST = "importedFirst names the files whose import was behind them on disk, such as a picture changed since it was imported or one never imported, which were imported before the run through project_import reimport, so the run reads them as they are now. importedFirst is left out when every import was current. notImportedFirst lists, as project_import reimport does, any it could not import, and importNote says that the run reads those as they were. The import gives every script without a .uid sidecar one, as the editor's scan does, writing it beside the script in the project, and uidsWrittenFirst names those scripts. The files inside an addon are counted per addon rather than named, under importedInAddons and uidsWrittenInAddons. importTookMs is how long the import took. That time is outside the run's own time limit, so a call that imported can take that much longer than the limit it was given.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', EXPORT_PLATFORMS, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR2, TOOL_SPECS, SPECS_BY_NAME;
|
|
18143
18143
|
var init_tool_definitions = __esm(() => {
|
|
18144
18144
|
init_launch();
|
|
18145
18145
|
PROJECT_INFO_SECTIONS = [
|
|
@@ -18411,7 +18411,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18411
18411
|
prefix: {
|
|
18412
18412
|
type: "string",
|
|
18413
18413
|
ops: ["get"],
|
|
18414
|
-
description:
|
|
18414
|
+
description: `get: answer with every setting whose name starts with this, and the type the engine registers for each, a typed array or dictionary with what it holds as project.godot writes it (Dictionary[String, bool]), instead of one setting by name. "debug/gdscript/warnings/" answers the whole family. They come in the engine's order: its own settings as it registers them, then the project's as project.godot lists them, so autoloads come in the order they load. The type matters: a family of levels can hold a setting that is a bool, and writing a level over it looks like it worked.`
|
|
18415
18415
|
},
|
|
18416
18416
|
value: {
|
|
18417
18417
|
blank: true,
|
|
@@ -19349,7 +19349,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
19349
19349
|
{
|
|
19350
19350
|
name: "resource_edit",
|
|
19351
19351
|
title: "Edit resources",
|
|
19352
|
-
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`, so a style box, a font or a constant is set through modify by that name, as `Button/styles/normal` given the res:// path of a StyleBox. A Curve, Curve2D or Curve3D reads as the inspector shows it, point_count and point_0/position and the rest, rather than the _data list the file stores, and so does one built into a resource or a scene. create and modify answer each property given under properties as the file holds it once saved, in the same form. 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 answered under setByScript rather than alsoChanged, with setByScriptNote. A resource where that cannot be done is refused and the file left as it was. A read with none leaves out what such a script set as the file loaded.",
|
|
19352
|
+
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`, so a style box, a font or a constant is set through modify by that name, as `Button/styles/normal` given the res:// path of a StyleBox. A Curve, Curve2D or Curve3D reads as the inspector shows it, point_count and point_0/position and the rest, rather than the _data list the file stores, and so does one built into a resource or a scene. create and modify answer each property given under properties as the file holds it once saved, in the same form. 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. The engine saves only what it loaded, so a modify can drop a line it does not load, such as a shader parameter whose uniform was renamed. In a .tres file, alsoSaved lists each other property of the main resource that the save changed, added or dropped, as {property, was, saved} in the file's own text. was is left out for an added line and saved for a dropped one. Embedded resources and values that read the same are not listed. read of a shader says whether it compiles under compiles, with compileErrors as create_shader gives them. 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 answered under setByScript rather than alsoChanged, with setByScriptNote. A resource where that cannot be done is refused and the file left as it was. A read with none leaves out what such a script set as the file loaded.",
|
|
19353
19353
|
parameters: {
|
|
19354
19354
|
projectPath: PROJECT_PATH,
|
|
19355
19355
|
resourcePath: {
|
|
@@ -20254,7 +20254,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
20254
20254
|
name: "runtime_wait",
|
|
20255
20255
|
title: "Wait in the running game",
|
|
20256
20256
|
description: "Lets the running game get on with it and answers when something has happened: a number of frames, a signal, a property reaching a value, or words appearing on a screen. Records a signal or a property from before the call that sets it off, so a change over before the next call arrives is still seen. Needs the game running with the runtime addon.",
|
|
20257
|
-
answer: "recorded: changes, each with frame (process frames since the watch was armed, 0 the reading at arming: a process frame is no set time, and a headless game, drawing nothing, runs them far faster than the sixty physics ticks a second that _physics_process sees, so game_time is the clock), elapsed_ms (wall-clock milliseconds since the arming), game_time (the seconds of process time the game had been given between the arming and that frame, scaled by Engine.time_scale as a tween is, and like frame it counts from the watch, not from the start of the game) and value. frames and elapsed_ms say how long it recorded. calls lists each runtime_invoke set or call and runtime_input sent while it recorded, in order, with call (the tool and op), what it was given of path, method, property, action, keycode, button, pressed and says, and frame, the last reading taken before it ran, so what it changed shows from the next reading at the earliest. Two calls sent one after the other arrive frames apart, which frame tells apart from a delay in the game itself. calls_dropped counts any past 1000. dropped says so when more than 1000 changes came, and ended when recording stopped before it was collected: the node was freed, the path stopped reading, or 120000 ms passed.",
|
|
20257
|
+
answer: "until: met, and value, the last reading of the property when the wait answered, whatever the wait asked for. outside_declared, for == and !=, says the value waited for is not one the property declares. elapsed_ms and frames say how long it waited. " + "recorded: changes, each with frame (process frames since the watch was armed, 0 the reading at arming: a process frame is no set time, and a headless game, drawing nothing, runs them far faster than the sixty physics ticks a second that _physics_process sees, so game_time is the clock), elapsed_ms (wall-clock milliseconds since the arming), game_time (the seconds of process time the game had been given between the arming and that frame, scaled by Engine.time_scale as a tween is, and like frame it counts from the watch, not from the start of the game) and value. frames and elapsed_ms say how long it recorded. calls lists each runtime_invoke set or call and runtime_input sent while it recorded, in order, with call (the tool and op), what it was given of path, method, property, action, keycode, button, pressed and says, and frame, the last reading taken before it ran, so what it changed shows from the next reading at the earliest. Two calls sent one after the other arrive frames apart, which frame tells apart from a delay in the game itself. calls_dropped counts any past 1000. dropped says so when more than 1000 changes came, and ended when recording stopped before it was collected: the node was freed, the path stopped reading, or 120000 ms passed.",
|
|
20258
20258
|
parameters: {
|
|
20259
20259
|
projectPath: RUNNING_PROJECT_PATH,
|
|
20260
20260
|
pid: RUNNING_PID,
|
|
@@ -52755,6 +52755,23 @@ class GodotServer {
|
|
|
52755
52755
|
jsonTextResponse(payload) {
|
|
52756
52756
|
return { content: [{ type: "text", text: answerJson(payload) }] };
|
|
52757
52757
|
}
|
|
52758
|
+
byTheEditor(answered, effect) {
|
|
52759
|
+
const [first, ...rest] = answered.content;
|
|
52760
|
+
if (answered.isError === true || first?.type !== "text") {
|
|
52761
|
+
return answered;
|
|
52762
|
+
}
|
|
52763
|
+
let payload;
|
|
52764
|
+
try {
|
|
52765
|
+
payload = JSON.parse(first.text);
|
|
52766
|
+
} catch {
|
|
52767
|
+
return answered;
|
|
52768
|
+
}
|
|
52769
|
+
if (!isRecord(payload)) {
|
|
52770
|
+
return answered;
|
|
52771
|
+
}
|
|
52772
|
+
const said = effect === "reads" ? { readBy: "the editor" } : { writtenBy: "the editor" };
|
|
52773
|
+
return { ...answered, content: [{ type: "text", text: answerJson({ ...payload, ...said }) }, ...rest] };
|
|
52774
|
+
}
|
|
52758
52775
|
async run() {
|
|
52759
52776
|
this.noteTheStart();
|
|
52760
52777
|
const located = await this.locator.find();
|
|
@@ -59346,7 +59363,7 @@ ${error.text}`;
|
|
|
59346
59363
|
const projectPath = project.value.path;
|
|
59347
59364
|
await this.waitForTheEditorOpenedHere(args, []);
|
|
59348
59365
|
if (this.editorHasOpen(projectPath)) {
|
|
59349
|
-
return await throughTheEditor();
|
|
59366
|
+
return this.byTheEditor(await throughTheEditor(), "reads");
|
|
59350
59367
|
}
|
|
59351
59368
|
const where = resolveWithinProject(projectPath, readNonEmptyString(args, pathKey) ?? "");
|
|
59352
59369
|
if (!where.ok) {
|
|
@@ -59384,7 +59401,7 @@ ${error.text}`;
|
|
|
59384
59401
|
if (answered.isError === true && declaredButUncached(projectPath, typeName) !== null) {
|
|
59385
59402
|
answered.content.push({ type: "text", text: uncachedClassNote([typeName]) });
|
|
59386
59403
|
}
|
|
59387
|
-
return answered;
|
|
59404
|
+
return this.byTheEditor(answered, effect);
|
|
59388
59405
|
}
|
|
59389
59406
|
if (effect === "writes") {
|
|
59390
59407
|
if (waited !== null) {
|
|
@@ -59740,7 +59757,7 @@ ${error.text}`;
|
|
|
59740
59757
|
return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
|
|
59741
59758
|
...instead,
|
|
59742
59759
|
"Open the project in the editor with editor_launch, or by hand",
|
|
59743
|
-
"Enable the addon under Project > Project Settings > Plugins",
|
|
59760
|
+
...project === null ? ["Enable the addon under Project > Project Settings > Plugins"] : [],
|
|
59744
59761
|
"editor_status says whether the bridge is up and what it is listening on"
|
|
59745
59762
|
]);
|
|
59746
59763
|
}
|
|
@@ -62661,6 +62678,19 @@ async function registerRuntime(godot, projectPath) {
|
|
|
62661
62678
|
}
|
|
62662
62679
|
console.log(` ${useTheShippedLoader(RUNTIME_AUTOLOAD.name, path)}`);
|
|
62663
62680
|
}
|
|
62681
|
+
var COMMANDS = [
|
|
62682
|
+
"setup",
|
|
62683
|
+
"upgrade",
|
|
62684
|
+
"uninstall",
|
|
62685
|
+
"harnesses",
|
|
62686
|
+
"doctor",
|
|
62687
|
+
"runtime",
|
|
62688
|
+
"classes",
|
|
62689
|
+
"test",
|
|
62690
|
+
"lsp",
|
|
62691
|
+
"version",
|
|
62692
|
+
"help"
|
|
62693
|
+
];
|
|
62664
62694
|
var SETUP_FLAGS = new Set(["--runtime", "--no-runtime", "--no-connect", "--no-skill", "--json", "--yes"]);
|
|
62665
62695
|
function unknownOption(flag) {
|
|
62666
62696
|
const known = [...SETUP_FLAGS, ...HARNESSES.map((harness) => `--${harness.id}`)];
|
|
@@ -62859,6 +62889,13 @@ async function uninstall() {
|
|
|
62859
62889
|
for (const path of addons) {
|
|
62860
62890
|
console.log(`removed ${path}`);
|
|
62861
62891
|
}
|
|
62892
|
+
if (addons.length > 0) {
|
|
62893
|
+
const refreshed = await runOperation(godot, "refresh_class_cache", {}, projectPath);
|
|
62894
|
+
said2(refreshed, "rebuilding the class list");
|
|
62895
|
+
if (refreshed.ok) {
|
|
62896
|
+
console.log(`class list rebuilt: ${String(refreshed.payload["classes"])} classes`);
|
|
62897
|
+
}
|
|
62898
|
+
}
|
|
62862
62899
|
const anything = addons.length > 0 || before.runtimeAutoloadPath !== null || enabled.length > 0;
|
|
62863
62900
|
console.log(anything ? `
|
|
62864
62901
|
Reconnect your harness so it stops spawning a server that is no longer installed.` : `
|
|
@@ -63177,8 +63214,11 @@ async function main() {
|
|
|
63177
63214
|
case "-h":
|
|
63178
63215
|
printHelp();
|
|
63179
63216
|
return;
|
|
63180
|
-
default:
|
|
63181
|
-
|
|
63217
|
+
default: {
|
|
63218
|
+
const [close] = nearest(command, [...COMMANDS]);
|
|
63219
|
+
const offered = close === undefined ? "" : ` Did you mean ${close}?`;
|
|
63220
|
+
throw new UsageError(`Unknown command: ${command}.${offered} Run gdharness help.`);
|
|
63221
|
+
}
|
|
63182
63222
|
}
|
|
63183
63223
|
}
|
|
63184
63224
|
await main().catch((error) => {
|
|
@@ -140,8 +140,9 @@ static func _global_constant(target: Object, declared: Dictionary, word: String)
|
|
|
140
140
|
)
|
|
141
141
|
if constant != wanted:
|
|
142
142
|
continue
|
|
143
|
+
# Typed, because a project that makes untyped declarations errors compiles this line too.
|
|
143
144
|
var reader: GDScript = GDScript.new()
|
|
144
|
-
reader.source_code = "extends RefCounted\nconst READ = %s\n" % constant
|
|
145
|
+
reader.source_code = "extends RefCounted\nconst READ: int = %s\n" % constant
|
|
145
146
|
if (reader.reload() == OK
|
|
146
147
|
and reader.get_script_constant_map().get("READ") == hinted[value_word]):
|
|
147
148
|
return hinted[value_word]
|
|
@@ -610,9 +610,30 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
610
610
|
var declared_class: String = str(script.get_global_name())
|
|
611
611
|
if not declared_class.is_empty():
|
|
612
612
|
answer["scriptClass"] = declared_class
|
|
613
|
+
# A shader file edited by hand is otherwise checked only where it is first drawn with.
|
|
614
|
+
if loaded is Shader and file == res_path:
|
|
615
|
+
var errors: Array[Dictionary] = shader_errors(file)
|
|
616
|
+
answer["compiles"] = errors.is_empty()
|
|
617
|
+
if not errors.is_empty():
|
|
618
|
+
answer["compileErrors"] = errors
|
|
613
619
|
return answer
|
|
614
620
|
|
|
615
621
|
|
|
622
|
+
## The errors the engine raises compiling the shader at [param path], as {line, message} in order.
|
|
623
|
+
## A shader is compiled once something reads what it declares, and a create answering only the path
|
|
624
|
+
## left a missing semicolon to show up wherever the shader was first drawn with.
|
|
625
|
+
static func shader_errors(path: String) -> Array[Dictionary]:
|
|
626
|
+
var caught: GdhShaderErrors = GdhShaderErrors.new()
|
|
627
|
+
OS.add_logger(caught)
|
|
628
|
+
var shader: Shader = (
|
|
629
|
+
ResourceLoader.load(path, "Shader", ResourceLoader.CACHE_MODE_IGNORE) as Shader
|
|
630
|
+
)
|
|
631
|
+
if shader != null:
|
|
632
|
+
var _uniforms: Array = shader.get_shader_uniform_list()
|
|
633
|
+
OS.remove_logger(caught)
|
|
634
|
+
return caught.raised
|
|
635
|
+
|
|
636
|
+
|
|
616
637
|
## The names the [code][resource][/code] section of [param text], a .tres file, sets, or those of
|
|
617
638
|
## the embedded resource with the id [param id] when one is given, as a set.
|
|
618
639
|
static func _names_set_in(text: String, id: String) -> Dictionary:
|
|
@@ -668,3 +689,28 @@ static func _kept_in(value: Variant, file: String, id: String, seen: Dictionary)
|
|
|
668
689
|
if found != null:
|
|
669
690
|
return found
|
|
670
691
|
return null
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
## Every shader error the engine raises while it is attached, in order.
|
|
695
|
+
class GdhShaderErrors:
|
|
696
|
+
extends Logger
|
|
697
|
+
|
|
698
|
+
var raised: Array[Dictionary] = []
|
|
699
|
+
|
|
700
|
+
func _log_message(_message: String, _error: bool) -> void:
|
|
701
|
+
pass
|
|
702
|
+
|
|
703
|
+
# The engine's own signature for a Logger, eight parameters and not this project's to shape.
|
|
704
|
+
# gdstyle:ignore=quality/max-parameters
|
|
705
|
+
func _log_error(
|
|
706
|
+
_function: String,
|
|
707
|
+
_file: String,
|
|
708
|
+
line: int,
|
|
709
|
+
code: String,
|
|
710
|
+
rationale: String,
|
|
711
|
+
_editor_notify: bool,
|
|
712
|
+
error_type: int,
|
|
713
|
+
_script_backtraces: Array[ScriptBacktrace],
|
|
714
|
+
) -> void:
|
|
715
|
+
if error_type == Logger.ERROR_TYPE_SHADER:
|
|
716
|
+
raised.append({ "line": line, "message": code if rationale.is_empty() else rationale })
|
|
@@ -88,11 +88,11 @@ static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
|
|
|
88
88
|
var base: String = override_base(name)
|
|
89
89
|
if not base.is_empty():
|
|
90
90
|
described["override_of"] = base
|
|
91
|
+
# Kept in the engine's order: its own settings as it registers them, so layer_2 comes
|
|
92
|
+
# before layer_10, then the project's as project.godot lists them, the order autoloads
|
|
93
|
+
# load in.
|
|
91
94
|
found.append(described)
|
|
92
95
|
|
|
93
|
-
found.sort_custom(
|
|
94
|
-
func(a: Dictionary, b: Dictionary) -> bool: return a["setting"] < b["setting"]
|
|
95
|
-
)
|
|
96
96
|
return { "prefix": prefix, "count": found.size(), "settings": found }
|
|
97
97
|
|
|
98
98
|
|
|
@@ -306,6 +306,12 @@ func create_animation(args: Dictionary) -> Dictionary:
|
|
|
306
306
|
var player: AnimationPlayer = opened[1]
|
|
307
307
|
if player.has_animation(StringName(animation_name)):
|
|
308
308
|
return scene.refuse("Animation already exists: " + animation_name)
|
|
309
|
+
# The library the animation goes in is this call's own change, made or not.
|
|
310
|
+
var library_name: String = animation_name.get_slice(
|
|
311
|
+
"/",
|
|
312
|
+
0,
|
|
313
|
+
) if animation_name.contains("/") else ""
|
|
314
|
+
scene.asked(player, ["libraries/" + library_name])
|
|
309
315
|
var library: Array = _library_for(player, animation_name)
|
|
310
316
|
if library[0] == null:
|
|
311
317
|
return scene.refuse(str(library[1]))
|
|
@@ -333,10 +333,52 @@ func modify_resource(args: Dictionary) -> Dictionary:
|
|
|
333
333
|
if not failed.is_empty():
|
|
334
334
|
return { "ok": false, "error": failed }
|
|
335
335
|
_note_put_back(answer, resource, res_path)
|
|
336
|
+
var also: Array[Dictionary] = _also_saved(
|
|
337
|
+
resource, res_path, properties.keys(), previous, answer
|
|
338
|
+
)
|
|
339
|
+
if not also.is_empty():
|
|
340
|
+
answer["alsoSaved"] = also
|
|
336
341
|
answer["properties"] = _held(resource, properties.keys())
|
|
337
342
|
return answer
|
|
338
343
|
|
|
339
344
|
|
|
345
|
+
## Each line of the main resource the save at [param path] changed or dropped that the call did not
|
|
346
|
+
## ask for, as {property, was, saved} in the file's own text, was left out where [param previous]
|
|
347
|
+
## set nothing and saved where the save sets nothing. The engine saves only what it loaded, so a
|
|
348
|
+
## line it does not load, such as a shader parameter whose uniform was renamed or a variable a
|
|
349
|
+
## script no longer declares, goes with the first save after it.
|
|
350
|
+
func _also_saved(
|
|
351
|
+
resource: Resource, path: String, names: Array, previous: Variant, answer: Dictionary
|
|
352
|
+
) -> Array[Dictionary]:
|
|
353
|
+
var noted: Array[Dictionary] = []
|
|
354
|
+
if not previous is PackedByteArray or not path.ends_with(".tres"):
|
|
355
|
+
return noted
|
|
356
|
+
var bytes: PackedByteArray = previous
|
|
357
|
+
var asked: Dictionary = {}
|
|
358
|
+
var changed: Dictionary = answer.get("alsoChanged", {})
|
|
359
|
+
var told: Array = names + _properties.moved_by_engine.keys() + changed.keys()
|
|
360
|
+
for key: Variant in told:
|
|
361
|
+
asked[str(key)] = true
|
|
362
|
+
asked[str(key).get_slice(":", 0)] = true
|
|
363
|
+
var after: String = FileAccess.get_file_as_string(path)
|
|
364
|
+
for change: Array in GdhHeadlessCheck.main_changes(bytes.get_string_from_utf8(), after, asked):
|
|
365
|
+
var property: String = str(change[0])
|
|
366
|
+
var was: Variant = change[2] if change[1] else null
|
|
367
|
+
if GdhHeadlessCheck.reads_the_same(
|
|
368
|
+
was,
|
|
369
|
+
change[3],
|
|
370
|
+
GdhHeadlessCheck.default_of(resource, property),
|
|
371
|
+
):
|
|
372
|
+
continue
|
|
373
|
+
var entry: Dictionary = { "property": property }
|
|
374
|
+
if change[1]:
|
|
375
|
+
entry["was"] = change[2]
|
|
376
|
+
if change[3] != null:
|
|
377
|
+
entry["saved"] = change[3]
|
|
378
|
+
noted.append(entry)
|
|
379
|
+
return noted
|
|
380
|
+
|
|
381
|
+
|
|
340
382
|
## Moves to setByScript each property alsoChanged in [param answer] gave a value [param resource] no
|
|
341
383
|
## longer holds once saved at [param path]: the save put it back as its script's doing.
|
|
342
384
|
func _note_put_back(answer: Dictionary, resource: Resource, path: String) -> void:
|
|
@@ -426,28 +468,13 @@ func create_shader(args: Dictionary) -> Dictionary:
|
|
|
426
468
|
|
|
427
469
|
_written(shader_path)
|
|
428
470
|
var answer: Dictionary = { "ok": true, "shaderPath": shader_path, "shaderType": shader_type }
|
|
429
|
-
var errors: Array[Dictionary] =
|
|
471
|
+
var errors: Array[Dictionary] = GdhFileReads.shader_errors(shader_path)
|
|
430
472
|
answer["compiles"] = errors.is_empty()
|
|
431
473
|
if not errors.is_empty():
|
|
432
474
|
answer["compileErrors"] = errors
|
|
433
475
|
return answer
|
|
434
476
|
|
|
435
477
|
|
|
436
|
-
## The errors the engine raises compiling the shader at [param path], as {line, message} in order.
|
|
437
|
-
## A shader is compiled once something reads what it declares, and a create answering only the path
|
|
438
|
-
## left a missing semicolon to show up wherever the shader was first drawn with.
|
|
439
|
-
func _shader_errors(path: String) -> Array[Dictionary]:
|
|
440
|
-
var caught: GdhShaderErrors = GdhShaderErrors.new()
|
|
441
|
-
OS.add_logger(caught)
|
|
442
|
-
var shader: Shader = (
|
|
443
|
-
ResourceLoader.load(path, "Shader", ResourceLoader.CACHE_MODE_IGNORE) as Shader
|
|
444
|
-
)
|
|
445
|
-
if shader != null:
|
|
446
|
-
var _uniforms: Array = shader.get_shader_uniform_list()
|
|
447
|
-
OS.remove_logger(caught)
|
|
448
|
-
return caught.raised
|
|
449
|
-
|
|
450
|
-
|
|
451
478
|
## Saves [param theme] over [param theme_path] with [param items] changed, answering "" or why not.
|
|
452
479
|
func _save_theme(theme: Theme, theme_path: String, items: Array) -> String:
|
|
453
480
|
var previous: PackedByteArray = FileAccess.get_file_as_bytes(theme_path)
|
|
@@ -654,28 +681,3 @@ func _noting_variation(answer: Dictionary) -> Dictionary:
|
|
|
654
681
|
% ["is" if close.size() == 1 else "are", ", ".join(close), type]
|
|
655
682
|
)
|
|
656
683
|
return answer
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
## Every shader error the engine raises while it is attached, in order.
|
|
660
|
-
class GdhShaderErrors:
|
|
661
|
-
extends Logger
|
|
662
|
-
|
|
663
|
-
var raised: Array[Dictionary] = []
|
|
664
|
-
|
|
665
|
-
func _log_message(_message: String, _error: bool) -> void:
|
|
666
|
-
pass
|
|
667
|
-
|
|
668
|
-
# The engine's own signature for a Logger, eight parameters and not this project's to shape.
|
|
669
|
-
# gdstyle:ignore=quality/max-parameters
|
|
670
|
-
func _log_error(
|
|
671
|
-
_function: String,
|
|
672
|
-
_file: String,
|
|
673
|
-
line: int,
|
|
674
|
-
code: String,
|
|
675
|
-
rationale: String,
|
|
676
|
-
_editor_notify: bool,
|
|
677
|
-
error_type: int,
|
|
678
|
-
_script_backtraces: Array[ScriptBacktrace],
|
|
679
|
-
) -> void:
|
|
680
|
-
if error_type == Logger.ERROR_TYPE_SHADER:
|
|
681
|
-
raised.append({ "line": line, "message": code if rationale.is_empty() else rationale })
|
|
@@ -140,8 +140,9 @@ static func _global_constant(target: Object, declared: Dictionary, word: String)
|
|
|
140
140
|
)
|
|
141
141
|
if constant != wanted:
|
|
142
142
|
continue
|
|
143
|
+
# Typed, because a project that makes untyped declarations errors compiles this line too.
|
|
143
144
|
var reader: GDScript = GDScript.new()
|
|
144
|
-
reader.source_code = "extends RefCounted\nconst READ = %s\n" % constant
|
|
145
|
+
reader.source_code = "extends RefCounted\nconst READ: int = %s\n" % constant
|
|
145
146
|
if (reader.reload() == OK
|
|
146
147
|
and reader.get_script_constant_map().get("READ") == hinted[value_word]):
|
|
147
148
|
return hinted[value_word]
|
|
@@ -140,8 +140,9 @@ static func _global_constant(target: Object, declared: Dictionary, word: String)
|
|
|
140
140
|
)
|
|
141
141
|
if constant != wanted:
|
|
142
142
|
continue
|
|
143
|
+
# Typed, because a project that makes untyped declarations errors compiles this line too.
|
|
143
144
|
var reader: GDScript = GDScript.new()
|
|
144
|
-
reader.source_code = "extends RefCounted\nconst READ = %s\n" % constant
|
|
145
|
+
reader.source_code = "extends RefCounted\nconst READ: int = %s\n" % constant
|
|
145
146
|
if (reader.reload() == OK
|
|
146
147
|
and reader.get_script_constant_map().get("READ") == hinted[value_word]):
|
|
147
148
|
return hinted[value_word]
|
|
@@ -610,9 +610,30 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
610
610
|
var declared_class: String = str(script.get_global_name())
|
|
611
611
|
if not declared_class.is_empty():
|
|
612
612
|
answer["scriptClass"] = declared_class
|
|
613
|
+
# A shader file edited by hand is otherwise checked only where it is first drawn with.
|
|
614
|
+
if loaded is Shader and file == res_path:
|
|
615
|
+
var errors: Array[Dictionary] = shader_errors(file)
|
|
616
|
+
answer["compiles"] = errors.is_empty()
|
|
617
|
+
if not errors.is_empty():
|
|
618
|
+
answer["compileErrors"] = errors
|
|
613
619
|
return answer
|
|
614
620
|
|
|
615
621
|
|
|
622
|
+
## The errors the engine raises compiling the shader at [param path], as {line, message} in order.
|
|
623
|
+
## A shader is compiled once something reads what it declares, and a create answering only the path
|
|
624
|
+
## left a missing semicolon to show up wherever the shader was first drawn with.
|
|
625
|
+
static func shader_errors(path: String) -> Array[Dictionary]:
|
|
626
|
+
var caught: GdhShaderErrors = GdhShaderErrors.new()
|
|
627
|
+
OS.add_logger(caught)
|
|
628
|
+
var shader: Shader = (
|
|
629
|
+
ResourceLoader.load(path, "Shader", ResourceLoader.CACHE_MODE_IGNORE) as Shader
|
|
630
|
+
)
|
|
631
|
+
if shader != null:
|
|
632
|
+
var _uniforms: Array = shader.get_shader_uniform_list()
|
|
633
|
+
OS.remove_logger(caught)
|
|
634
|
+
return caught.raised
|
|
635
|
+
|
|
636
|
+
|
|
616
637
|
## The names the [code][resource][/code] section of [param text], a .tres file, sets, or those of
|
|
617
638
|
## the embedded resource with the id [param id] when one is given, as a set.
|
|
618
639
|
static func _names_set_in(text: String, id: String) -> Dictionary:
|
|
@@ -668,3 +689,28 @@ static func _kept_in(value: Variant, file: String, id: String, seen: Dictionary)
|
|
|
668
689
|
if found != null:
|
|
669
690
|
return found
|
|
670
691
|
return null
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
## Every shader error the engine raises while it is attached, in order.
|
|
695
|
+
class GdhShaderErrors:
|
|
696
|
+
extends Logger
|
|
697
|
+
|
|
698
|
+
var raised: Array[Dictionary] = []
|
|
699
|
+
|
|
700
|
+
func _log_message(_message: String, _error: bool) -> void:
|
|
701
|
+
pass
|
|
702
|
+
|
|
703
|
+
# The engine's own signature for a Logger, eight parameters and not this project's to shape.
|
|
704
|
+
# gdstyle:ignore=quality/max-parameters
|
|
705
|
+
func _log_error(
|
|
706
|
+
_function: String,
|
|
707
|
+
_file: String,
|
|
708
|
+
line: int,
|
|
709
|
+
code: String,
|
|
710
|
+
rationale: String,
|
|
711
|
+
_editor_notify: bool,
|
|
712
|
+
error_type: int,
|
|
713
|
+
_script_backtraces: Array[ScriptBacktrace],
|
|
714
|
+
) -> void:
|
|
715
|
+
if error_type == Logger.ERROR_TYPE_SHADER:
|
|
716
|
+
raised.append({ "line": line, "message": code if rationale.is_empty() else rationale })
|
|
@@ -113,7 +113,8 @@ func get_project_health(params: Dictionary) -> Dictionary:
|
|
|
113
113
|
"total_scripts": script_files.size(),
|
|
114
114
|
}
|
|
115
115
|
if compiled.has("checked"):
|
|
116
|
-
|
|
116
|
+
# The ones that compiled: each failure is one of the scripts checked.
|
|
117
|
+
scripts["compiled"] = GdhRead.as_int(compiled["checked"]) - failures.size()
|
|
117
118
|
else:
|
|
118
119
|
scripts["not_compiled"] = str(compiled.get("not_checked", "no compile was asked for"))
|
|
119
120
|
checks["scripts"] = scripts
|
|
@@ -88,11 +88,11 @@ static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
|
|
|
88
88
|
var base: String = override_base(name)
|
|
89
89
|
if not base.is_empty():
|
|
90
90
|
described["override_of"] = base
|
|
91
|
+
# Kept in the engine's order: its own settings as it registers them, so layer_2 comes
|
|
92
|
+
# before layer_10, then the project's as project.godot lists them, the order autoloads
|
|
93
|
+
# load in.
|
|
91
94
|
found.append(described)
|
|
92
95
|
|
|
93
|
-
found.sort_custom(
|
|
94
|
-
func(a: Dictionary, b: Dictionary) -> bool: return a["setting"] < b["setting"]
|
|
95
|
-
)
|
|
96
96
|
return { "prefix": prefix, "count": found.size(), "settings": found }
|
|
97
97
|
|
|
98
98
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.64",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|