gdharness 1.3.0 → 1.3.2

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.
Files changed (72) hide show
  1. package/build/cli.js +322 -96
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -19
  3. package/build/godot/addons/gdharness_editor/bridge_client.gd +8 -8
  4. package/build/godot/addons/gdharness_editor/declared_values.gd +116 -0
  5. package/build/godot/addons/gdharness_editor/declared_values.gd.uid +1 -0
  6. package/build/godot/addons/gdharness_editor/file_reads.gd +65 -25
  7. package/build/godot/addons/gdharness_editor/headless_check.gd +7 -7
  8. package/build/godot/addons/gdharness_editor/plugin.gd +6 -6
  9. package/build/godot/addons/gdharness_editor/property_values.gd +222 -59
  10. package/build/godot/addons/gdharness_editor/reading.gd +65 -0
  11. package/build/godot/addons/gdharness_editor/scene_file.gd +28 -23
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +49 -94
  13. package/build/godot/addons/gdharness_editor/tool_executor.gd +24 -24
  14. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +52 -41
  15. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +4 -4
  16. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +2 -2
  17. package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +14 -14
  18. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +175 -74
  19. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +17 -17
  20. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +14 -12
  21. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +7 -7
  22. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +88 -84
  23. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +4 -4
  24. package/build/godot/addons/gdharness_editor/value_shapes.gd +118 -0
  25. package/build/godot/addons/gdharness_editor/value_shapes.gd.uid +1 -0
  26. package/build/godot/addons/gdharness_runtime/reading.gd +65 -0
  27. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +18 -18
  28. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +29 -29
  29. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +21 -18
  30. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +86 -80
  31. package/build/godot/addons/gdharness_runtime/runtime_input.gd +65 -64
  32. package/build/godot/addons/gdharness_runtime/runtime_items.gd +6 -6
  33. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +8 -8
  34. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +6 -6
  35. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +10 -10
  36. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +7 -7
  37. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +41 -41
  38. package/build/godot/addons/gdharness_runtime/runtime_says.gd +3 -3
  39. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +33 -33
  40. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +20 -20
  41. package/build/godot/addons/gdharness_runtime/runtime_values.gd +52 -25
  42. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +56 -50
  43. package/build/godot/addons/gdharness_runtime/serialisation.gd +49 -94
  44. package/build/godot/addons/gdharness_runtime/value_shapes.gd +118 -0
  45. package/build/godot/addons/gdharness_runtime/value_shapes.gd.uid +1 -0
  46. package/build/godot/operations/animation_reads.gd +7 -19
  47. package/build/godot/operations/audio_buses.gd +10 -10
  48. package/build/godot/operations/class_cache.gd +16 -16
  49. package/build/godot/operations/classdb_queries.gd +25 -15
  50. package/build/godot/operations/declared_values.gd +116 -0
  51. package/build/godot/operations/dependencies.gd +141 -33
  52. package/build/godot/operations/editor_tools.gd +12 -12
  53. package/build/godot/operations/export_presets.gd +14 -14
  54. package/build/godot/operations/file_reads.gd +65 -25
  55. package/build/godot/operations/file_walk.gd +34 -0
  56. package/build/godot/operations/gdscript_analysis.gd +27 -22
  57. package/build/godot/operations/gdscript_authoring.gd +122 -17
  58. package/build/godot/operations/godot_operations.gd +66 -63
  59. package/build/godot/operations/import_pipeline.gd +59 -20
  60. package/build/godot/operations/input_actions.gd +90 -18
  61. package/build/godot/operations/logger.gd +1 -1
  62. package/build/godot/operations/plugins.gd +8 -8
  63. package/build/godot/operations/project_config.gd +83 -20
  64. package/build/godot/operations/project_diagnostics.gd +36 -11
  65. package/build/godot/operations/property_values.gd +222 -59
  66. package/build/godot/operations/reading.gd +65 -0
  67. package/build/godot/operations/rename_checks.gd +3 -3
  68. package/build/godot/operations/resource_files.gd +9 -6
  69. package/build/godot/operations/script_check.gd +23 -7
  70. package/build/godot/operations/serialisation.gd +49 -94
  71. package/build/godot/operations/value_shapes.gd +118 -0
  72. package/package.json +1 -1
@@ -13,17 +13,17 @@ extends RefCounted
13
13
  ## reaches, and every script loaded after it that depends on it compiles in silence, reload() and all.
14
14
  ## So a run ends at the first script that raised anything, and the rest go to a fresh engine.
15
15
 
16
- const Read = preload("reading.gd")
17
- const Log = preload("logger.gd")
16
+ const GdhRead = preload("reading.gd")
17
+ const GdhLog = preload("logger.gd")
18
18
 
19
19
  ## Where the engine raises a script's compile errors from.
20
20
  const RELOAD: String = "GDScript::reload"
21
21
 
22
- var _log: Log
22
+ var _log: GdhLog
23
23
 
24
24
 
25
25
  ## Every compile error the engine raises while it is attached, in order.
26
- class Caught:
26
+ class GdhCaught:
27
27
  extends Logger
28
28
 
29
29
  var raised: Array[Dictionary] = []
@@ -50,10 +50,26 @@ class Caught:
50
50
  )
51
51
 
52
52
 
53
- func _init(p_log: Log) -> void:
53
+ func _init(p_log: GdhLog) -> void:
54
54
  _log = p_log
55
55
 
56
56
 
57
+ ## One line for [param failure], a script [method check_scripts] answered as failing, with its
58
+ ## first error and the note the server put on it.
59
+ static func failure_line(failure: Dictionary) -> String:
60
+ var errors: Array = failure.get("errors", [])
61
+ var said: String = "%s does not compile" % str(failure.get("path", ""))
62
+ if not errors.is_empty():
63
+ var first: Dictionary = errors[0]
64
+ var line: int = GdhRead.as_int(first.get("line", 0))
65
+ var message: String = str(first.get("message", ""))
66
+ said += ": line %d, %s" % [line, message] if line > 0 else ": " + message
67
+ if errors.size() > 1:
68
+ said += " (and %d more)" % (errors.size() - 1)
69
+ var note: String = str(failure.get("note", ""))
70
+ return said if note.is_empty() else "%s %s" % [said, note]
71
+
72
+
57
73
  ## Whether the scripts in [code]paths[/code] compile in a fresh engine, which reads the class cache
58
74
  ## as it starts and so resolves every class the cache lists, up to and including the first that
59
75
  ## raised an error. [code]checked[/code] names the scripts read, in order, and [code]failed[/code]
@@ -62,7 +78,7 @@ func _init(p_log: Log) -> void:
62
78
  ## resource cache, so a script loaded for an earlier one is compiled again for its own entry.
63
79
  func check_scripts(params: Dictionary) -> Dictionary:
64
80
  var asked: Array = params.get("paths", [])
65
- var caught: Caught = Caught.new()
81
+ var caught: GdhCaught = GdhCaught.new()
66
82
  OS.add_logger(caught)
67
83
  var checked: Array[String] = []
68
84
  var failed: Array[String] = []
@@ -80,7 +96,7 @@ func check_scripts(params: Dictionary) -> Dictionary:
80
96
  if not at in others:
81
97
  others.append(at)
82
98
  continue
83
- var found: Dictionary = {"line": Read.as_int(error["line"]), "message": str(error["message"])}
99
+ var found: Dictionary = {"line": GdhRead.as_int(error["line"]), "message": str(error["message"])}
84
100
  if not found in errors:
85
101
  errors.append(found)
86
102
  if loaded == null and errors.is_empty():
@@ -8,7 +8,8 @@ extends RefCounted
8
8
  ## before: the same Vector2 came back three ways depending on which tool was asked, and a caller
9
9
  ## reading a property off the editor could not feed it back to the game.
10
10
 
11
- const Read = preload("reading.gd")
11
+ const GdhRead = preload("reading.gd")
12
+ const GdhShapes = preload("value_shapes.gd")
12
13
 
13
14
  # What the engine carries from one to another without complaint, which is what a value written to a
14
15
  # property or handed to a parameter may arrive as. Numbers include bool because the engine counts it
@@ -120,35 +121,6 @@ const BUILDERS: Dictionary = {
120
121
  "Dictionary": "_build_dictionary",
121
122
  }
122
123
 
123
- # What each tag has to carry before it is built. A tag naming a type whose keys are not there is a
124
- # tag somebody wrote by mistake or wrote for a different shape, and building one out of defaults
125
- # answers with a Transform2D of zeros where the caller meant something else entirely. Unbuilt, it
126
- # comes back as it arrived, and whoever asked can read it against the type they were expecting.
127
- const REQUIRED: Dictionary = {
128
- "Vector2": [["x", "y"]],
129
- "Vector2i": [["x", "y"]],
130
- "Vector3": [["x", "y", "z"]],
131
- "Vector3i": [["x", "y", "z"]],
132
- "Vector4": [["x", "y", "z", "w"]],
133
- "Vector4i": [["x", "y", "z", "w"]],
134
- "Rect2": [["position", "size"], ["x", "y", "width", "height"]],
135
- "Rect2i": [["position", "size"], ["x", "y", "width", "height"]],
136
- "Plane": [["normal", "d"]],
137
- "Quaternion": [["x", "y", "z", "w"]],
138
- "AABB": [["position", "size"]],
139
- "Basis": [["x", "y", "z"]],
140
- "Transform2D": [["x", "y", "origin"]],
141
- "Transform3D": [["basis", "origin"]],
142
- "Projection": [["x", "y", "z", "w"]],
143
- "Color": [["r", "g", "b"]],
144
- "NodePath": [["path"]],
145
- "PackedByteArray": [["base64"]],
146
- "float": [["value"]],
147
- "int": [["value"]],
148
- "RID": [["id"]],
149
- "Dictionary": [["entries"]],
150
- }
151
-
152
124
 
153
125
  # Converts a Godot value into something JSON can carry. A type with no entry in the table
154
126
  # passes through as itself, which is what the JSON-native ones want.
@@ -246,8 +218,8 @@ func deserialize_value(value: Variant) -> Variant:
246
218
  # under the target project's warning levels, so one such project lost every headless call.
247
219
  var tag: String = str(fields["_type"])
248
220
  var builder: String = BUILDERS.get(tag, "")
249
- var needed: Array = REQUIRED.get(tag, [])
250
- if builder.is_empty() or not _carries(fields, needed):
221
+ var spare: Array = GdhShapes.spare(fields, tag)
222
+ if builder.is_empty() or not spare.any(func(left: Array) -> bool: return left.is_empty()):
251
223
  return fields
252
224
  if tag == "float" and str(fields["value"]) not in ["inf", "-inf", "nan"]:
253
225
  return fields
@@ -430,9 +402,18 @@ func fitted(value: Variant, type: int) -> Variant:
430
402
  if rebuilt is Array and LIST_TYPES.has(type):
431
403
  return type_convert(rebuilt, type)
432
404
 
405
+ # The inspector and every colour picker give a colour as its html code. Anything else stays the
406
+ # text it is, for the refusal to name.
407
+ if type == TYPE_COLOR and rebuilt is String and Color.html_is_valid(str(rebuilt)):
408
+ return Color.html(str(rebuilt))
409
+
433
410
  var simple: Array[int] = [TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING]
434
411
  if not simple.has(type) or not simple.has(typeof(rebuilt)):
435
412
  return rebuilt
413
+ # A number or a truth value stays as sent where text is wanted, for the refusal to name. Converted,
414
+ # 1 arrived as the double it is in JSON and was saved as "1.0", which is not the text anyone sends.
415
+ if type == TYPE_STRING and not rebuilt is String:
416
+ return rebuilt
436
417
 
437
418
  # Digits are read as digits, since read as JSON they become a double and one past 2^53 a
438
419
  # neighbour of the number written.
@@ -441,7 +422,7 @@ func fitted(value: Variant, type: int) -> Variant:
441
422
 
442
423
  if rebuilt is String and type != TYPE_STRING:
443
424
  var text: String = rebuilt
444
- var parsed: Variant = Read.json_or_null(text)
425
+ var parsed: Variant = GdhRead.json_or_null(text)
445
426
  # A string that does not read as the type wanted is handed back as the string it is, so
446
427
  # whoever asked can refuse it. type_convert answers 0 for "not an index" and true for any
447
428
  # text at all, and a caller who sent a word got back a number they never sent: get_child
@@ -461,7 +442,7 @@ func fitted(value: Variant, type: int) -> Variant:
461
442
  static func inexact(given: Variant, type: int) -> String:
462
443
  if type != TYPE_INT or (typeof(given) != TYPE_FLOAT and typeof(given) != TYPE_INT):
463
444
  return ""
464
- var number: float = Read.as_float(given)
445
+ var number: float = GdhRead.as_float(given)
465
446
  if not is_finite(number) or absf(number) < EXACT:
466
447
  return ""
467
448
  return (
@@ -474,34 +455,6 @@ static func inexact(given: Variant, type: int) -> String:
474
455
  )
475
456
 
476
457
 
477
- ## How a value of [param type] is written to be read as one, for a refusal to name, or empty for a
478
- ## type that is not built from fields. A refusal naming only the two types left a caller to find
479
- ## the shape in the description of another op.
480
- static func written_as(type: int) -> String:
481
- var tag: String = type_string(type)
482
- var alternatives: Array = REQUIRED.get(tag, [])
483
- if alternatives.is_empty() or tag in ["float", "int", "Dictionary"]:
484
- return ""
485
- var keys: Array = alternatives[0]
486
- var parts: Array[String] = []
487
- for key: Variant in keys:
488
- parts.append('"%s": ...' % key)
489
- return "{%s}" % ", ".join(parts)
490
-
491
-
492
- ## Whether [param fields] carries every key of any one of [param alternatives].
493
- ##
494
- ## More than one spelling because a rectangle has two: the two corners it is answered with, and the
495
- ## four numbers the editor's own tools answered with before the serialisers were made one, which a
496
- ## caller may have kept.
497
- func _carries(fields: Dictionary, alternatives: Array) -> bool:
498
- for alternative: Variant in alternatives:
499
- var keys: Array = alternative
500
- if keys.all(func(key: Variant) -> bool: return fields.has(key)):
501
- return true
502
- return false
503
-
504
-
505
458
  func _serialize_nil(_value: Variant) -> Variant:
506
459
  return null
507
460
 
@@ -769,42 +722,44 @@ func _describe_object(value: Object) -> Dictionary:
769
722
 
770
723
 
771
724
  func _build_vector2(fields: Dictionary) -> Vector2:
772
- return Vector2(Read.as_float(fields.get("x", 0)), Read.as_float(fields.get("y", 0)))
725
+ return Vector2(GdhRead.as_float(fields.get("x", 0)), GdhRead.as_float(fields.get("y", 0)))
773
726
 
774
727
 
775
728
  func _build_vector2i(fields: Dictionary) -> Vector2i:
776
- return Vector2i(Read.as_int(fields.get("x", 0)), Read.as_int(fields.get("y", 0)))
729
+ return Vector2i(GdhRead.as_int(fields.get("x", 0)), GdhRead.as_int(fields.get("y", 0)))
777
730
 
778
731
 
779
732
  func _build_vector3(fields: Dictionary) -> Vector3:
780
733
  return Vector3(
781
- Read.as_float(fields.get("x", 0)),
782
- Read.as_float(fields.get("y", 0)),
783
- Read.as_float(fields.get("z", 0))
734
+ GdhRead.as_float(fields.get("x", 0)),
735
+ GdhRead.as_float(fields.get("y", 0)),
736
+ GdhRead.as_float(fields.get("z", 0))
784
737
  )
785
738
 
786
739
 
787
740
  func _build_vector3i(fields: Dictionary) -> Vector3i:
788
741
  return Vector3i(
789
- Read.as_int(fields.get("x", 0)), Read.as_int(fields.get("y", 0)), Read.as_int(fields.get("z", 0))
742
+ GdhRead.as_int(fields.get("x", 0)),
743
+ GdhRead.as_int(fields.get("y", 0)),
744
+ GdhRead.as_int(fields.get("z", 0))
790
745
  )
791
746
 
792
747
 
793
748
  func _build_vector4(fields: Dictionary) -> Vector4:
794
749
  return Vector4(
795
- Read.as_float(fields.get("x", 0)),
796
- Read.as_float(fields.get("y", 0)),
797
- Read.as_float(fields.get("z", 0)),
798
- Read.as_float(fields.get("w", 0))
750
+ GdhRead.as_float(fields.get("x", 0)),
751
+ GdhRead.as_float(fields.get("y", 0)),
752
+ GdhRead.as_float(fields.get("z", 0)),
753
+ GdhRead.as_float(fields.get("w", 0))
799
754
  )
800
755
 
801
756
 
802
757
  func _build_vector4i(fields: Dictionary) -> Vector4i:
803
758
  return Vector4i(
804
- Read.as_int(fields.get("x", 0)),
805
- Read.as_int(fields.get("y", 0)),
806
- Read.as_int(fields.get("z", 0)),
807
- Read.as_int(fields.get("w", 0))
759
+ GdhRead.as_int(fields.get("x", 0)),
760
+ GdhRead.as_int(fields.get("y", 0)),
761
+ GdhRead.as_int(fields.get("z", 0)),
762
+ GdhRead.as_int(fields.get("w", 0))
808
763
  )
809
764
 
810
765
 
@@ -813,10 +768,10 @@ func _build_vector4i(fields: Dictionary) -> Vector4i:
813
768
  func _build_rect2(fields: Dictionary) -> Rect2:
814
769
  if not fields.has("position"):
815
770
  return Rect2(
816
- Read.as_float(fields.get("x", 0)),
817
- Read.as_float(fields.get("y", 0)),
818
- Read.as_float(fields.get("width", 0)),
819
- Read.as_float(fields.get("height", 0))
771
+ GdhRead.as_float(fields.get("x", 0)),
772
+ GdhRead.as_float(fields.get("y", 0)),
773
+ GdhRead.as_float(fields.get("width", 0)),
774
+ GdhRead.as_float(fields.get("height", 0))
820
775
  )
821
776
  return Rect2(_vector2_at(fields, "position"), _vector2_at(fields, "size"))
822
777
 
@@ -824,24 +779,24 @@ func _build_rect2(fields: Dictionary) -> Rect2:
824
779
  func _build_rect2i(fields: Dictionary) -> Rect2i:
825
780
  if not fields.has("position"):
826
781
  return Rect2i(
827
- Read.as_int(fields.get("x", 0)),
828
- Read.as_int(fields.get("y", 0)),
829
- Read.as_int(fields.get("width", 0)),
830
- Read.as_int(fields.get("height", 0))
782
+ GdhRead.as_int(fields.get("x", 0)),
783
+ GdhRead.as_int(fields.get("y", 0)),
784
+ GdhRead.as_int(fields.get("width", 0)),
785
+ GdhRead.as_int(fields.get("height", 0))
831
786
  )
832
787
  return Rect2i(_vector2i_at(fields, "position"), _vector2i_at(fields, "size"))
833
788
 
834
789
 
835
790
  func _build_plane(fields: Dictionary) -> Plane:
836
- return Plane(_vector3_at(fields, "normal"), Read.as_float(fields.get("d", 0)))
791
+ return Plane(_vector3_at(fields, "normal"), GdhRead.as_float(fields.get("d", 0)))
837
792
 
838
793
 
839
794
  func _build_quaternion(fields: Dictionary) -> Quaternion:
840
795
  return Quaternion(
841
- Read.as_float(fields.get("x", 0)),
842
- Read.as_float(fields.get("y", 0)),
843
- Read.as_float(fields.get("z", 0)),
844
- Read.as_float(fields.get("w", 0))
796
+ GdhRead.as_float(fields.get("x", 0)),
797
+ GdhRead.as_float(fields.get("y", 0)),
798
+ GdhRead.as_float(fields.get("z", 0)),
799
+ GdhRead.as_float(fields.get("w", 0))
845
800
  )
846
801
 
847
802
 
@@ -881,10 +836,10 @@ func _build_projection(fields: Dictionary) -> Projection:
881
836
 
882
837
  func _build_color(fields: Dictionary) -> Color:
883
838
  return Color(
884
- Read.as_float(fields.get("r", 0)),
885
- Read.as_float(fields.get("g", 0)),
886
- Read.as_float(fields.get("b", 0)),
887
- Read.as_float(fields.get("a", 1), 1.0)
839
+ GdhRead.as_float(fields.get("r", 0)),
840
+ GdhRead.as_float(fields.get("g", 0)),
841
+ GdhRead.as_float(fields.get("b", 0)),
842
+ GdhRead.as_float(fields.get("a", 1), 1.0)
888
843
  )
889
844
 
890
845
 
@@ -897,7 +852,7 @@ func _build_bytes(fields: Dictionary) -> PackedByteArray:
897
852
 
898
853
 
899
854
  func _build_float(fields: Dictionary) -> float:
900
- return Read.as_float(fields)
855
+ return GdhRead.as_float(fields)
901
856
 
902
857
 
903
858
  func _build_int(fields: Dictionary) -> int:
@@ -0,0 +1,118 @@
1
+ extends RefCounted
2
+
3
+ ## How each engine type built from fields is written: the fields it needs, the ones it reads when
4
+ ## they are there, and what a refusal says about a value that is not one.
5
+ ##
6
+ ## This file is the one beside the operations, and `bun run sync:gd` copies it into each addon.
7
+
8
+ # What each tag has to carry before it is built. A tag naming a type whose keys are not there is a
9
+ # tag somebody wrote by mistake or wrote for a different shape, and building one out of defaults
10
+ # answers with a Transform2D of zeros where the caller meant something else entirely. Unbuilt, it
11
+ # comes back as it arrived, and whoever asked can read it against the type they were expecting.
12
+ const REQUIRED: Dictionary = {
13
+ "Vector2": [["x", "y"]],
14
+ "Vector2i": [["x", "y"]],
15
+ "Vector3": [["x", "y", "z"]],
16
+ "Vector3i": [["x", "y", "z"]],
17
+ "Vector4": [["x", "y", "z", "w"]],
18
+ "Vector4i": [["x", "y", "z", "w"]],
19
+ "Rect2": [["position", "size"], ["x", "y", "width", "height"]],
20
+ "Rect2i": [["position", "size"], ["x", "y", "width", "height"]],
21
+ "Plane": [["normal", "d"]],
22
+ "Quaternion": [["x", "y", "z", "w"]],
23
+ "AABB": [["position", "size"]],
24
+ "Basis": [["x", "y", "z"]],
25
+ "Transform2D": [["x", "y", "origin"]],
26
+ "Transform3D": [["basis", "origin"]],
27
+ "Projection": [["x", "y", "z", "w"]],
28
+ "Color": [["r", "g", "b"]],
29
+ "NodePath": [["path"]],
30
+ "PackedByteArray": [["base64"]],
31
+ "float": [["value"]],
32
+ "int": [["value"]],
33
+ "RID": [["id"]],
34
+ "Dictionary": [["entries"]],
35
+ }
36
+ ## The fields a builder reads when they are there and does without when not.
37
+ const OPTIONAL: Dictionary = {
38
+ "Color": ["a"],
39
+ "PackedByteArray": ["size"],
40
+ }
41
+
42
+
43
+ ## How a value of [param type] is written to be read as one, for a refusal to name, or empty for a
44
+ ## type that is not built from fields. A refusal naming only the two types leaves a caller to find
45
+ ## the shape in the description of another op.
46
+ static func written_as(type: int) -> String:
47
+ var tag: String = type_string(type)
48
+ var alternatives: Array = REQUIRED.get(tag, [])
49
+ if alternatives.is_empty() or tag in ["float", "int", "Dictionary"]:
50
+ return ""
51
+ var keys: Array = alternatives[0]
52
+ var optional: Array = OPTIONAL.get(tag, [])
53
+ var parts: Array[String] = []
54
+ for key: Variant in keys + optional:
55
+ parts.append('"%s": ...' % key)
56
+ var shape: String = "{%s}" % ", ".join(parts)
57
+ if type == TYPE_COLOR:
58
+ return shape + ' or as its html code, "#ff8000"'
59
+ return shape
60
+
61
+
62
+ ## The keys of [param fields] that a [param tag] built from them would not read, one list for each
63
+ ## spelling of its fields carried whole. A rectangle has two: its corners, and four numbers.
64
+ ##
65
+ ## A builder drops a key it does not know without a word, so {"x": 1, "y": 2, "z": 3} for a Vector2
66
+ ## would be written as (1, 2) and answered as written: a value is built only when a list is empty.
67
+ static func spare(fields: Dictionary, tag: String) -> Array:
68
+ var optional: Array = OPTIONAL.get(tag, [])
69
+ var lists: Array = []
70
+ var alternatives: Array = REQUIRED.get(tag, [])
71
+ for alternative: Variant in alternatives:
72
+ var keys: Array = alternative
73
+ if keys.all(func(key: Variant) -> bool: return fields.has(key)):
74
+ var over: Callable = func(key: Variant) -> bool:
75
+ return str(key) != "_type" and not keys.has(key) and not optional.has(key)
76
+ lists.append(fields.keys().filter(over))
77
+ return lists
78
+
79
+
80
+ ## The sentences a refusal of [param value] where a [param type] goes ends with: which of its keys
81
+ ## kept it from being read as one, and how one is written. Empty for a type not built from fields.
82
+ ##
83
+ ## The refusal names only the two types, and the key is what the caller has to take out.
84
+ static func remedy(value: Variant, type: int) -> String:
85
+ if type == TYPE_STRING and typeof(value) in [TYPE_BOOL, TYPE_INT, TYPE_FLOAT]:
86
+ var number: Variant = value
87
+ if value is float:
88
+ var whole: float = value
89
+ if is_finite(whole) and whole == floorf(whole):
90
+ number = int(whole)
91
+ return ' Text is sent in quotes, as "%s".' % str(number)
92
+ var shape: String = written_as(type)
93
+ if shape.is_empty():
94
+ return ""
95
+ return _unread(value, type) + " A %s is sent as %s." % [type_string(type), shape]
96
+
97
+
98
+ static func _unread(value: Variant, type: int) -> String:
99
+ if not value is Dictionary:
100
+ return ""
101
+ var fields: Dictionary = value
102
+ var tag: String = type_string(type)
103
+ var lists: Array = spare(fields, tag)
104
+ var built: bool = lists.any(func(over: Array) -> bool: return over.is_empty())
105
+ if lists.is_empty() or built or str(fields.get("_type", tag)) != tag:
106
+ return ""
107
+ lists.sort_custom(func(a: Array, b: Array) -> bool: return a.size() < b.size())
108
+ var left: Array = lists[0]
109
+ var named: PackedStringArray = []
110
+ for key: Variant in left:
111
+ var _named: bool = named.append('"%s"' % key)
112
+ var fields_called: String = "field called" if named.size() == 1 else "fields called"
113
+ var note: String = (
114
+ " A %s has no %s %s, so the value is not read as one" % [tag, fields_called, " or ".join(named)]
115
+ )
116
+ if left.has("type") and not fields.has("_type"):
117
+ return note + ': a value names its type under "_type".'
118
+ return note + "."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
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",