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 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 and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. Each file the written one refers to is named by its uid, as the editor names it, except a file the project's uid cache has no uid for yet (one made since the project was last scanned), which is named by path and listed under namedByPath with the call that scans it. 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;
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: '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. 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.'
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
- throw new UsageError(`Unknown command: ${command}. Run gdharness help.`);
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] = _shader_errors(shader_path)
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
- scripts["compiled"] = GdhRead.as_int(compiled["checked"])
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.62",
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",