gdharness 1.3.3 → 1.3.4

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 (43) hide show
  1. package/build/cli.js +105 -74
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +3 -2
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +157 -4
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
  7. package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
  8. package/build/godot/addons/gdharness_editor/property_values.gd +195 -221
  9. package/build/godot/addons/gdharness_editor/reading.gd +15 -0
  10. package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
  11. package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/scene_file.gd +97 -14
  13. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
  14. package/build/godot/addons/gdharness_editor/tool_executor.gd +9 -3
  15. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +38 -35
  16. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
  17. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
  18. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
  19. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +68 -2
  20. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +94 -333
  21. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
  22. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
  23. package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
  24. package/build/godot/addons/gdharness_runtime/runtime_input.gd +10 -26
  25. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
  26. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
  27. package/build/godot/addons/gdharness_runtime/runtime_values.gd +30 -0
  28. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
  29. package/build/godot/operations/animation_reads.gd +3 -2
  30. package/build/godot/operations/declared_values.gd +157 -4
  31. package/build/godot/operations/dependencies.gd +14 -39
  32. package/build/godot/operations/editor_tools.gd +4 -3
  33. package/build/godot/operations/file_reads.gd +15 -3
  34. package/build/godot/operations/gdscript_analysis.gd +229 -75
  35. package/build/godot/operations/gdscript_authoring.gd +142 -34
  36. package/build/godot/operations/import_pipeline.gd +17 -6
  37. package/build/godot/operations/logger.gd +7 -0
  38. package/build/godot/operations/property_paths.gd +225 -0
  39. package/build/godot/operations/property_values.gd +195 -221
  40. package/build/godot/operations/reading.gd +15 -0
  41. package/build/godot/operations/same_values.gd +177 -0
  42. package/build/godot/operations/serialisation.gd +75 -3
  43. package/package.json +1 -1
@@ -0,0 +1,177 @@
1
+ @tool
2
+ extends RefCounted
3
+
4
+ ## Whether a value read back is the value that was given, as the tools that write properties judge
5
+ ## what a write kept and what it changed besides.
6
+
7
+ const GdhRead = preload("reading.gd")
8
+ const GdhSerialisation = preload("serialisation.gd")
9
+
10
+ # The types made of floats, which a read back compares within float precision: a Vector2 property
11
+ # stores 32-bit components, so 0.1 comes back as 0.100000001.
12
+ const FLOAT_SHAPES: Array[int] = [
13
+ TYPE_VECTOR2,
14
+ TYPE_VECTOR3,
15
+ TYPE_VECTOR4,
16
+ TYPE_QUATERNION,
17
+ TYPE_COLOR,
18
+ TYPE_RECT2,
19
+ TYPE_TRANSFORM2D,
20
+ TYPE_BASIS,
21
+ TYPE_TRANSFORM3D,
22
+ TYPE_PLANE,
23
+ TYPE_AABB,
24
+ TYPE_PROJECTION,
25
+ ]
26
+
27
+ const PACKED: Array[int] = [
28
+ TYPE_PACKED_BYTE_ARRAY,
29
+ TYPE_PACKED_INT32_ARRAY,
30
+ TYPE_PACKED_INT64_ARRAY,
31
+ TYPE_PACKED_FLOAT32_ARRAY,
32
+ TYPE_PACKED_FLOAT64_ARRAY,
33
+ TYPE_PACKED_STRING_ARRAY,
34
+ TYPE_PACKED_VECTOR2_ARRAY,
35
+ TYPE_PACKED_VECTOR3_ARRAY,
36
+ TYPE_PACKED_VECTOR4_ARRAY,
37
+ TYPE_PACKED_COLOR_ARRAY,
38
+ ]
39
+
40
+ # The pairs of built-in resources being compared, so a resource that holds itself ends the
41
+ # comparison instead of recursing into it for ever.
42
+ static var _comparing: Array[Array] = []
43
+
44
+
45
+ ## Whether [param held] is [param given], as a property read back after a set answers it.
46
+ ##
47
+ ## Close enough rather than equal for anything made of floats: a Vector2 property stores 32-bit
48
+ ## components, so 0.1 comes back as 0.100000001. A packed array read back against the list it was
49
+ ## given compares element by element, since `==` between the two is an error rather than false.
50
+ static func same(held: Variant, given: Variant) -> bool:
51
+ var held_type: int = typeof(held)
52
+ var given_type: int = typeof(given)
53
+ # An empty object property reads back as an Object that is null, not as nil, and the two are
54
+ # the same nothing.
55
+ var held_none: bool = held_type == TYPE_NIL or (held_type == TYPE_OBJECT and held == null)
56
+ var given_none: bool = given_type == TYPE_NIL or (given_type == TYPE_OBJECT and given == null)
57
+ if held_none or given_none:
58
+ return held_none == given_none
59
+ if held_type == TYPE_OBJECT or given_type == TYPE_OBJECT:
60
+ return _same_object(held, given)
61
+ if GdhSerialisation.NUMBERS.has(held_type) and GdhSerialisation.NUMBERS.has(given_type):
62
+ if held_type == TYPE_BOOL or given_type == TYPE_BOOL:
63
+ return held_type == given_type and held == given
64
+ var held_number: float = GdhRead.as_float(held)
65
+ var given_number: float = GdhRead.as_float(given)
66
+ return (is_nan(held_number) and is_nan(given_number)) or is_equal_approx(held_number, given_number)
67
+ if GdhSerialisation.TEXTS.has(held_type) and GdhSerialisation.TEXTS.has(given_type):
68
+ return str(held) == str(given)
69
+ if _listed(held_type) and _listed(given_type):
70
+ var held_items: Array = type_convert(held, TYPE_ARRAY)
71
+ var given_items: Array = type_convert(given, TYPE_ARRAY)
72
+ if held_items.size() != given_items.size():
73
+ return false
74
+ for index: int in held_items.size():
75
+ if not same(held_items[index], given_items[index]):
76
+ return false
77
+ return true
78
+ if held_type == TYPE_DICTIONARY and given_type == TYPE_DICTIONARY:
79
+ var held_entries: Dictionary = held
80
+ var given_entries: Dictionary = given
81
+ return _same_entries(held_entries, given_entries)
82
+ if held_type != given_type:
83
+ return false
84
+ return _same_shape(held, given)
85
+
86
+
87
+ static func _same_object(held: Variant, given: Variant) -> bool:
88
+ if typeof(held) != TYPE_OBJECT or typeof(given) != TYPE_OBJECT:
89
+ return false
90
+ if is_same(held, given):
91
+ return true
92
+ if not held is Resource or not given is Resource:
93
+ return false
94
+ var held_resource: Resource = held
95
+ var given_resource: Resource = given
96
+ if not _built_in(held_resource):
97
+ return held_resource.resource_path == given_resource.resource_path
98
+ if held_resource.get_class() != given_resource.get_class():
99
+ return false
100
+ # A resource local to the scene is duplicated for every instance, so the copy read back is
101
+ # another object of the same class.
102
+ if held_resource.resource_local_to_scene:
103
+ return true
104
+ return _built_in(given_resource) and _same_stored(held_resource, given_resource)
105
+
106
+
107
+ ## A resource no other file can refer to: one with no path, or one of a file's own sub-resources,
108
+ ## whose path is the file's with "::" and an id after it.
109
+ static func _built_in(resource: Resource) -> bool:
110
+ return resource.resource_path.is_empty() or resource.resource_path.contains("::")
111
+
112
+
113
+ ## Two built-in resources of one class, by every property they store. Neither has a path to be
114
+ ## known by, and a file loaded again holds a new object for each, so a Curve written into a
115
+ ## resource file and read back was judged lost by the save that had just kept it.
116
+ static func _same_stored(held: Resource, given: Resource) -> bool:
117
+ for pair: Array in _comparing:
118
+ if is_same(pair[0], held) and is_same(pair[1], given):
119
+ return true
120
+ _comparing.append([held, given])
121
+ var alike: bool = true
122
+ for property: Dictionary in held.get_property_list():
123
+ var usage: int = GdhRead.as_int(property.get("usage", 0))
124
+ var name: String = str(property.get("name", ""))
125
+ if (
126
+ not (usage & PROPERTY_USAGE_STORAGE)
127
+ or (name.begins_with("resource_") and name != "resource_name")
128
+ ):
129
+ continue
130
+ if not same(held.get(name), given.get(name)):
131
+ alike = false
132
+ break
133
+ _comparing.pop_back()
134
+ return alike
135
+
136
+
137
+ static func _same_entries(held: Dictionary, given: Dictionary) -> bool:
138
+ if held.size() != given.size():
139
+ return false
140
+ for key: Variant in given:
141
+ var found: bool = false
142
+ for held_key: Variant in held:
143
+ if same(held_key, key):
144
+ found = same(held[held_key], given[key])
145
+ break
146
+ if not found:
147
+ return false
148
+ return true
149
+
150
+
151
+ ## Two values of one type made of floats, compared component by component through the type's own
152
+ ## text form, which lists every component: each type has its own is_equal_approx, and reaching it
153
+ ## through a Variant is a call the typed gate refuses.
154
+ static func _same_shape(held: Variant, given: Variant) -> bool:
155
+ if typeof(held) not in FLOAT_SHAPES:
156
+ return held == given
157
+ var held_parts: Array[float] = _components(held)
158
+ var given_parts: Array[float] = _components(given)
159
+ if held_parts.size() != given_parts.size():
160
+ return false
161
+ for index: int in held_parts.size():
162
+ if not same(held_parts[index], given_parts[index]):
163
+ return false
164
+ return true
165
+
166
+
167
+ static func _components(value: Variant) -> Array[float]:
168
+ var parts: Array[float] = []
169
+ var text: String = var_to_str(value)
170
+ var inner: String = text.substr(text.find("(") + 1).trim_suffix(")")
171
+ for part: String in inner.split(",", false):
172
+ parts.append(part.strip_edges().to_float())
173
+ return parts
174
+
175
+
176
+ static func _listed(type: int) -> bool:
177
+ return type == TYPE_ARRAY or PACKED.has(type)
@@ -121,6 +121,10 @@ const BUILDERS: Dictionary = {
121
121
  "Dictionary": "_build_dictionary",
122
122
  }
123
123
 
124
+ ## The built-in resources being described, outermost first, so one holding itself somewhere inside
125
+ ## is answered by its class rather than without end.
126
+ var _describing: Array[Resource] = []
127
+
124
128
 
125
129
  # Converts a Godot value into something JSON can carry. A type with no entry in the table
126
130
  # passes through as itself, which is what the JSON-native ones want.
@@ -665,9 +669,21 @@ func _serialize_object(value: Object) -> Variant:
665
669
  return _describe_input_event(event)
666
670
  if value is Resource:
667
671
  var resource: Resource = value
668
- if resource.resource_path.is_empty():
669
- return {"class": resource.get_class(), "_type": "Resource"}
670
- return {"path": resource.resource_path, "class": resource.get_class(), "_type": "Resource"}
672
+ # A file's built-in resources load with a path of the file's own and "::", which no other
673
+ # file can refer to.
674
+ if resource.resource_path.is_empty() or resource.resource_path.contains("::"):
675
+ return _describe_built_in(resource)
676
+ var filed: Dictionary = {
677
+ "path": resource.resource_path, "class": resource.get_class(), "_type": "Resource"
678
+ }
679
+ # The engine class of a resource with a script is the one its script extends, so a Stats
680
+ # file read as a Resource and nothing said it was a Stats.
681
+ var attached: Variant = resource.get_script()
682
+ if attached is Script:
683
+ var script: Script = attached
684
+ if not script.get_global_name().is_empty():
685
+ filed["scriptClass"] = str(script.get_global_name())
686
+ return filed
671
687
  if value is Node:
672
688
  var node: Node = value
673
689
  if node.is_inside_tree():
@@ -675,6 +691,62 @@ func _serialize_object(value: Object) -> Variant:
675
691
  return _describe_object(value)
676
692
 
677
693
 
694
+ ## A resource built into the file it was read from, in the form a write builds one from: its class
695
+ ## under _type beside every property it stores away from its default. As its class alone it said
696
+ ## nothing of what it held, so a CollisionShape2D's radius could not be read, and it could not be
697
+ ## written back.
698
+ func _describe_built_in(resource: Resource) -> Dictionary:
699
+ if _describing.has(resource):
700
+ return {"class": resource.get_class(), "_type": "Resource"}
701
+ _describing.append(resource)
702
+ var described: Dictionary = {"_type": resource.get_class()}
703
+ var attached: Variant = resource.get_script()
704
+ var script: Script = null
705
+ if attached is Script:
706
+ script = attached
707
+ described["script"] = serialize_value(script)
708
+ var fresh: Object = _fresh(script)
709
+ for property: Dictionary in resource.get_property_list():
710
+ var usage: int = property.get("usage", 0)
711
+ var name: String = str(property.get("name", ""))
712
+ if (
713
+ not (usage & PROPERTY_USAGE_STORAGE)
714
+ or name == "script"
715
+ or (name.begins_with("resource_") and name != "resource_name")
716
+ ):
717
+ continue
718
+ var held: Variant = resource.get(name)
719
+ var default: Variant = ClassDB.class_get_property_default_value(resource.get_class(), name)
720
+ if fresh != null:
721
+ default = fresh.get(name)
722
+ elif script != null and usage & PROPERTY_USAGE_SCRIPT_VARIABLE:
723
+ default = script.get_property_default_value(name)
724
+ if typeof(held) == typeof(default) and held == default:
725
+ continue
726
+ described[name] = serialize_value(held)
727
+ _describing.pop_back()
728
+ return described
729
+
730
+
731
+ ## A new instance of [param script] to read defaults off, or null where none should be made. What
732
+ ## the script's own code sets as it is made (a resource_name in _init, a list built by a
733
+ ## constructor, which the engine reports no default for) is then a default rather than a value
734
+ ## the file holds. None is made for a script whose _init needs arguments, nor in the editor for one
735
+ ## that is not a tool: the editor never runs those, and can_instantiate says so.
736
+ static func _fresh(script: Script) -> Object:
737
+ if script == null or not script.can_instantiate():
738
+ return null
739
+ for method: Dictionary in script.get_script_method_list():
740
+ if str(method.get("name", "")) == "_init":
741
+ var arguments: Array = method.get("args", [])
742
+ var defaults: Array = method.get("default_args", [])
743
+ if arguments.size() > defaults.size():
744
+ return null
745
+ # Called by name: new() is declared on each kind of script rather than on Script.
746
+ var made: Variant = script.call("new")
747
+ return made if made is Object else null
748
+
749
+
678
750
  ## An input event, by what it is bound to: the engine's own words for it and every field it stores
679
751
  ## away from its default. As a bare resource it was its class and nothing else, so an input action
680
752
  ## read back from project.godot could not say whether it was bound to Space or to a joypad button.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
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",