gdharness 1.3.63 → 1.3.65
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 +29 -6
- package/build/godot/addons/gdharness_editor/file_reads.gd +54 -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/operations/file_reads.gd +54 -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
|
@@ -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. An added entry has no was key, and a dropped entry has no saved key. 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,
|
|
@@ -59757,7 +59757,7 @@ ${error.text}`;
|
|
|
59757
59757
|
return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
|
|
59758
59758
|
...instead,
|
|
59759
59759
|
"Open the project in the editor with editor_launch, or by hand",
|
|
59760
|
-
"Enable the addon under Project > Project Settings > Plugins",
|
|
59760
|
+
...project === null ? ["Enable the addon under Project > Project Settings > Plugins"] : [],
|
|
59761
59761
|
"editor_status says whether the bridge is up and what it is listening on"
|
|
59762
59762
|
]);
|
|
59763
59763
|
}
|
|
@@ -62678,6 +62678,19 @@ async function registerRuntime(godot, projectPath) {
|
|
|
62678
62678
|
}
|
|
62679
62679
|
console.log(` ${useTheShippedLoader(RUNTIME_AUTOLOAD.name, path)}`);
|
|
62680
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
|
+
];
|
|
62681
62694
|
var SETUP_FLAGS = new Set(["--runtime", "--no-runtime", "--no-connect", "--no-skill", "--json", "--yes"]);
|
|
62682
62695
|
function unknownOption(flag) {
|
|
62683
62696
|
const known = [...SETUP_FLAGS, ...HARNESSES.map((harness) => `--${harness.id}`)];
|
|
@@ -62876,6 +62889,13 @@ async function uninstall() {
|
|
|
62876
62889
|
for (const path of addons) {
|
|
62877
62890
|
console.log(`removed ${path}`);
|
|
62878
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
|
+
}
|
|
62879
62899
|
const anything = addons.length > 0 || before.runtimeAutoloadPath !== null || enabled.length > 0;
|
|
62880
62900
|
console.log(anything ? `
|
|
62881
62901
|
Reconnect your harness so it stops spawning a server that is no longer installed.` : `
|
|
@@ -63194,8 +63214,11 @@ async function main() {
|
|
|
63194
63214
|
case "-h":
|
|
63195
63215
|
printHelp();
|
|
63196
63216
|
return;
|
|
63197
|
-
default:
|
|
63198
|
-
|
|
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
|
+
}
|
|
63199
63222
|
}
|
|
63200
63223
|
}
|
|
63201
63224
|
await main().catch((error) => {
|
|
@@ -610,9 +610,38 @@ 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
|
+
# A headless engine's output comes back with the answer, where these errors came a second time
|
|
627
|
+
# with a backtrace through gdharness's own scripts. The marks are logger.gd's ASIDE and RESUMED,
|
|
628
|
+
# which the editor addon does not ship, and an editor's Output panel would show them.
|
|
629
|
+
var headless: bool = not Engine.is_editor_hint()
|
|
630
|
+
if headless:
|
|
631
|
+
printerr("[gdharness] aside")
|
|
632
|
+
var caught: GdhShaderErrors = GdhShaderErrors.new()
|
|
633
|
+
OS.add_logger(caught)
|
|
634
|
+
var shader: Shader = (
|
|
635
|
+
ResourceLoader.load(path, "Shader", ResourceLoader.CACHE_MODE_IGNORE) as Shader
|
|
636
|
+
)
|
|
637
|
+
if shader != null:
|
|
638
|
+
var _uniforms: Array = shader.get_shader_uniform_list()
|
|
639
|
+
OS.remove_logger(caught)
|
|
640
|
+
if headless:
|
|
641
|
+
printerr("[gdharness] resumed")
|
|
642
|
+
return caught.raised
|
|
643
|
+
|
|
644
|
+
|
|
616
645
|
## The names the [code][resource][/code] section of [param text], a .tres file, sets, or those of
|
|
617
646
|
## the embedded resource with the id [param id] when one is given, as a set.
|
|
618
647
|
static func _names_set_in(text: String, id: String) -> Dictionary:
|
|
@@ -668,3 +697,28 @@ static func _kept_in(value: Variant, file: String, id: String, seen: Dictionary)
|
|
|
668
697
|
if found != null:
|
|
669
698
|
return found
|
|
670
699
|
return null
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
## Every shader error the engine raises while it is attached, in order.
|
|
703
|
+
class GdhShaderErrors:
|
|
704
|
+
extends Logger
|
|
705
|
+
|
|
706
|
+
var raised: Array[Dictionary] = []
|
|
707
|
+
|
|
708
|
+
func _log_message(_message: String, _error: bool) -> void:
|
|
709
|
+
pass
|
|
710
|
+
|
|
711
|
+
# The engine's own signature for a Logger, eight parameters and not this project's to shape.
|
|
712
|
+
# gdstyle:ignore=quality/max-parameters
|
|
713
|
+
func _log_error(
|
|
714
|
+
_function: String,
|
|
715
|
+
_file: String,
|
|
716
|
+
line: int,
|
|
717
|
+
code: String,
|
|
718
|
+
rationale: String,
|
|
719
|
+
_editor_notify: bool,
|
|
720
|
+
error_type: int,
|
|
721
|
+
_script_backtraces: Array[ScriptBacktrace],
|
|
722
|
+
) -> void:
|
|
723
|
+
if error_type == Logger.ERROR_TYPE_SHADER:
|
|
724
|
+
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 })
|
|
@@ -610,9 +610,38 @@ 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
|
+
# A headless engine's output comes back with the answer, where these errors came a second time
|
|
627
|
+
# with a backtrace through gdharness's own scripts. The marks are logger.gd's ASIDE and RESUMED,
|
|
628
|
+
# which the editor addon does not ship, and an editor's Output panel would show them.
|
|
629
|
+
var headless: bool = not Engine.is_editor_hint()
|
|
630
|
+
if headless:
|
|
631
|
+
printerr("[gdharness] aside")
|
|
632
|
+
var caught: GdhShaderErrors = GdhShaderErrors.new()
|
|
633
|
+
OS.add_logger(caught)
|
|
634
|
+
var shader: Shader = (
|
|
635
|
+
ResourceLoader.load(path, "Shader", ResourceLoader.CACHE_MODE_IGNORE) as Shader
|
|
636
|
+
)
|
|
637
|
+
if shader != null:
|
|
638
|
+
var _uniforms: Array = shader.get_shader_uniform_list()
|
|
639
|
+
OS.remove_logger(caught)
|
|
640
|
+
if headless:
|
|
641
|
+
printerr("[gdharness] resumed")
|
|
642
|
+
return caught.raised
|
|
643
|
+
|
|
644
|
+
|
|
616
645
|
## The names the [code][resource][/code] section of [param text], a .tres file, sets, or those of
|
|
617
646
|
## the embedded resource with the id [param id] when one is given, as a set.
|
|
618
647
|
static func _names_set_in(text: String, id: String) -> Dictionary:
|
|
@@ -668,3 +697,28 @@ static func _kept_in(value: Variant, file: String, id: String, seen: Dictionary)
|
|
|
668
697
|
if found != null:
|
|
669
698
|
return found
|
|
670
699
|
return null
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
## Every shader error the engine raises while it is attached, in order.
|
|
703
|
+
class GdhShaderErrors:
|
|
704
|
+
extends Logger
|
|
705
|
+
|
|
706
|
+
var raised: Array[Dictionary] = []
|
|
707
|
+
|
|
708
|
+
func _log_message(_message: String, _error: bool) -> void:
|
|
709
|
+
pass
|
|
710
|
+
|
|
711
|
+
# The engine's own signature for a Logger, eight parameters and not this project's to shape.
|
|
712
|
+
# gdstyle:ignore=quality/max-parameters
|
|
713
|
+
func _log_error(
|
|
714
|
+
_function: String,
|
|
715
|
+
_file: String,
|
|
716
|
+
line: int,
|
|
717
|
+
code: String,
|
|
718
|
+
rationale: String,
|
|
719
|
+
_editor_notify: bool,
|
|
720
|
+
error_type: int,
|
|
721
|
+
_script_backtraces: Array[ScriptBacktrace],
|
|
722
|
+
) -> void:
|
|
723
|
+
if error_type == Logger.ERROR_TYPE_SHADER:
|
|
724
|
+
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.65",
|
|
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",
|