gdharness 1.2.40 → 1.2.42
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 +103 -38
- package/build/godot/addons/gdharness_editor/animation_reads.gd +14 -2
- package/build/godot/addons/gdharness_editor/bridge_client.gd +2 -1
- package/build/godot/addons/gdharness_editor/file_reads.gd +134 -10
- package/build/godot/addons/gdharness_editor/headless_check.gd +56 -15
- package/build/godot/addons/gdharness_editor/property_values.gd +1 -1
- package/build/godot/addons/gdharness_editor/reading.gd +40 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +215 -12
- package/build/godot/addons/gdharness_editor/serialisation.gd +5 -1
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +39 -5
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +68 -15
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +346 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +438 -0
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +102 -60
- package/build/godot/addons/gdharness_runtime/reading.gd +40 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +3 -1
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +11 -6
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +3 -1
- package/build/godot/addons/gdharness_runtime/serialisation.gd +5 -1
- package/build/godot/operations/animation_reads.gd +14 -2
- package/build/godot/operations/editor_tools.gd +28 -1
- package/build/godot/operations/file_reads.gd +134 -10
- package/build/godot/operations/godot_operations.gd +2 -1
- package/build/godot/operations/property_values.gd +1 -1
- package/build/godot/operations/reading.gd +40 -0
- package/build/godot/operations/serialisation.gd +5 -1
- package/build/index.js +103 -38
- package/package.json +1 -1
|
@@ -15,9 +15,10 @@ const Read = preload("reading.gd")
|
|
|
15
15
|
const Serialisation = preload("serialisation.gd")
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
## The nodes of [param scene_path] as {name, type, path, children}, with instanceOf
|
|
19
|
-
## instances another scene and every stored property when
|
|
20
|
-
## file's value or the default where it sets none.
|
|
18
|
+
## The nodes of [param scene_path] as {name, type, path, children}, with instanceOf and
|
|
19
|
+
## editableChildren on a node that instances another scene and every stored property when
|
|
20
|
+
## [param include_properties] is set, the file's value or the default where it sets none.
|
|
21
|
+
## [param depth] -1 reads every level.
|
|
21
22
|
static func scene_tree(scene_path: String, depth: int, include_properties: bool) -> Dictionary:
|
|
22
23
|
var laid: Dictionary = _laid(scene_path)
|
|
23
24
|
if not laid.get("ok", false):
|
|
@@ -41,6 +42,31 @@ static func scene_node(scene_path: String, node_path: String) -> Dictionary:
|
|
|
41
42
|
return {"ok": true, "name": found["name"], "type": found["type"], "properties": found["properties"]}
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
## [param said], the refusal for [param node_path], which [param scene_path] does not have, with
|
|
46
|
+
## where the path leaves the scene as [method Read.missing_below] says it.
|
|
47
|
+
static func missing_from(said: String, scene_path: String, node_path: String) -> String:
|
|
48
|
+
var laid: Dictionary = _laid(scene_path)
|
|
49
|
+
if not laid.get("ok", false):
|
|
50
|
+
return said
|
|
51
|
+
var nodes: Dictionary = laid["nodes"]
|
|
52
|
+
var parts: PackedStringArray = node_path.trim_prefix("./").split("/", false)
|
|
53
|
+
var at: String = "."
|
|
54
|
+
var reached: PackedStringArray = []
|
|
55
|
+
for part: String in parts:
|
|
56
|
+
var below: String = _joined(at, part)
|
|
57
|
+
if not nodes.has(below):
|
|
58
|
+
break
|
|
59
|
+
at = below
|
|
60
|
+
var _walked: bool = reached.append(part)
|
|
61
|
+
if reached.size() >= parts.size():
|
|
62
|
+
return said
|
|
63
|
+
var names: PackedStringArray = []
|
|
64
|
+
var children: Array[String] = nodes[at]["children"]
|
|
65
|
+
for child: String in children:
|
|
66
|
+
var _named: bool = names.append(str(nodes[child]["name"]))
|
|
67
|
+
return Read.missing_below(said, str(nodes["."]["name"]), reached, parts[reached.size()], names)
|
|
68
|
+
|
|
69
|
+
|
|
44
70
|
## Every node of [param scene_path] as {ok, nodes}, keyed by path, or a refusal.
|
|
45
71
|
static func _laid(scene_path: String) -> Dictionary:
|
|
46
72
|
if not FileAccess.file_exists(scene_path):
|
|
@@ -51,13 +77,11 @@ static func _laid(scene_path: String) -> Dictionary:
|
|
|
51
77
|
if loaded == null:
|
|
52
78
|
return {"ok": false, "error": "Failed to load %s as a scene" % scene_path}
|
|
53
79
|
if not loaded is PackedScene:
|
|
80
|
+
var kind: String = loaded.get_class()
|
|
81
|
+
var sort: String = ("an " if kind.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + kind
|
|
54
82
|
return {
|
|
55
83
|
"ok": false,
|
|
56
|
-
"error":
|
|
57
|
-
(
|
|
58
|
-
"%s is a %s, not a scene: resource_edit read reads its properties."
|
|
59
|
-
% [scene_path, loaded.get_class()]
|
|
60
|
-
)
|
|
84
|
+
"error": "%s is %s, not a scene: resource_edit read reads its properties." % [scene_path, sort]
|
|
61
85
|
}
|
|
62
86
|
var packed: PackedScene = loaded
|
|
63
87
|
var nodes: Dictionary = {}
|
|
@@ -82,8 +106,18 @@ static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: A
|
|
|
82
106
|
var refused: String = _lay(base, prefix, nodes, within)
|
|
83
107
|
if not refused.is_empty():
|
|
84
108
|
return refused
|
|
109
|
+
var ids: Array[int] = _unique_ids(state.get_path(), state.get_node_count())
|
|
85
110
|
for index: int in state.get_node_count():
|
|
86
111
|
var path: String = _joined(prefix, str(state.get_node_path(index)))
|
|
112
|
+
var recovered: bool = false
|
|
113
|
+
if not nodes.has(path) and index > 0 and _overrides(state, index) and ids[index] >= 0:
|
|
114
|
+
# The engine finds an entry whose node was renamed or moved in the scene it comes from by
|
|
115
|
+
# the node's unique_id, and keeps the node's new name.
|
|
116
|
+
var parent: String = _joined(prefix, str(state.get_node_path(index, true)))
|
|
117
|
+
var found: String = _by_id(nodes, _owner_of(nodes, parent, prefix), ids[index])
|
|
118
|
+
recovered = not found.is_empty()
|
|
119
|
+
if recovered:
|
|
120
|
+
path = found
|
|
87
121
|
var fresh: bool = not nodes.has(path)
|
|
88
122
|
if fresh:
|
|
89
123
|
var instance: PackedScene = state.get_node_instance(index)
|
|
@@ -98,6 +132,7 @@ static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: A
|
|
|
98
132
|
if not nodes.has(path):
|
|
99
133
|
return "%s holds no nodes" % instance.resource_path
|
|
100
134
|
nodes[path]["instanceOf"] = instance.resource_path
|
|
135
|
+
nodes[path]["editableChildren"] = false
|
|
101
136
|
elif state.is_node_instance_placeholder(index):
|
|
102
137
|
nodes[path] = _made("InstancePlaceholder")
|
|
103
138
|
nodes[path]["instanceOf"] = state.get_node_instance_placeholder(index)
|
|
@@ -106,9 +141,12 @@ static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: A
|
|
|
106
141
|
continue
|
|
107
142
|
else:
|
|
108
143
|
nodes[path] = _made(str(state.get_node_type(index)))
|
|
144
|
+
if ids[index] >= 0:
|
|
145
|
+
nodes[path]["uniqueId"] = ids[index]
|
|
109
146
|
# The name is the state's even for an entry laid already, since an inherited scene names its
|
|
110
147
|
# root itself; a root instanced inside another scene is renamed by that scene once laid.
|
|
111
|
-
|
|
148
|
+
if not recovered:
|
|
149
|
+
nodes[path]["name"] = str(state.get_node_name(index))
|
|
112
150
|
if fresh:
|
|
113
151
|
# The root of what is laid here is placed by whoever instanced it.
|
|
114
152
|
if path != prefix:
|
|
@@ -123,6 +161,86 @@ static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: A
|
|
|
123
161
|
held[str(state.get_node_property_name(index, property))] = state.get_node_property_value(
|
|
124
162
|
index, property
|
|
125
163
|
)
|
|
164
|
+
# Only the scene being read and the scenes it inherits share its root, the owner whose editable
|
|
165
|
+
# instances a write here sets; a file instanced below makes its own owner's editable.
|
|
166
|
+
if prefix == ".":
|
|
167
|
+
for editable: String in _editable_in(state.get_path()):
|
|
168
|
+
var at: String = _joined(prefix, editable)
|
|
169
|
+
var marked: Dictionary = nodes.get(at, {})
|
|
170
|
+
if marked.has("editableChildren"):
|
|
171
|
+
marked["editableChildren"] = true
|
|
172
|
+
return ""
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## The paths of the instances [param scene_path] gives editable children, from its root. SceneState
|
|
176
|
+
## does not give them to a script, so they are read off the scene's bundle.
|
|
177
|
+
static func _editable_in(scene_path: String) -> PackedStringArray:
|
|
178
|
+
var found: PackedStringArray = []
|
|
179
|
+
var loaded: Resource = ResourceLoader.load(scene_path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
180
|
+
if not loaded is PackedScene:
|
|
181
|
+
return found
|
|
182
|
+
var given: Variant = loaded.get("_bundled")
|
|
183
|
+
if not given is Dictionary:
|
|
184
|
+
return found
|
|
185
|
+
var bundled: Dictionary = given
|
|
186
|
+
var listed: Variant = bundled.get("editable_instances", [])
|
|
187
|
+
if listed is Array:
|
|
188
|
+
for one: Variant in listed:
|
|
189
|
+
var _kept: bool = found.append(str(one))
|
|
190
|
+
return found
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
## The unique_id of each node entry in the text scene at [param scene_path], in the order its state
|
|
194
|
+
## holds them, -1 where an entry has none. All -1 for a binary scene, and for a file whose entries do
|
|
195
|
+
## not number [param count], since SceneState does not give the ids to a script.
|
|
196
|
+
static func _unique_ids(scene_path: String, count: int) -> Array[int]:
|
|
197
|
+
var ids: Array[int] = []
|
|
198
|
+
var _sized: int = ids.resize(count)
|
|
199
|
+
ids.fill(-1)
|
|
200
|
+
if not scene_path.ends_with(".tscn") or not FileAccess.file_exists(scene_path):
|
|
201
|
+
return ids
|
|
202
|
+
var read: Array[int] = []
|
|
203
|
+
var pattern: RegEx = RegEx.create_from_string(" unique_id=(\\d+)")
|
|
204
|
+
for line: String in FileAccess.get_file_as_string(scene_path).split("\n"):
|
|
205
|
+
if line.begins_with("[node "):
|
|
206
|
+
var found: RegExMatch = pattern.search(line)
|
|
207
|
+
read.append(int(found.get_string(1)) if found != null else -1)
|
|
208
|
+
return read if read.size() == count else ids
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
## Whether entry [param index] of [param state] only sets values on a node another file makes.
|
|
212
|
+
static func _overrides(state: SceneState, index: int) -> bool:
|
|
213
|
+
return (
|
|
214
|
+
state.get_node_instance(index) == null
|
|
215
|
+
and not state.is_node_instance_placeholder(index)
|
|
216
|
+
and str(state.get_node_type(index)).is_empty()
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
## The node whose file owns what sits under [param parent]: the nearest instance at or above it, or
|
|
221
|
+
## [param prefix], the root of what is being laid, as the engine searches.
|
|
222
|
+
static func _owner_of(nodes: Dictionary, parent: String, prefix: String) -> String:
|
|
223
|
+
var at: String = parent
|
|
224
|
+
while at != prefix and at != "." and nodes.has(at):
|
|
225
|
+
var entry: Dictionary = nodes[at]
|
|
226
|
+
if entry.has("instanceOf"):
|
|
227
|
+
break
|
|
228
|
+
at = "." if not at.contains("/") else at.get_base_dir()
|
|
229
|
+
return at if nodes.has(at) else prefix
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
## The path of the node [param owner] owns whose unique_id is [param id], or "": one under an instance
|
|
233
|
+
## nested below [param owner] belongs to that instance's file, whose ids are numbered apart.
|
|
234
|
+
static func _by_id(nodes: Dictionary, owner: String, id: int) -> String:
|
|
235
|
+
for path: String in nodes:
|
|
236
|
+
if path == owner or not (owner == "." or path.begins_with(owner + "/")):
|
|
237
|
+
continue
|
|
238
|
+
var entry: Dictionary = nodes[path]
|
|
239
|
+
if Read.as_int(entry.get("uniqueId", -1), -1) != id:
|
|
240
|
+
continue
|
|
241
|
+
var above: String = "." if not path.contains("/") else path.get_base_dir()
|
|
242
|
+
if _owner_of(nodes, above, owner) == owner:
|
|
243
|
+
return path
|
|
126
244
|
return ""
|
|
127
245
|
|
|
128
246
|
|
|
@@ -162,12 +280,18 @@ static func _entry(
|
|
|
162
280
|
# The root's file is the scene being read, so only another node's names a second scene.
|
|
163
281
|
if path != "." and node.has("instanceOf"):
|
|
164
282
|
data["instanceOf"] = node["instanceOf"]
|
|
283
|
+
if node.has("editableChildren"):
|
|
284
|
+
data["editableChildren"] = node["editableChildren"]
|
|
165
285
|
if include_properties:
|
|
166
286
|
var given: Dictionary = node["properties"]
|
|
167
287
|
data["properties"] = _stored(str(node["type"]), given)
|
|
288
|
+
var below: Array[String] = node["children"]
|
|
289
|
+
# Counted rather than left as an empty list, which reads as a node with no children.
|
|
168
290
|
if depth >= 0 and current_depth >= depth:
|
|
291
|
+
if not below.is_empty():
|
|
292
|
+
data["childrenNotListed"] = below.size()
|
|
169
293
|
return data
|
|
170
|
-
for child: String in
|
|
294
|
+
for child: String in below:
|
|
171
295
|
children.append(_entry(nodes, child, include_properties, depth, current_depth + 1))
|
|
172
296
|
return data
|
|
173
297
|
|
|
@@ -129,7 +129,8 @@ func _answer(operation: String, params: Dictionary, answer_path: String) -> void
|
|
|
129
129
|
)
|
|
130
130
|
quit(1)
|
|
131
131
|
return
|
|
132
|
-
|
|
132
|
+
# Keys in the order they were put in, which for a map read is the order the resource keeps.
|
|
133
|
+
var written: bool = answer.store_string(JSON.stringify(payload, "", false))
|
|
133
134
|
answer.close()
|
|
134
135
|
if not written:
|
|
135
136
|
_log.error("Could not write the answer to " + answer_path)
|
|
@@ -590,7 +590,7 @@ static func _listed(type: int) -> bool:
|
|
|
590
590
|
|
|
591
591
|
## [param value] as a caller would write it, for a refusal to quote.
|
|
592
592
|
func shown(value: Variant) -> String:
|
|
593
|
-
return JSON.stringify(_values.serialize_value(value))
|
|
593
|
+
return JSON.stringify(_values.serialize_value(value), "", false)
|
|
594
594
|
|
|
595
595
|
|
|
596
596
|
## A dictionary carrying its own type name, as the serialiser writes it.
|
|
@@ -22,6 +22,9 @@ extends RefCounted
|
|
|
22
22
|
# number was expected. Absent keys never reach here: the callers pass their own default to
|
|
23
23
|
# `Dictionary.get`, so what arrives is that default.
|
|
24
24
|
|
|
25
|
+
## How many of a node's children a refusal for a path missing below it names.
|
|
26
|
+
const NAMED_CHILDREN: int = 12
|
|
27
|
+
|
|
25
28
|
|
|
26
29
|
static func as_int(value: Variant, fallback: int = 0) -> int:
|
|
27
30
|
if value is int:
|
|
@@ -99,3 +102,40 @@ static func json_or_null(text: String) -> Variant:
|
|
|
99
102
|
if json.parse(text) != OK:
|
|
100
103
|
return null
|
|
101
104
|
return json.data
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
## [param said], the refusal for [param node_path] found nowhere in a scene whose root is called
|
|
108
|
+
## [param root_name], with how paths are read when the path starts with that name and the root has
|
|
109
|
+
## no child called that. Paths go from the root, so its own name there names a child of it, and a
|
|
110
|
+
## caller who read the root as Main in a tree wrote Main/Body and was told only that nothing is there.
|
|
111
|
+
static func from_the_root(said: String, node_path: String, root_name: String) -> String:
|
|
112
|
+
var first: String = node_path.get_slice("/", 0)
|
|
113
|
+
if root_name.is_empty() or first != root_name:
|
|
114
|
+
return said
|
|
115
|
+
var rest: String = node_path.substr(first.length() + 1)
|
|
116
|
+
var how: String = 'Paths go from the root, %s, whose own path is "."' % root_name
|
|
117
|
+
if not rest.is_empty():
|
|
118
|
+
how += ', so this one is "%s"' % rest
|
|
119
|
+
return "%s%s %s." % [said, "" if said.ends_with(".") else ".", how]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## [param said], the refusal for a path a scene does not have, with where the path leaves it: the
|
|
123
|
+
## last node of the path it has, [param reached] from the root, called [param root_name], when that
|
|
124
|
+
## is empty; the name below it that is missing; and [param children], the names that node has, as
|
|
125
|
+
## after a node was moved or renamed. Names up to [constant NAMED_CHILDREN] of them.
|
|
126
|
+
static func missing_below(
|
|
127
|
+
said: String, root_name: String, reached: PackedStringArray, missing: String, children: PackedStringArray
|
|
128
|
+
) -> String:
|
|
129
|
+
var shown: String = ", ".join(children.slice(0, NAMED_CHILDREN))
|
|
130
|
+
if children.size() > NAMED_CHILDREN:
|
|
131
|
+
shown += " and %d more" % (children.size() - NAMED_CHILDREN)
|
|
132
|
+
return (
|
|
133
|
+
"%s%s %s has no %s; %s."
|
|
134
|
+
% [
|
|
135
|
+
said,
|
|
136
|
+
"" if said.ends_with(".") else ".",
|
|
137
|
+
"The root, %s," % root_name if reached.is_empty() else "/".join(reached),
|
|
138
|
+
missing,
|
|
139
|
+
"its children are " + shown if not children.is_empty() else "it has no children",
|
|
140
|
+
]
|
|
141
|
+
)
|
|
@@ -664,8 +664,12 @@ func _serialize_array(value: Array) -> Array:
|
|
|
664
664
|
|
|
665
665
|
# Base64 rather than a list of numbers. A byte array is a texture, a sound or a save file often
|
|
666
666
|
# enough that the list would be the whole answer, megabytes of it, and unreadable either way.
|
|
667
|
+
#
|
|
668
|
+
# The engine's encoder raises an error on an empty array, and an empty tile_map_data is every
|
|
669
|
+
# TileMapLayer with nothing painted yet, so a read of one carried an engine error in its answer.
|
|
667
670
|
func _serialize_bytes(value: PackedByteArray) -> Dictionary:
|
|
668
|
-
|
|
671
|
+
var text: String = "" if value.is_empty() else Marshalls.raw_to_base64(value)
|
|
672
|
+
return {"base64": text, "size": value.size(), "_type": "PackedByteArray"}
|
|
669
673
|
|
|
670
674
|
|
|
671
675
|
# The packed vector and colour arrays, each element serialised the way one of them on its own is.
|