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.
Files changed (31) hide show
  1. package/build/cli.js +103 -38
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +14 -2
  3. package/build/godot/addons/gdharness_editor/bridge_client.gd +2 -1
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +134 -10
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +56 -15
  6. package/build/godot/addons/gdharness_editor/property_values.gd +1 -1
  7. package/build/godot/addons/gdharness_editor/reading.gd +40 -0
  8. package/build/godot/addons/gdharness_editor/scene_file.gd +215 -12
  9. package/build/godot/addons/gdharness_editor/serialisation.gd +5 -1
  10. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +39 -5
  11. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +68 -15
  12. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +346 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd.uid +1 -0
  14. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +438 -0
  15. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd.uid +1 -0
  16. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +102 -60
  17. package/build/godot/addons/gdharness_runtime/reading.gd +40 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +3 -1
  19. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +2 -2
  20. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +11 -6
  21. package/build/godot/addons/gdharness_runtime/runtime_values.gd +3 -1
  22. package/build/godot/addons/gdharness_runtime/serialisation.gd +5 -1
  23. package/build/godot/operations/animation_reads.gd +14 -2
  24. package/build/godot/operations/editor_tools.gd +28 -1
  25. package/build/godot/operations/file_reads.gd +134 -10
  26. package/build/godot/operations/godot_operations.gd +2 -1
  27. package/build/godot/operations/property_values.gd +1 -1
  28. package/build/godot/operations/reading.gd +40 -0
  29. package/build/godot/operations/serialisation.gd +5 -1
  30. package/build/index.js +103 -38
  31. 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 on a node that
19
- ## instances another scene and every stored property when [param include_properties] is set, the
20
- ## file's value or the default where it sets none. [param depth] -1 reads every level.
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
- nodes[path]["name"] = str(state.get_node_name(index))
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 node["children"]:
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
- var written: bool = answer.store_string(JSON.stringify(payload))
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
- return {"base64": Marshalls.raw_to_base64(value), "size": value.size(), "_type": "PackedByteArray"}
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.