gdharness 1.3.21 → 1.3.23
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 +370 -296
- package/build/godot/addons/gdharness_editor/file_reads.gd +11 -4
- package/build/godot/addons/gdharness_editor/serialisation.gd +4 -6
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +46 -27
- package/build/godot/addons/gdharness_editor/value_shapes.gd +32 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +4 -6
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +32 -0
- package/build/godot/operations/file_reads.gd +11 -4
- package/build/godot/operations/serialisation.gd +4 -6
- package/build/godot/operations/value_shapes.gd +32 -0
- package/build/godot/tests/listed.gd +96 -4
- package/build/keeper.js +7 -1
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@ extends RefCounted
|
|
|
13
13
|
const GdhRead = preload("reading.gd")
|
|
14
14
|
const GdhSame = preload("same_values.gd")
|
|
15
15
|
const GdhSerialisation = preload("serialisation.gd")
|
|
16
|
+
const GdhShapes = preload("value_shapes.gd")
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
## The nodes of [param scene_path] as {name, type, path, children}, with instanceOf and
|
|
@@ -515,18 +516,24 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
515
516
|
scripted[str(declared.get("name", ""))] = true
|
|
516
517
|
var serialiser: GdhSerialisation = GdhSerialisation.new()
|
|
517
518
|
var properties: Dictionary = {}
|
|
518
|
-
|
|
519
|
+
# A Curve's points are read off one with none, since the engine has no default for point_0.
|
|
520
|
+
var fresh: Object = (
|
|
521
|
+
ClassDB.instantiate(loaded.get_class()) if GdhShapes.STORED_APART.has(loaded.get_class()) else null
|
|
522
|
+
)
|
|
523
|
+
for p: Dictionary in GdhShapes.read_properties(loaded):
|
|
519
524
|
var property: String = str(p.get("name", ""))
|
|
520
525
|
if property.is_empty() or property == "script":
|
|
521
526
|
continue
|
|
522
|
-
if not (GdhRead.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
523
|
-
continue
|
|
524
527
|
var value: Variant = loaded.get(property)
|
|
525
528
|
if not include_defaults:
|
|
526
529
|
var default: Variant = (
|
|
527
530
|
script.get_property_default_value(property)
|
|
528
531
|
if scripted.has(property)
|
|
529
|
-
else
|
|
532
|
+
else (
|
|
533
|
+
fresh.get(property)
|
|
534
|
+
if fresh != null
|
|
535
|
+
else ClassDB.class_get_property_default_value(loaded.get_class(), property)
|
|
536
|
+
)
|
|
530
537
|
)
|
|
531
538
|
if GdhSame.same(value, default):
|
|
532
539
|
continue
|
|
@@ -698,14 +698,12 @@ func _describe_built_in(resource: Resource) -> Dictionary:
|
|
|
698
698
|
else:
|
|
699
699
|
described["_type"] = str(script.get_global_name())
|
|
700
700
|
var fresh: Object = _fresh(script)
|
|
701
|
-
|
|
701
|
+
if fresh == null and GdhShapes.STORED_APART.has(resource.get_class()):
|
|
702
|
+
fresh = ClassDB.instantiate(resource.get_class())
|
|
703
|
+
for property: Dictionary in GdhShapes.read_properties(resource):
|
|
702
704
|
var usage: int = property.get("usage", 0)
|
|
703
705
|
var name: String = str(property.get("name", ""))
|
|
704
|
-
if (
|
|
705
|
-
not (usage & PROPERTY_USAGE_STORAGE)
|
|
706
|
-
or name == "script"
|
|
707
|
-
or (name.begins_with("resource_") and name != "resource_name")
|
|
708
|
-
):
|
|
706
|
+
if name == "script" or (name.begins_with("resource_") and name != "resource_name"):
|
|
709
707
|
continue
|
|
710
708
|
var held: Variant = resource.get(name)
|
|
711
709
|
var default: Variant = ClassDB.class_get_property_default_value(resource.get_class(), name)
|
|
@@ -58,7 +58,8 @@ func add_animation_track(args: Dictionary) -> Dictionary:
|
|
|
58
58
|
)
|
|
59
59
|
)
|
|
60
60
|
var keyframes: Array = keyed[1]
|
|
61
|
-
var
|
|
61
|
+
var replacing: bool = GdhRead.as_bool(args.get("replaceKeys", false))
|
|
62
|
+
var built: Array = _add_track(anim, track, target, target_path, keyframes, replacing)
|
|
62
63
|
if not str(built[1]).is_empty():
|
|
63
64
|
return scene.refuse(str(built[1]))
|
|
64
65
|
var track_idx: int = built[0]
|
|
@@ -82,6 +83,10 @@ func add_animation_track(args: Dictionary) -> Dictionary:
|
|
|
82
83
|
}
|
|
83
84
|
if built[3]:
|
|
84
85
|
answer["existingTrack"] = true
|
|
86
|
+
var replaced: Array = built[5]
|
|
87
|
+
if not replaced.is_empty():
|
|
88
|
+
answer["keysReplaced"] = replaced
|
|
89
|
+
answer["keysAdded"] = keyframes.size() - replaced.size()
|
|
85
90
|
if built[2] != null:
|
|
86
91
|
answer["methodFound"] = built[2]
|
|
87
92
|
# Written all the same, as a missing method is: the method may be changed next.
|
|
@@ -106,15 +111,16 @@ func add_animation_track(args: Dictionary) -> Dictionary:
|
|
|
106
111
|
|
|
107
112
|
|
|
108
113
|
## Adds [param track] to [param anim] with [param keyframes], as [index, why not, methodFound,
|
|
109
|
-
## existing, misfit]: methodFound is null for a property track, existing is whether the
|
|
110
|
-
## into a track the animation already had for that path,
|
|
111
|
-
## cannot reach the method, or ""
|
|
114
|
+
## existing, misfit, replaced]: methodFound is null for a property track, existing is whether the
|
|
115
|
+
## keys went into a track the animation already had for that path, misfit why a method key's
|
|
116
|
+
## arguments cannot reach the method, or "", and replaced each key [param replacing] let a keyframe
|
|
117
|
+
## take the place of, as its time and what it held.
|
|
112
118
|
##
|
|
113
119
|
## Into that track rather than a second one beside it, as the editor keys a property it already
|
|
114
120
|
## animates: two tracks for one path both play, the later overriding the earlier, and a key meant
|
|
115
121
|
## to extend the motion was split off where reading the animation showed two halves.
|
|
116
122
|
func _add_track(
|
|
117
|
-
anim: Animation, track: Dictionary, target: Node, target_path: String, keyframes: Array
|
|
123
|
+
anim: Animation, track: Dictionary, target: Node, target_path: String, keyframes: Array, replacing: bool
|
|
118
124
|
) -> Array:
|
|
119
125
|
var track_type: String = str(track.get("type", ""))
|
|
120
126
|
var keys: Array = []
|
|
@@ -125,21 +131,21 @@ func _add_track(
|
|
|
125
131
|
if track_type == "property":
|
|
126
132
|
var prop_name: String = str(track.get("property", ""))
|
|
127
133
|
if prop_name.is_empty():
|
|
128
|
-
return
|
|
134
|
+
return _not_added("track.property is required for property track")
|
|
129
135
|
var values: Array = _key_values(target, prop_name, keyframes)
|
|
130
136
|
if not values[0]:
|
|
131
|
-
return
|
|
137
|
+
return _not_added(str(values[1]))
|
|
132
138
|
keys = values[1]
|
|
133
139
|
path = NodePath(target_path + ":" + prop_name)
|
|
134
140
|
elif track_type == "method":
|
|
135
141
|
var method_name: String = str(track.get("method", ""))
|
|
136
142
|
if method_name.is_empty():
|
|
137
|
-
return
|
|
143
|
+
return _not_added("track.method is required for method track")
|
|
138
144
|
for index: int in keyframes.size():
|
|
139
145
|
var keyframe: Dictionary = keyframes[index]
|
|
140
146
|
var given: Variant = keyframe.get("args", [])
|
|
141
147
|
if not given is Array:
|
|
142
|
-
return
|
|
148
|
+
return _not_added("keyframes[%d].args must be a list." % index)
|
|
143
149
|
var raw: Array = given
|
|
144
150
|
var arguments: Array = _fitted(target, method_name, _parse_method_args(raw))
|
|
145
151
|
keys.append({"method": method_name, "args": arguments})
|
|
@@ -148,47 +154,60 @@ func _add_track(
|
|
|
148
154
|
method_found = target.has_method(method_name)
|
|
149
155
|
kind = Animation.TYPE_METHOD
|
|
150
156
|
else:
|
|
151
|
-
return
|
|
152
|
-
-1,
|
|
157
|
+
return _not_added(
|
|
153
158
|
(
|
|
154
159
|
"add_track makes property and method tracks, and track.type is %s; remove_track takes any."
|
|
155
160
|
% track_type
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
false,
|
|
159
|
-
""
|
|
160
|
-
]
|
|
161
|
+
)
|
|
162
|
+
)
|
|
161
163
|
|
|
162
164
|
var track_idx: int = anim.find_track(path, kind)
|
|
163
165
|
var existing: bool = track_idx >= 0
|
|
166
|
+
var replaced: Array[Dictionary] = []
|
|
164
167
|
if existing:
|
|
165
168
|
# Checked before any key goes in, since track_insert_key replaces a key at the same time
|
|
166
169
|
# without a word, and a refusal partway would leave the editor's scene half changed.
|
|
170
|
+
var values: GdhSerialisation = GdhSerialisation.new()
|
|
167
171
|
for index: int in keyframes.size():
|
|
168
172
|
var keyframe: Dictionary = keyframes[index]
|
|
169
173
|
var time: float = GdhRead.as_float(keyframe["time"])
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
var key: int = anim.track_find_key(track_idx, time, Animation.FIND_MODE_APPROX)
|
|
175
|
+
if key < 0:
|
|
176
|
+
continue
|
|
177
|
+
if not replacing:
|
|
178
|
+
return _not_added(
|
|
173
179
|
(
|
|
174
180
|
(
|
|
175
181
|
"Track %d already animates %s and has a key at %s s, which keyframes[%d] would "
|
|
176
|
-
+ "replace. Nothing was written."
|
|
182
|
+
+ "replace. Nothing was written; replaceKeys true replaces it."
|
|
177
183
|
)
|
|
178
184
|
% [track_idx, str(path), _seconds(time), index]
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
# A method key's value is its call, {"method": ..., "args": [...]}. The time is as asked:
|
|
188
|
+
# the key's own, read back from a scene's 32-bit times, puts 0.1 at 0.100000001490116.
|
|
189
|
+
(
|
|
190
|
+
replaced
|
|
191
|
+
. append(
|
|
192
|
+
{
|
|
193
|
+
"time": time,
|
|
194
|
+
"was": values.serialize_value(anim.track_get_key_value(track_idx, key)),
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
)
|
|
184
198
|
else:
|
|
185
199
|
track_idx = anim.add_track(kind)
|
|
186
200
|
anim.track_set_path(track_idx, path)
|
|
187
201
|
for index: int in keyframes.size():
|
|
188
202
|
var keyframe: Dictionary = keyframes[index]
|
|
189
203
|
if anim.track_insert_key(track_idx, GdhRead.as_float(keyframe["time"]), keys[index]) < 0:
|
|
190
|
-
return
|
|
191
|
-
return [track_idx, "", method_found, existing, misfit]
|
|
204
|
+
return _not_added("Keyframe %d could not be inserted." % index)
|
|
205
|
+
return [track_idx, "", method_found, existing, misfit, replaced]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
## What [method _add_track] answers when it added nothing, because of [param why].
|
|
209
|
+
static func _not_added(why: String) -> Array:
|
|
210
|
+
return [-1, why, null, false, "", []]
|
|
192
211
|
|
|
193
212
|
|
|
194
213
|
## [param arguments] as [param method] on [param target] takes them: a whole number read as a
|
|
@@ -7,6 +7,17 @@ extends RefCounted
|
|
|
7
7
|
|
|
8
8
|
const GdhRead = preload("reading.gd")
|
|
9
9
|
|
|
10
|
+
## The classes that store what the inspector shows, and a write takes, under other names: a Curve
|
|
11
|
+
## keeps its points as _data, numbers and modes in one list, and its range as _limits, where the
|
|
12
|
+
## inspector and resource_edit modify have point_count, point_0/position and min_value. A read
|
|
13
|
+
## answering _data could not be written back the way it was read.
|
|
14
|
+
const STORED_APART: Dictionary[String, Array] = {
|
|
15
|
+
"Curve": ["_data", "_limits"],
|
|
16
|
+
"Curve2D": ["_data"],
|
|
17
|
+
"Curve3D": ["_data"],
|
|
18
|
+
}
|
|
19
|
+
const HEADINGS: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
|
|
20
|
+
|
|
10
21
|
## What each packed array holds, one value of which every element is fitted to.
|
|
11
22
|
const PACKED_ELEMENTS: Dictionary = {
|
|
12
23
|
TYPE_PACKED_BYTE_ARRAY: TYPE_INT,
|
|
@@ -282,3 +293,24 @@ static func _unread(value: Variant, type: int) -> String:
|
|
|
282
293
|
if left.has("type") and not fields.has("_type"):
|
|
283
294
|
return note + ': a value names its type under "_type".'
|
|
284
295
|
return note + "."
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
## The properties of [param object] a read answers with, as [method Object.get_property_list] gives
|
|
299
|
+
## them: those it stores, with the inspector's own in place of what [constant STORED_APART] names.
|
|
300
|
+
static func read_properties(object: Object) -> Array[Dictionary]:
|
|
301
|
+
var apart: Array = STORED_APART.get(object.get_class(), [])
|
|
302
|
+
var read: Array[Dictionary] = []
|
|
303
|
+
for property: Dictionary in object.get_property_list():
|
|
304
|
+
var usage: int = GdhRead.as_int(property.get("usage", 0))
|
|
305
|
+
var name: String = str(property.get("name", ""))
|
|
306
|
+
if usage & PROPERTY_USAGE_STORAGE:
|
|
307
|
+
if not apart.has(name):
|
|
308
|
+
read.append(property)
|
|
309
|
+
elif (
|
|
310
|
+
not apart.is_empty()
|
|
311
|
+
and usage & PROPERTY_USAGE_EDITOR
|
|
312
|
+
and not usage & HEADINGS
|
|
313
|
+
and name != "resource_path"
|
|
314
|
+
):
|
|
315
|
+
read.append(property)
|
|
316
|
+
return read
|
|
@@ -698,14 +698,12 @@ func _describe_built_in(resource: Resource) -> Dictionary:
|
|
|
698
698
|
else:
|
|
699
699
|
described["_type"] = str(script.get_global_name())
|
|
700
700
|
var fresh: Object = _fresh(script)
|
|
701
|
-
|
|
701
|
+
if fresh == null and GdhShapes.STORED_APART.has(resource.get_class()):
|
|
702
|
+
fresh = ClassDB.instantiate(resource.get_class())
|
|
703
|
+
for property: Dictionary in GdhShapes.read_properties(resource):
|
|
702
704
|
var usage: int = property.get("usage", 0)
|
|
703
705
|
var name: String = str(property.get("name", ""))
|
|
704
|
-
if (
|
|
705
|
-
not (usage & PROPERTY_USAGE_STORAGE)
|
|
706
|
-
or name == "script"
|
|
707
|
-
or (name.begins_with("resource_") and name != "resource_name")
|
|
708
|
-
):
|
|
706
|
+
if name == "script" or (name.begins_with("resource_") and name != "resource_name"):
|
|
709
707
|
continue
|
|
710
708
|
var held: Variant = resource.get(name)
|
|
711
709
|
var default: Variant = ClassDB.class_get_property_default_value(resource.get_class(), name)
|
|
@@ -7,6 +7,17 @@ extends RefCounted
|
|
|
7
7
|
|
|
8
8
|
const GdhRead = preload("reading.gd")
|
|
9
9
|
|
|
10
|
+
## The classes that store what the inspector shows, and a write takes, under other names: a Curve
|
|
11
|
+
## keeps its points as _data, numbers and modes in one list, and its range as _limits, where the
|
|
12
|
+
## inspector and resource_edit modify have point_count, point_0/position and min_value. A read
|
|
13
|
+
## answering _data could not be written back the way it was read.
|
|
14
|
+
const STORED_APART: Dictionary[String, Array] = {
|
|
15
|
+
"Curve": ["_data", "_limits"],
|
|
16
|
+
"Curve2D": ["_data"],
|
|
17
|
+
"Curve3D": ["_data"],
|
|
18
|
+
}
|
|
19
|
+
const HEADINGS: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
|
|
20
|
+
|
|
10
21
|
## What each packed array holds, one value of which every element is fitted to.
|
|
11
22
|
const PACKED_ELEMENTS: Dictionary = {
|
|
12
23
|
TYPE_PACKED_BYTE_ARRAY: TYPE_INT,
|
|
@@ -282,3 +293,24 @@ static func _unread(value: Variant, type: int) -> String:
|
|
|
282
293
|
if left.has("type") and not fields.has("_type"):
|
|
283
294
|
return note + ': a value names its type under "_type".'
|
|
284
295
|
return note + "."
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
## The properties of [param object] a read answers with, as [method Object.get_property_list] gives
|
|
299
|
+
## them: those it stores, with the inspector's own in place of what [constant STORED_APART] names.
|
|
300
|
+
static func read_properties(object: Object) -> Array[Dictionary]:
|
|
301
|
+
var apart: Array = STORED_APART.get(object.get_class(), [])
|
|
302
|
+
var read: Array[Dictionary] = []
|
|
303
|
+
for property: Dictionary in object.get_property_list():
|
|
304
|
+
var usage: int = GdhRead.as_int(property.get("usage", 0))
|
|
305
|
+
var name: String = str(property.get("name", ""))
|
|
306
|
+
if usage & PROPERTY_USAGE_STORAGE:
|
|
307
|
+
if not apart.has(name):
|
|
308
|
+
read.append(property)
|
|
309
|
+
elif (
|
|
310
|
+
not apart.is_empty()
|
|
311
|
+
and usage & PROPERTY_USAGE_EDITOR
|
|
312
|
+
and not usage & HEADINGS
|
|
313
|
+
and name != "resource_path"
|
|
314
|
+
):
|
|
315
|
+
read.append(property)
|
|
316
|
+
return read
|
|
@@ -13,6 +13,7 @@ extends RefCounted
|
|
|
13
13
|
const GdhRead = preload("reading.gd")
|
|
14
14
|
const GdhSame = preload("same_values.gd")
|
|
15
15
|
const GdhSerialisation = preload("serialisation.gd")
|
|
16
|
+
const GdhShapes = preload("value_shapes.gd")
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
## The nodes of [param scene_path] as {name, type, path, children}, with instanceOf and
|
|
@@ -515,18 +516,24 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
515
516
|
scripted[str(declared.get("name", ""))] = true
|
|
516
517
|
var serialiser: GdhSerialisation = GdhSerialisation.new()
|
|
517
518
|
var properties: Dictionary = {}
|
|
518
|
-
|
|
519
|
+
# A Curve's points are read off one with none, since the engine has no default for point_0.
|
|
520
|
+
var fresh: Object = (
|
|
521
|
+
ClassDB.instantiate(loaded.get_class()) if GdhShapes.STORED_APART.has(loaded.get_class()) else null
|
|
522
|
+
)
|
|
523
|
+
for p: Dictionary in GdhShapes.read_properties(loaded):
|
|
519
524
|
var property: String = str(p.get("name", ""))
|
|
520
525
|
if property.is_empty() or property == "script":
|
|
521
526
|
continue
|
|
522
|
-
if not (GdhRead.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
523
|
-
continue
|
|
524
527
|
var value: Variant = loaded.get(property)
|
|
525
528
|
if not include_defaults:
|
|
526
529
|
var default: Variant = (
|
|
527
530
|
script.get_property_default_value(property)
|
|
528
531
|
if scripted.has(property)
|
|
529
|
-
else
|
|
532
|
+
else (
|
|
533
|
+
fresh.get(property)
|
|
534
|
+
if fresh != null
|
|
535
|
+
else ClassDB.class_get_property_default_value(loaded.get_class(), property)
|
|
536
|
+
)
|
|
530
537
|
)
|
|
531
538
|
if GdhSame.same(value, default):
|
|
532
539
|
continue
|
|
@@ -698,14 +698,12 @@ func _describe_built_in(resource: Resource) -> Dictionary:
|
|
|
698
698
|
else:
|
|
699
699
|
described["_type"] = str(script.get_global_name())
|
|
700
700
|
var fresh: Object = _fresh(script)
|
|
701
|
-
|
|
701
|
+
if fresh == null and GdhShapes.STORED_APART.has(resource.get_class()):
|
|
702
|
+
fresh = ClassDB.instantiate(resource.get_class())
|
|
703
|
+
for property: Dictionary in GdhShapes.read_properties(resource):
|
|
702
704
|
var usage: int = property.get("usage", 0)
|
|
703
705
|
var name: String = str(property.get("name", ""))
|
|
704
|
-
if (
|
|
705
|
-
not (usage & PROPERTY_USAGE_STORAGE)
|
|
706
|
-
or name == "script"
|
|
707
|
-
or (name.begins_with("resource_") and name != "resource_name")
|
|
708
|
-
):
|
|
706
|
+
if name == "script" or (name.begins_with("resource_") and name != "resource_name"):
|
|
709
707
|
continue
|
|
710
708
|
var held: Variant = resource.get(name)
|
|
711
709
|
var default: Variant = ClassDB.class_get_property_default_value(resource.get_class(), name)
|
|
@@ -7,6 +7,17 @@ extends RefCounted
|
|
|
7
7
|
|
|
8
8
|
const GdhRead = preload("reading.gd")
|
|
9
9
|
|
|
10
|
+
## The classes that store what the inspector shows, and a write takes, under other names: a Curve
|
|
11
|
+
## keeps its points as _data, numbers and modes in one list, and its range as _limits, where the
|
|
12
|
+
## inspector and resource_edit modify have point_count, point_0/position and min_value. A read
|
|
13
|
+
## answering _data could not be written back the way it was read.
|
|
14
|
+
const STORED_APART: Dictionary[String, Array] = {
|
|
15
|
+
"Curve": ["_data", "_limits"],
|
|
16
|
+
"Curve2D": ["_data"],
|
|
17
|
+
"Curve3D": ["_data"],
|
|
18
|
+
}
|
|
19
|
+
const HEADINGS: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
|
|
20
|
+
|
|
10
21
|
## What each packed array holds, one value of which every element is fitted to.
|
|
11
22
|
const PACKED_ELEMENTS: Dictionary = {
|
|
12
23
|
TYPE_PACKED_BYTE_ARRAY: TYPE_INT,
|
|
@@ -282,3 +293,24 @@ static func _unread(value: Variant, type: int) -> String:
|
|
|
282
293
|
if left.has("type") and not fields.has("_type"):
|
|
283
294
|
return note + ': a value names its type under "_type".'
|
|
284
295
|
return note + "."
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
## The properties of [param object] a read answers with, as [method Object.get_property_list] gives
|
|
299
|
+
## them: those it stores, with the inspector's own in place of what [constant STORED_APART] names.
|
|
300
|
+
static func read_properties(object: Object) -> Array[Dictionary]:
|
|
301
|
+
var apart: Array = STORED_APART.get(object.get_class(), [])
|
|
302
|
+
var read: Array[Dictionary] = []
|
|
303
|
+
for property: Dictionary in object.get_property_list():
|
|
304
|
+
var usage: int = GdhRead.as_int(property.get("usage", 0))
|
|
305
|
+
var name: String = str(property.get("name", ""))
|
|
306
|
+
if usage & PROPERTY_USAGE_STORAGE:
|
|
307
|
+
if not apart.has(name):
|
|
308
|
+
read.append(property)
|
|
309
|
+
elif (
|
|
310
|
+
not apart.is_empty()
|
|
311
|
+
and usage & PROPERTY_USAGE_EDITOR
|
|
312
|
+
and not usage & HEADINGS
|
|
313
|
+
and name != "resource_path"
|
|
314
|
+
):
|
|
315
|
+
read.append(property)
|
|
316
|
+
return read
|
|
@@ -25,6 +25,13 @@ const ROWS: String = "--rows-in"
|
|
|
25
25
|
## that is not parameterised. gdUnit4 prints nothing for a case it skips, and the caller needs every
|
|
26
26
|
## row an engine found to tell one that no engine ran.
|
|
27
27
|
const LEFT_OUT: String = "gdharness left out"
|
|
28
|
+
## Printed after a case or a suite's hooks that gdUnit4 says left orphan nodes, as
|
|
29
|
+
## `res://suite.gd > test_case left 2: Node "Sandbox" of res://sandbox.gd, Timer "Timer"`, with
|
|
30
|
+
## `after` for the suite's hooks. gdUnit4 names them only where a case calls
|
|
31
|
+
## collect_orphan_node_details(), and otherwise counts them.
|
|
32
|
+
const ORPHANS_LEFT: String = "gdharness orphans:"
|
|
33
|
+
## How many orphans one line names before it counts the rest.
|
|
34
|
+
const ORPHANS_NAMED: int = 8
|
|
28
35
|
## The flags turning off a report, with the gdUnit4 hook that writes it. A gate reads what a run
|
|
29
36
|
## prints, and each report finds every case it hears of in the session to write them all out as the
|
|
30
37
|
## run ends; project_test reads the JUnit one and turns off only the HTML one.
|
|
@@ -89,6 +96,17 @@ class GdhRunner:
|
|
|
89
96
|
var cases_ended: int = 0
|
|
90
97
|
## The marker of the shutdown walk's oldest end, made as the run starts.
|
|
91
98
|
var walk_scripts: Array[GDScript] = []
|
|
99
|
+
## The orphan nodes as the running case and the running suite began, and those already named in
|
|
100
|
+
## the suite, so each line names only what its own case or hooks left.
|
|
101
|
+
var orphans_at_case: Dictionary[int, bool] = {}
|
|
102
|
+
var orphans_at_suite: Dictionary[int, bool] = {}
|
|
103
|
+
var orphans_named: Dictionary[int, bool] = {}
|
|
104
|
+
## The orphans new as the suite's first case began, which its before() made, and whether a case
|
|
105
|
+
## has begun: gdUnit4 counts both hooks' orphans at the suite's end.
|
|
106
|
+
var orphans_from_before: Dictionary[int, bool] = {}
|
|
107
|
+
var case_began: bool = false
|
|
108
|
+
## The suite running, which a case's own event does not carry.
|
|
109
|
+
var suite_running: String = ""
|
|
92
110
|
|
|
93
111
|
## As the run ends, the marker of the walk's newest end, and every script held past the walk, on
|
|
94
112
|
## an engine whose walk can stop part way ([GdhShutdownWalk]).
|
|
@@ -156,11 +174,85 @@ class GdhRunner:
|
|
|
156
174
|
## is announced: everything that reads the session, the hooks, the cases and the console printing
|
|
157
175
|
## every case, is handed it after this.
|
|
158
176
|
func _on_gdunit_event(event: GdUnitEvent) -> void:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
177
|
+
match event.type():
|
|
178
|
+
GdUnitEvent.SESSION_START:
|
|
179
|
+
_test_session = GdhIndexedSession.of(_test_session)
|
|
180
|
+
GdUnitEvent.TESTSUITE_BEFORE:
|
|
181
|
+
suite_running = event.resource_path()
|
|
182
|
+
orphans_at_suite = orphan_ids()
|
|
183
|
+
orphans_named.clear()
|
|
184
|
+
orphans_from_before.clear()
|
|
185
|
+
case_began = false
|
|
186
|
+
GdUnitEvent.TESTCASE_BEFORE:
|
|
187
|
+
orphans_at_case = orphan_ids()
|
|
188
|
+
if not case_began:
|
|
189
|
+
case_began = true
|
|
190
|
+
for id: int in new_orphans(orphans_at_suite):
|
|
191
|
+
orphans_from_before[id] = true
|
|
192
|
+
GdUnitEvent.TESTCASE_AFTER:
|
|
193
|
+
cases_ended += 1
|
|
163
194
|
super(event)
|
|
195
|
+
# After gdUnit4's own report of the case, so the line falls under it.
|
|
196
|
+
if event.orphan_nodes() == 0:
|
|
197
|
+
return
|
|
198
|
+
if event.type() == GdUnitEvent.TESTCASE_AFTER:
|
|
199
|
+
say_left(event.test_name(), new_orphans(orphans_at_case))
|
|
200
|
+
elif event.type() == GdUnitEvent.TESTSUITE_AFTER:
|
|
201
|
+
var left: Array[int] = new_orphans(orphans_at_suite)
|
|
202
|
+
if not case_began:
|
|
203
|
+
say_left("before or after", left)
|
|
204
|
+
return
|
|
205
|
+
say_left("before", left.filter(func(id: int) -> bool: return orphans_from_before.has(id)))
|
|
206
|
+
say_left("after", left.filter(func(id: int) -> bool: return not orphans_from_before.has(id)))
|
|
207
|
+
|
|
208
|
+
## The orphans now that were not there in [param before] and no line has named yet.
|
|
209
|
+
func new_orphans(before: Dictionary[int, bool]) -> Array[int]:
|
|
210
|
+
var found: Array[int] = []
|
|
211
|
+
for id: int in orphan_ids():
|
|
212
|
+
if not before.has(id) and not orphans_named.has(id):
|
|
213
|
+
found.append(id)
|
|
214
|
+
return found
|
|
215
|
+
|
|
216
|
+
## Prints the orphans [param ids] as left by [param during], under [constant ORPHANS_LEFT], once
|
|
217
|
+
## each in a suite.
|
|
218
|
+
func say_left(during: String, ids: Array[int]) -> void:
|
|
219
|
+
if ids.is_empty():
|
|
220
|
+
return
|
|
221
|
+
var named: PackedStringArray = []
|
|
222
|
+
for id: int in ids:
|
|
223
|
+
orphans_named[id] = true
|
|
224
|
+
if named.size() < ORPHANS_NAMED:
|
|
225
|
+
var _named: bool = named.append(described(instance_from_id(id) as Node))
|
|
226
|
+
var more: String = "" if ids.size() <= named.size() else ", and %d more" % (ids.size() - named.size())
|
|
227
|
+
print(
|
|
228
|
+
(
|
|
229
|
+
"%s %s > %s left %d: %s%s"
|
|
230
|
+
% [ORPHANS_LEFT, suite_running, during, ids.size(), ", ".join(named), more]
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
## The nodes outside every tree that are not on their way out, by instance id, as gdUnit4 counts
|
|
235
|
+
## them: one queued for deletion is freed at the end of the frame.
|
|
236
|
+
static func orphan_ids() -> Dictionary[int, bool]:
|
|
237
|
+
var ids: Dictionary[int, bool] = {}
|
|
238
|
+
for id: int in Node.get_orphan_node_ids():
|
|
239
|
+
var node: Node = instance_from_id(id) as Node
|
|
240
|
+
if node != null and not node.is_queued_for_deletion():
|
|
241
|
+
ids[id] = true
|
|
242
|
+
return ids
|
|
243
|
+
|
|
244
|
+
## [param node] as an orphan line names it: its class, its name when it was given one, and its
|
|
245
|
+
## script when it has one.
|
|
246
|
+
static func described(node: Node) -> String:
|
|
247
|
+
var said: String = node.get_class()
|
|
248
|
+
if not node.name.is_empty():
|
|
249
|
+
said += ' "%s"' % node.name
|
|
250
|
+
var attached: Variant = node.get_script()
|
|
251
|
+
if attached is Script:
|
|
252
|
+
var script: Script = attached
|
|
253
|
+
if not script.resource_path.is_empty():
|
|
254
|
+
said += " of " + script.resource_path
|
|
255
|
+
return said
|
|
164
256
|
|
|
165
257
|
## [param args] with everything up to the script at [param own] given as gdUnit4's tool name. The
|
|
166
258
|
## path is compared with its separators and, on Windows, its case folded, since the engine
|
package/build/keeper.js
CHANGED
|
@@ -8308,6 +8308,11 @@ var IGNORING_CASE = " \\(ignoring case\\)";
|
|
|
8308
8308
|
var PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)(${IGNORING_CASE})?((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
|
|
8309
8309
|
var QUOTED_EQUALITY = new RegExp(`^Expecting(?: error message)?:\\n '([\\s\\S]*)'\\n but was\\n '([\\s\\S]*)'(?:${IGNORING_CASE})?\\n\\tat `);
|
|
8310
8310
|
var COLOUR = new RegExp(`${ESCAPE}\\[([0-9;]*)m`, "g");
|
|
8311
|
+
var HOOKS_LEFT = new Map([
|
|
8312
|
+
["before", "before()"],
|
|
8313
|
+
["after", "after()"],
|
|
8314
|
+
["before or after", "before() or after()"]
|
|
8315
|
+
]);
|
|
8311
8316
|
|
|
8312
8317
|
// src/suite-judge.ts
|
|
8313
8318
|
var TIME = String.raw`(?:\d+h )?(?:\d+min )?(?:\d+s )?\d+ms`;
|
|
@@ -8324,9 +8329,10 @@ var WINDOWS_EXITS = new Map([
|
|
|
8324
8329
|
]);
|
|
8325
8330
|
var MEMORY_EXITS = new Set([3221225495, 3221225773]);
|
|
8326
8331
|
var GDUNIT_FAILED = 100;
|
|
8332
|
+
var GDUNIT_ORPHANS = 101;
|
|
8327
8333
|
var GDUNIT_EXITS = new Map([
|
|
8328
8334
|
[GDUNIT_FAILED, "gdUnit4's code for a case that failed or errored"],
|
|
8329
|
-
[
|
|
8335
|
+
[GDUNIT_ORPHANS, "gdUnit4's code for orphan nodes left behind"],
|
|
8330
8336
|
[103, "gdUnit4's code for a suite that cannot run headless"],
|
|
8331
8337
|
[104, "gdUnit4's code for a Godot version it does not support"],
|
|
8332
8338
|
[105, "gdUnit4's code for scripts that did not compile"]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.23",
|
|
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",
|