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 CHANGED
@@ -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. 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
- 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
+ }
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] = _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 })
@@ -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
- 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.63",
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",