gdharness 1.2.41 → 1.3.0

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 (30) hide show
  1. package/build/cli.js +643 -378
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -4
  3. package/build/godot/addons/gdharness_editor/bridge_client.gd +1 -1
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +134 -10
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +34 -4
  6. package/build/godot/addons/gdharness_editor/property_values.gd +4 -9
  7. package/build/godot/addons/gdharness_editor/reading.gd +25 -0
  8. package/build/godot/addons/gdharness_editor/scene_file.gd +209 -15
  9. package/build/godot/addons/gdharness_editor/serialisation.gd +5 -1
  10. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +9 -9
  11. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +346 -0
  12. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd.uid +1 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +438 -0
  14. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +91 -49
  16. package/build/godot/addons/gdharness_runtime/reading.gd +25 -0
  17. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +2 -2
  18. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +29 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_input.gd +2 -7
  20. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +6 -14
  21. package/build/godot/addons/gdharness_runtime/serialisation.gd +5 -1
  22. package/build/godot/operations/animation_reads.gd +7 -4
  23. package/build/godot/operations/editor_tools.gd +19 -1
  24. package/build/godot/operations/file_reads.gd +134 -10
  25. package/build/godot/operations/property_values.gd +4 -9
  26. package/build/godot/operations/reading.gd +25 -0
  27. package/build/godot/operations/serialisation.gd +5 -1
  28. package/build/keeper.js +10 -4
  29. package/package.json +1 -2
  30. package/build/index.js +0 -45043
@@ -62,10 +62,13 @@ static func _bare(scene_path: String, node_path: String, kind: String) -> Array:
62
62
  var type: String = str(read.get("type", ""))
63
63
  if not ClassDB.class_exists(type) or not ClassDB.is_parent_class(type, kind):
64
64
  var said: String = "%s not found at: %s" % [kind, node_path]
65
- if read.is_empty() and FileReads.scene_node(scene_path, node_path.get_slice("/", 0)).is_empty():
66
- said = Read.from_the_root(
67
- said, node_path, str(FileReads.scene_node(scene_path, ".").get("name", ""))
68
- )
65
+ if read.is_empty():
66
+ var hinted: String = said
67
+ if FileReads.scene_node(scene_path, node_path.get_slice("/", 0)).is_empty():
68
+ hinted = Read.from_the_root(
69
+ said, node_path, str(FileReads.scene_node(scene_path, ".").get("name", ""))
70
+ )
71
+ said = hinted if hinted != said else FileReads.missing_from(said, scene_path, node_path)
69
72
  return [null, {"ok": false, "error": said}]
70
73
  var bare: Node = ClassDB.instantiate(type)
71
74
  bare.name = str(read["name"])
@@ -518,7 +518,7 @@ func _handle_message(json_string: String) -> void:
518
518
 
519
519
 
520
520
  ## Says [param tool_name] was not run because the editor is scanning or importing, for the server to
521
- ## ask again once that is over. An older server reads it as an ordinary refusal.
521
+ ## ask again once that is over.
522
522
  func send_busy(request_id: String, tool_name: String) -> void:
523
523
  _send_message(
524
524
  {
@@ -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
 
@@ -248,7 +248,8 @@ static func entries(lines: PackedStringArray) -> Dictionary:
248
248
 
249
249
 
250
250
  ## Each property the main resource of [param after] sets differently from [param before], leaving
251
- ## out [param asked], as [name, whether before sets it, the text before gives it].
251
+ ## out [param asked], as [name, whether before sets it, the text before gives it, the text after
252
+ ## gives it or null].
252
253
  static func main_changes(before: String, after: String, asked: Dictionary) -> Array[Array]:
253
254
  var was: Dictionary = _main_entries(before)
254
255
  var now: Dictionary = _main_entries(after)
@@ -259,10 +260,25 @@ static func main_changes(before: String, after: String, asked: Dictionary) -> Ar
259
260
  var name: String = str(key)
260
261
  if asked.has(name) or was.get(name) == now.get(name):
261
262
  continue
262
- changed.append([name, was.has(name), str(was.get(name, ""))])
263
+ changed.append([name, was.has(name), str(was.get(name, "")), now.get(name)])
263
264
  return changed
264
265
 
265
266
 
267
+ ## Whether [param was] and [param now], the texts two files give a property or null where one sets
268
+ ## nothing, read as the same value, nothing standing for [param default]. A file written by hand
269
+ ## spells a value its own way, 1 for 1.0, and may set a default, which the saver leaves out.
270
+ static func reads_the_same(was: Variant, now: Variant, default: Variant) -> bool:
271
+ var values: Array = []
272
+ for text: Variant in [was, now]:
273
+ if text == null:
274
+ values.append(default)
275
+ elif "Resource(" in str(text):
276
+ return false
277
+ else:
278
+ values.append(str_to_var(str(text)))
279
+ return PropertyValues.same(values[0], values[1])
280
+
281
+
266
282
  static func _main_entries(text: String) -> Dictionary:
267
283
  var main: Dictionary = sections(text).get("resource", {})
268
284
  return entries_of(main)
@@ -298,7 +314,8 @@ static func scripted_sections(text: String) -> Dictionary:
298
314
 
299
315
 
300
316
  ## Each property an embedded resource of [param after] sets differently from [param before], as
301
- ## [its id, name, whether before sets it, the text before gives it]. Only those with a script that
317
+ ## [its id, name, whether before sets it, the text before gives it, the text after gives it or
318
+ ## null]. Only those with a script that
302
319
  ## is not a tool are compared, since only a script runs differently here, and those in
303
320
  ## [param reached_ids], which the tool changed, are left out.
304
321
  static func embedded_changes(before: String, after: String, reached_ids: Dictionary) -> Array[Array]:
@@ -321,7 +338,7 @@ static func embedded_changes(before: String, after: String, reached_ids: Diction
321
338
  for entry: Variant in all:
322
339
  var name: String = str(entry)
323
340
  if was.get(name) != now.get(name):
324
- changed.append([id, name, was.has(name), str(was.get(name, ""))])
341
+ changed.append([id, name, was.has(name), str(was.get(name, "")), now.get(name)])
325
342
  return changed
326
343
 
327
344
 
@@ -345,11 +362,15 @@ static func settle_saved(
345
362
  var embedded: Array[Array] = []
346
363
  for found: Array in embedded_changes(before, after, reached_ids):
347
364
  var holder: Resource = by_id.get(found[0])
365
+ if holder != null and _unchanged(holder, found.slice(1)):
366
+ continue
348
367
  if found[2] or holder == null or not at_default(holder, str(found[1])):
349
368
  embedded.append(found)
350
369
  var top: Array[Array] = []
351
370
  if main != null:
352
371
  for found: Array in main_changes(before, after, asked):
372
+ if _unchanged(main, found):
373
+ continue
353
374
  if found[1] or not at_default(main, str(found[0])):
354
375
  top.append(found)
355
376
  if embedded.is_empty() and top.is_empty():
@@ -382,6 +403,15 @@ static func settle_saved(
382
403
  return ""
383
404
 
384
405
 
406
+ ## Whether the saved text gives a property of [param object] what the file did, spelt another way.
407
+ ## [param change] is [name, whether the file set it, the text it gave, the text after or null].
408
+ static func _unchanged(object: Object, change: Array) -> bool:
409
+ var name: String = change[0]
410
+ var was_set: bool = change[1]
411
+ var was: Variant = change[2] if was_set else null
412
+ return reads_the_same(was, change[3], default_of(object, name))
413
+
414
+
385
415
  ## Every embedded resource [param values] reach, at any depth, by its id in the file.
386
416
  static func embedded_in(values: Array, found: Dictionary) -> Dictionary:
387
417
  for value: Variant in values:
@@ -600,18 +600,13 @@ func shown(value: Variant) -> String:
600
600
  ## not mine". A tag that names a type but lacks the keys to build it is left unhandled on purpose,
601
601
  ## so the caller can still read it against the type the property declares.
602
602
  func _parse_tagged_dictionary(value: Dictionary) -> Array:
603
- var type_tag: String = str(value.get("_type", value.get("type", "")))
603
+ var type_tag: String = str(value.get("_type", ""))
604
604
  if type_tag == "Resource":
605
605
  return _parse_resource_tag(value)
606
606
 
607
607
  # The shared builder is the same one the answer was written by, so every shape it writes comes
608
- # back as itself. `type` as the tag is read as `_type`: it is what these tools answered with
609
- # before the serialisers were made one, and a caller who kept an answer can still send it.
610
- var tagged: Dictionary = value
611
- if not value.has("_type") and not type_tag.is_empty():
612
- tagged = value.duplicate()
613
- tagged["_type"] = type_tag
614
- var rebuilt: Variant = _values.deserialize_value(tagged)
608
+ # back as itself.
609
+ var rebuilt: Variant = _values.deserialize_value(value)
615
610
  if not rebuilt is Dictionary:
616
611
  return [true, rebuilt]
617
612
 
@@ -660,7 +655,7 @@ func _parse_new_resource(type_tag: String, value: Dictionary) -> Array:
660
655
  var wanted: Dictionary = {}
661
656
  for key: Variant in value:
662
657
  var property: String = str(key)
663
- if property != "_type" and property != "type":
658
+ if property != "_type":
664
659
  wanted[property] = value[key]
665
660
  var refused: String = write_all(built, wanted)
666
661
  if not refused.is_empty():
@@ -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:
@@ -114,3 +117,25 @@ static func from_the_root(said: String, node_path: String, root_name: String) ->
114
117
  if not rest.is_empty():
115
118
  how += ', so this one is "%s"' % rest
116
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
+ )