gdharness 1.2.41 → 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.
@@ -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
+ )
@@ -31,13 +31,26 @@ const HeadlessCheck = preload("headless_check.gd")
31
31
  const PUTTING_BACK_PASSES: int = 4
32
32
  const PropertyValues = preload("property_values.gd")
33
33
  const Read = preload("reading.gd")
34
+ ## The names Object.ConnectFlags gives each bit. The number alone left a reader guessing that 2 is
35
+ ## the persistence every connection in a saved scene carries.
36
+ const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
37
+ Object.CONNECT_DEFERRED: "deferred",
38
+ Object.CONNECT_PERSIST: "persist",
39
+ Object.CONNECT_ONE_SHOT: "one_shot",
40
+ Object.CONNECT_REFERENCE_COUNTED: "reference_counted",
41
+ Object.CONNECT_APPEND_SOURCE_OBJECT: "append_source_object",
42
+ }
34
43
 
35
44
  ## Whether a scan asked for through [method scan_later] is still to start, which a caller waiting
36
45
  ## for the editor's files to settle has to wait through as well.
37
46
  static var scan_due: bool = false
47
+ ## The files a headless save named by path alone because the uid cache holds no uid for them,
48
+ ## gathered for the answer to the call that saved, the one call its engine runs.
49
+ static var by_path: PackedStringArray = []
38
50
 
39
51
  var root: Node = null
40
52
  var path: String = ""
53
+ ## The path of each node a scene this one inherits gives, with the file that makes it.
41
54
  var _base_paths: Dictionary = {}
42
55
  var _state: SceneState = null
43
56
  ## What the file stores on each node it was opened with, by the node's instance id, so a node keeps
@@ -78,9 +91,16 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
78
91
  return {"ok": false, "error": "Failed to instantiate " + path}
79
92
  _state = packed.get_state()
80
93
  var base: SceneState = _state.get_base_scene_state()
94
+ # Each node a base gives, with the file that makes it: the deepest base listing it as more than
95
+ # values set on a node from further down.
81
96
  while base != null:
82
97
  for index: int in base.get_node_count():
83
- _base_paths[_relative(str(base.get_node_path(index)))] = true
98
+ var given: String = _relative(str(base.get_node_path(index)))
99
+ var made: bool = (
100
+ base.get_node_instance(index) != null or not str(base.get_node_type(index)).is_empty()
101
+ )
102
+ if made or not _base_paths.has(given):
103
+ _base_paths[given] = base.get_path()
84
104
  base = base.get_base_scene_state()
85
105
  # On a read as well, which would otherwise answer what the scripts set as what the file holds.
86
106
  if not Engine.is_editor_hint():
@@ -123,6 +143,13 @@ func asked(node: Node, names: Array) -> void:
123
143
  _reached.merge(HeadlessCheck.reached(node, property))
124
144
 
125
145
 
146
+ ## Takes [param resource], built into the scene, as one the tool changed, which a write saves as it is.
147
+ func reached_into(resource: Resource) -> void:
148
+ var id: String = resource.get_scene_unique_id()
149
+ if not id.is_empty():
150
+ _reached[id] = true
151
+
152
+
126
153
  ## Takes each copy in [param copies], keyed by the node it was made from, as holding what that node's
127
154
  ## file entry holds.
128
155
  func copied(copies: Dictionary) -> void:
@@ -151,12 +178,45 @@ func refuse(error: String) -> Dictionary:
151
178
  return {"ok": false, "error": error}
152
179
 
153
180
 
181
+ ## Each bit set in [param flags], a connection's, by name, and whatever bits are left unnamed as one
182
+ ## number.
183
+ static func flag_names(flags: int) -> Array[String]:
184
+ var names: Array[String] = []
185
+ var rest: int = flags
186
+ for bit: int in CONNECT_FLAG_NAMES:
187
+ if flags & bit:
188
+ names.append(CONNECT_FLAG_NAMES[bit])
189
+ rest &= ~bit
190
+ if rest != 0:
191
+ names.append(str(rest))
192
+ return names
193
+
194
+
154
195
  ## [param said], the refusal for [param node_path] found nowhere in this scene, with how a path is
155
- ## read when it starts with the root's own name.
196
+ ## read when it starts with the root's own name, and otherwise with the last node of the path the
197
+ ## scene has and what is under it, as after a node was moved or renamed.
156
198
  func not_found(said: String, node_path: String) -> String:
157
- if root == null or root.has_node(NodePath(node_path.get_slice("/", 0))):
199
+ if root == null:
200
+ return said
201
+ if not root.has_node(NodePath(node_path.get_slice("/", 0))):
202
+ var hinted: String = Read.from_the_root(said, node_path, str(root.name))
203
+ if hinted != said:
204
+ return hinted
205
+ var parts: PackedStringArray = node_path.trim_prefix("./").split("/", false)
206
+ var at: Node = root
207
+ var reached: PackedStringArray = []
208
+ for part: String in parts:
209
+ var below: Node = at.get_node_or_null(NodePath(part))
210
+ if below == null:
211
+ break
212
+ at = below
213
+ var _walked: bool = reached.append(part)
214
+ if reached.size() >= parts.size():
158
215
  return said
159
- return Read.from_the_root(said, node_path, str(root.name))
216
+ var names: PackedStringArray = []
217
+ for child: Node in at.get_children():
218
+ var _named: bool = names.append(str(child.name))
219
+ return Read.missing_below(said, str(root.name), reached, parts[reached.size()], names)
160
220
 
161
221
 
162
222
  func close() -> void:
@@ -165,6 +225,27 @@ func close() -> void:
165
225
  root = null
166
226
 
167
227
 
228
+ ## What the file holds under the instance at [param at] that goes once its children are not
229
+ ## editable, as {values, nodes}: the paths of the instance's nodes it sets values on, which stay with
230
+ ## the instance's own values, and of the nodes the scene added below one of them, which are gone,
231
+ ## since the packer skips everything below a node of an instance that is not editable. A node added
232
+ ## straight under [param at] is the scene's own and stays.
233
+ func held_under(at: String) -> Dictionary:
234
+ var values: Array[String] = []
235
+ var nodes: Array[String] = []
236
+ for index: int in _state.get_node_count():
237
+ var entry: String = _relative(str(_state.get_node_path(index)))
238
+ var parent: String = _relative(str(_state.get_node_path(index, true)))
239
+ var values_only: bool = (
240
+ _state.get_node_instance(index) == null and str(_state.get_node_type(index)).is_empty()
241
+ )
242
+ if values_only and entry.begins_with(at + "/"):
243
+ values.append(entry)
244
+ elif not values_only and parent.begins_with(at + "/"):
245
+ nodes.append(entry)
246
+ return {"values": values, "nodes": nodes}
247
+
248
+
168
249
  ## The path of [param node] as a tool names it: "." for the root.
169
250
  func path_of(node: Node) -> String:
170
251
  return str(root.get_path_to(node))
@@ -175,16 +256,46 @@ func path_of(node: Node) -> String:
175
256
  func foreign(node: Node) -> String:
176
257
  if node == root or node.owner == root:
177
258
  return ""
178
- if node.owner != null and root.is_editable_instance(node.owner):
259
+ if node.owner == null:
260
+ return (
261
+ "%s is made by a script as the scene is instanced, and no scene file saves anything about it."
262
+ % path_of(node)
263
+ )
264
+ if root.is_editable_instance(node.owner):
179
265
  return ""
180
- var owner_path: String = path_of(node.owner) if node.owner != null else "?"
181
- return (
266
+ var said: String = (
182
267
  (
183
268
  "%s belongs to the scene instanced at %s (%s), and this scene saves nothing about it. Edit "
184
- + "that scene instead."
269
+ + "that scene instead, which changes every instance of it"
185
270
  )
186
- % [path_of(node), owner_path, node.owner.scene_file_path if node.owner != null else "?"]
271
+ % [path_of(node), path_of(node.owner), node.owner.scene_file_path]
187
272
  )
273
+ var steps: PackedStringArray = []
274
+ for closed: String in _closed_around(node):
275
+ var _added: bool = steps.append("nodePath %s editableChildren true" % closed)
276
+ if steps.is_empty():
277
+ return said + "."
278
+ return (
279
+ said
280
+ + ", or have this scene keep its own values for this instance with scene_node set "
281
+ + ", then ".join(steps)
282
+ + "."
283
+ )
284
+
285
+
286
+ ## The instances around [param node] whose children are not editable, outermost first: each must be
287
+ ## made editable, in that order, before this scene keeps values of its own for [param node], since
288
+ ## the packer skips every node under an instance that is not. Empty when nothing would let it.
289
+ func _closed_around(node: Node) -> PackedStringArray:
290
+ var closed: PackedStringArray = []
291
+ var instance: Node = node.owner
292
+ while instance != root:
293
+ if instance == null:
294
+ return []
295
+ if not root.is_editable_instance(instance):
296
+ var _at: int = closed.insert(0, path_of(instance))
297
+ instance = instance.owner
298
+ return closed
188
299
 
189
300
 
190
301
  ## Why [param node] cannot be moved, renamed, copied or deleted, or "": the scene may only
@@ -201,10 +312,10 @@ func fixed(node: Node) -> String:
201
312
  if _base_paths.has(at):
202
313
  return (
203
314
  (
204
- "%s comes from the scene %s inherits from, which creates it again on load. Edit that "
315
+ "%s is made by %s, a scene %s inherits, which creates it again on load. Edit that "
205
316
  + "scene instead."
206
317
  )
207
- % [at, path]
318
+ % [at, _base_paths[at], path]
208
319
  )
209
320
  return ""
210
321
 
@@ -332,10 +443,25 @@ func unusable_name(parent: Node, child_name: String, moving: Node = null) -> Str
332
443
 
333
444
  ## The names of the properties this scene's own file sets on [param node], whatever their values.
334
445
  func set_here(node: Node) -> Dictionary:
446
+ return set_at(path_of(node))
447
+
448
+
449
+ ## Where the values of [param node], at [param at] in the file, come from when the file does not set
450
+ ## them: "" for a node this scene makes, which holds them all, or the scene that makes the node, the
451
+ ## one it is instanced from or one this scene inherits.
452
+ func values_from(node: Node, at: String) -> String:
453
+ if node != root and not node.scene_file_path.is_empty():
454
+ return node.scene_file_path
455
+ if node != root and node.owner != root:
456
+ return node.owner.scene_file_path if node.owner != null else ""
457
+ return str(_base_paths.get(at, ""))
458
+
459
+
460
+ ## The names of the properties this scene's own file sets on the node at [param at].
461
+ func set_at(at: String) -> Dictionary:
335
462
  var names: Dictionary = {}
336
463
  if _state == null:
337
464
  return names
338
- var at: String = path_of(node)
339
465
  for index: int in _state.get_node_count():
340
466
  if _relative(str(_state.get_node_path(index))) != at:
341
467
  continue
@@ -447,17 +573,85 @@ static func save_packed(packed: PackedScene, scene_path: String) -> Dictionary:
447
573
  static func save_keeping_uid(resource: Resource, file_path: String) -> Error:
448
574
  if Engine.is_editor_hint():
449
575
  return ResourceSaver.save(resource, file_path)
450
- var uid: int = _uid_in(file_path)
576
+ var uid: int = uid_in(file_path)
451
577
  var saving: Error = ResourceSaver.save(resource, file_path)
452
578
  if saving != OK:
453
579
  return saving
454
580
  if uid == ResourceUID.INVALID_ID:
455
581
  uid = ResourceUID.create_id()
456
- return ResourceSaver.set_uid(file_path, uid)
582
+ var marked: Error = ResourceSaver.set_uid(file_path, uid)
583
+ if marked != OK:
584
+ return marked
585
+ return _with_external_uids(file_path)
586
+
587
+
588
+ ## Puts back the uid on each external resource the text file at [param file_path] names. Only the
589
+ ## editor's file system tells the saver a path's uid, so a headless save writes every reference by
590
+ ## path alone, and each breaks when the file it names is moved.
591
+ static func _with_external_uids(file_path: String) -> Error:
592
+ if not (file_path.ends_with(".tscn") or file_path.ends_with(".tres")):
593
+ return OK
594
+ var lines: PackedStringArray = FileAccess.get_file_as_string(file_path).split("\n")
595
+ var pattern: RegEx = RegEx.create_from_string(' path="([^"]+)"')
596
+ var changed: bool = false
597
+ for at: int in lines.size():
598
+ var line: String = lines[at]
599
+ if not line.begins_with("[ext_resource ") or line.contains(' uid="'):
600
+ continue
601
+ var named: RegExMatch = pattern.search(line)
602
+ if named == null:
603
+ continue
604
+ var uid: String = reference_uid(named.get_string(1))
605
+ if not uid.is_empty():
606
+ lines[at] = line.replace(' path="', uid + ' path="')
607
+ changed = true
608
+ if not changed:
609
+ return OK
610
+ var file: FileAccess = FileAccess.open(file_path, FileAccess.WRITE)
611
+ if file == null:
612
+ return FileAccess.get_open_error()
613
+ if not file.store_string("\n".join(lines)):
614
+ return ERR_FILE_CANT_WRITE
615
+ file.close()
616
+ return OK
617
+
618
+
619
+ ## [method uid_attribute] for a reference about to be written, noting [param named] under
620
+ ## [member by_path] when it is left to be named by path.
621
+ static func reference_uid(named: String) -> String:
622
+ var uid: String = uid_attribute(named)
623
+ if uid.is_empty():
624
+ note_by_path(named)
625
+ return uid
626
+
627
+
628
+ ## Notes [param named], a reference just written by path alone, under [member by_path] when the uid
629
+ ## cache holds nothing for it: a file made since the project was last scanned, whose uid the cache
630
+ ## learns from the next scan. Not a file whose uid the cache gives to another, which a scan leaves.
631
+ static func note_by_path(named: String) -> void:
632
+ if Engine.is_editor_hint() or not FileAccess.file_exists(named) or by_path.has(named):
633
+ return
634
+ var own: int = uid_in(named)
635
+ if own == ResourceUID.INVALID_ID and FileAccess.file_exists(named + ".uid"):
636
+ own = ResourceUID.text_to_id(FileAccess.get_file_as_string(named + ".uid").strip_edges())
637
+ if own == ResourceUID.INVALID_ID or not ResourceUID.has_id(own):
638
+ var _noted: bool = by_path.append(named)
639
+
640
+
641
+ ## The uid attribute a reference to [param named] is written with, as ' uid="uid://..."', or "".
642
+ ##
643
+ ## Only a uid the uid cache gives that path, which is what the loader resolves it by. A uid the cache
644
+ ## does not know (a file made with no editor since the last scan) is answered with a warning on every
645
+ ## load, and one it gives another file (a scene copied whole keeps its original's) loads that file.
646
+ static func uid_attribute(named: String) -> String:
647
+ var id: int = ResourceLoader.get_resource_uid(named)
648
+ if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id) or ResourceUID.get_id_path(id) != named:
649
+ return ""
650
+ return ' uid="%s"' % ResourceUID.id_to_text(id)
457
651
 
458
652
 
459
653
  ## The uid the file at [param file_path] declares, or [constant ResourceUID.INVALID_ID].
460
- static func _uid_in(file_path: String) -> int:
654
+ static func uid_in(file_path: String) -> int:
461
655
  if not FileAccess.file_exists(file_path):
462
656
  return ResourceUID.INVALID_ID
463
657
  var file: FileAccess = FileAccess.open(file_path, FileAccess.READ)
@@ -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.