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.
@@ -0,0 +1,346 @@
1
+ @tool
2
+ extends RefCounted
3
+
4
+ ## The two ways a scene keeps its own values for nodes another scene makes: by inheriting that scene,
5
+ ## or by instancing it with its children editable.
6
+
7
+ const FileReads = preload("../file_reads.gd")
8
+ const Paths = preload("scene_paths.gd")
9
+ const SceneFile = preload("../scene_file.gd")
10
+
11
+
12
+ ## Why a create at [param scene_path] is refused when a file is there already, which saving over
13
+ ## would replace nodes and all.
14
+ static func taken(scene_path: String) -> Dictionary:
15
+ return {
16
+ "ok": false,
17
+ "error":
18
+ (
19
+ (
20
+ "%s already exists, and scene_create op=create would replace everything in it. Edit it "
21
+ + "with scene_node, or create the scene under another path."
22
+ )
23
+ % scene_path
24
+ )
25
+ }
26
+
27
+
28
+ ## scene_create for [param args] naming a scene to inherit: [param scene_path], [param base_path]
29
+ ## and [param script_path] are the res:// paths it gives, the last "" when it gives no script.
30
+ static func inherited(
31
+ args: Dictionary, scene_path: String, base_path: String, script_path: String
32
+ ) -> Dictionary:
33
+ if scene_path.is_empty():
34
+ return {"ok": false, "error": "Missing scenePath"}
35
+ if args.has("rootNodeType"):
36
+ return {
37
+ "ok": false,
38
+ "error":
39
+ "inherits takes the root's class from %s, so rootNodeType is not given with it." % base_path
40
+ }
41
+ if FileAccess.file_exists(scene_path):
42
+ return taken(scene_path)
43
+ var script: Script = null
44
+ if not script_path.is_empty():
45
+ script = load(script_path) as Script
46
+ if script == null:
47
+ return {"ok": false, "error": "No script loads from " + script_path}
48
+ var uncompiled: String = SceneFile.uncompiled(script, script_path)
49
+ if not uncompiled.is_empty():
50
+ return {"ok": false, "error": uncompiled}
51
+ return inherit(scene_path, base_path, str(args.get("rootNodeName", "")), script)
52
+
53
+
54
+ ## Writes [param scene_path] as a scene inheriting [param base_path], its root named
55
+ ## [param root_name] (the base root's name when empty) and carrying [param script] when one is
56
+ ## given. Answers what scene_create answers, or a refusal.
57
+ ##
58
+ ## Written as the text the editor's New Inherited Scene saves rather than packed from an instance of
59
+ ## the base: a headless engine making that instance runs every script in the base, and the pack would
60
+ ## keep what they set as this scene's own values.
61
+ static func inherit(scene_path: String, base_path: String, root_name: String, script: Script) -> Dictionary:
62
+ var base: Dictionary = FileReads.scene_node(base_path, ".")
63
+ if not base.get("ok", false):
64
+ return {"ok": false, "error": "inherits: " + str(base.get("error", "%s holds no nodes" % base_path))}
65
+ var root_type: String = str(base["type"])
66
+ var name: String = str(base["name"]) if root_name.is_empty() else root_name
67
+ var unfit: String = SceneFile.unfit_name(name)
68
+ if not unfit.is_empty():
69
+ return {"ok": false, "error": unfit}
70
+ if script != null:
71
+ var extended: String = str(script.get_instance_base_type())
72
+ if not ClassDB.is_parent_class(root_type, extended):
73
+ return {
74
+ "ok": false,
75
+ "error":
76
+ (
77
+ "%s extends %s, so it cannot be on the %s root %s inherits."
78
+ % [script.resource_path, extended, root_type, base_path]
79
+ )
80
+ }
81
+ var folder: String = SceneFile.make_folder_for(scene_path)
82
+ if not folder.is_empty():
83
+ return {"ok": false, "error": folder}
84
+
85
+ var lines: PackedStringArray = [
86
+ '[gd_scene format=3 uid="%s"]' % ResourceUID.id_to_text(ResourceUID.create_id()),
87
+ "",
88
+ (
89
+ '[ext_resource type="PackedScene"%s path="%s" id="1_base"]'
90
+ % [SceneFile.reference_uid(base_path), base_path]
91
+ ),
92
+ ]
93
+ if script != null:
94
+ var _resourced: bool = lines.append(
95
+ (
96
+ '[ext_resource type="Script"%s path="%s" id="2_script"]'
97
+ % [SceneFile.reference_uid(script.resource_path), script.resource_path]
98
+ )
99
+ )
100
+ lines.append_array(["", '[node name="%s" instance=ExtResource("1_base")]' % name])
101
+ if script != null:
102
+ var _attached: bool = lines.append('script = ExtResource("2_script")')
103
+ var file: FileAccess = FileAccess.open(scene_path, FileAccess.WRITE)
104
+ if file == null or not file.store_string("\n".join(lines) + "\n"):
105
+ return {
106
+ "ok": false,
107
+ "error": "Could not write %s: %s" % [scene_path, error_string(FileAccess.get_open_error())]
108
+ }
109
+ file.close()
110
+
111
+ var loaded: PackedScene = ResourceLoader.load(scene_path, "PackedScene", ResourceLoader.CACHE_MODE_IGNORE)
112
+ var instanced: PackedScene = loaded.get_state().get_node_instance(0) if loaded != null else null
113
+ if instanced == null or instanced.resource_path != base_path:
114
+ var _removed: Error = DirAccess.remove_absolute(scene_path)
115
+ return {
116
+ "ok": false,
117
+ "error":
118
+ "%s did not load back as a scene inheriting %s, so it was removed." % [scene_path, base_path]
119
+ }
120
+ SceneFile.refresh(scene_path)
121
+ return {
122
+ "ok": true,
123
+ "scenePath": scene_path,
124
+ "inherits": base_path,
125
+ "rootNodeType": root_type,
126
+ "rootNodeName": name
127
+ }
128
+
129
+
130
+ ## Makes the children of [param node], an instance in [param scene], editable or not, answering why
131
+ ## it cannot or "". With them editable the scene keeps its own values for them, as the editor's
132
+ ## Editable Children does; made not editable again, the values it kept for them are dropped. The
133
+ ## caller has had [method SceneFile.foreign] refuse a node inside an instance that is not editable.
134
+ static func set_editable(scene: SceneFile, node: Node, editable: bool) -> String:
135
+ if node.owner == null or node.scene_file_path.is_empty():
136
+ return (
137
+ "editableChildren is a setting of an instance in this scene, and %s is not one."
138
+ % scene.path_of(node)
139
+ )
140
+ scene.root.set_editable_instance(node, editable)
141
+ return ""
142
+
143
+
144
+ ## Carries the move of a node of [param changed] from [param old_path] to [param new_path], both
145
+ ## from its root, into each other text scene of the project that names the node by path: one that
146
+ ## instances or inherits [param changed], at any depth. Its editable instances, its entries for the
147
+ ## node and for what is below it (their names and parents), and its connections are rewritten to
148
+ ## the new path. The engine finds
149
+ ## a moved node's entries by its unique_id, but an editable instance only by its path, so a scene
150
+ ## saved after the move lost the values it kept below one. The node paths each holds are carried
151
+ ## into [param carried], which holds those of [param changed] already; with [param new_path] "",
152
+ ## for a node deleted, they are only named. Answers followed, the scenes rewritten, and
153
+ ## notFollowed, each one left as it was with why, each only when it names any, with what
154
+ ## [param carried] gives.
155
+ static func follow(changed: String, old_path: String, new_path: String, carried: Paths) -> Dictionary:
156
+ var texts: Dictionary = {}
157
+ _read_scenes("res://", texts)
158
+ var kin: Array[String] = [changed]
159
+ var grew: bool = true
160
+ while grew:
161
+ grew = false
162
+ for scene_path: String in texts:
163
+ if kin.has(scene_path):
164
+ continue
165
+ var text: String = texts[scene_path]
166
+ for one: String in kin:
167
+ if text.contains(' path="%s"' % one):
168
+ kin.append(scene_path)
169
+ grew = true
170
+ break
171
+ var followed: Array[String] = []
172
+ var not_followed: Array[Dictionary] = []
173
+ for scene_path: String in kin:
174
+ if scene_path == changed:
175
+ continue
176
+ var moves: Dictionary = {}
177
+ if _inherits(scene_path, changed, texts):
178
+ moves[old_path] = new_path
179
+ var read: Dictionary = FileReads.scene_tree(scene_path, -1, false)
180
+ if not read.get("ok", false):
181
+ not_followed.append({"scenePath": scene_path, "reason": str(read.get("error", ""))})
182
+ continue
183
+ var tree: Dictionary = read["tree"]
184
+ _instances_of(tree, changed, old_path, new_path, moves)
185
+ var text: String = texts[scene_path]
186
+ var unsaved: String = SceneFile.unsaved_in_editor(scene_path)
187
+ var rewritten: String = text
188
+ if not new_path.is_empty():
189
+ rewritten = _rewritten(text, moves, old_path.get_base_dir() != new_path.get_base_dir())
190
+ if rewritten != text:
191
+ if not unsaved.is_empty():
192
+ not_followed.append({"scenePath": scene_path, "reason": unsaved})
193
+ continue
194
+ var file: FileAccess = FileAccess.open(scene_path, FileAccess.WRITE)
195
+ if file == null or not file.store_string(rewritten):
196
+ not_followed.append(
197
+ {"scenePath": scene_path, "reason": error_string(FileAccess.get_open_error())}
198
+ )
199
+ continue
200
+ file.close()
201
+ SceneFile.refresh(scene_path)
202
+ followed.append(scene_path)
203
+ if moves.is_empty() or not text.contains("NodePath("):
204
+ continue
205
+ if not unsaved.is_empty() and not new_path.is_empty():
206
+ not_followed.append({"scenePath": scene_path, "reason": unsaved})
207
+ continue
208
+ var before: int = carried.retargeted.size()
209
+ var unwritten: String = carried.carry_into(scene_path, moves)
210
+ if not unwritten.is_empty() and not followed.has(scene_path):
211
+ not_followed.append({"scenePath": scene_path, "reason": unwritten})
212
+ elif carried.retargeted.size() > before and not followed.has(scene_path):
213
+ followed.append(scene_path)
214
+ var answer: Dictionary = carried.answer()
215
+ if not followed.is_empty():
216
+ answer["followed"] = followed
217
+ if not not_followed.is_empty():
218
+ answer["notFollowed"] = not_followed
219
+ return answer
220
+
221
+
222
+ ## The text of every .tscn under [param directory] into [param texts], keyed by res:// path, leaving
223
+ ## out the engine's own folder and hidden ones.
224
+ static func _read_scenes(directory: String, texts: Dictionary) -> void:
225
+ for name: String in DirAccess.get_directories_at(directory):
226
+ if not name.begins_with("."):
227
+ _read_scenes(directory.path_join(name), texts)
228
+ for name: String in DirAccess.get_files_at(directory):
229
+ if name.ends_with(".tscn"):
230
+ texts[directory.path_join(name)] = FileAccess.get_file_as_string(directory.path_join(name))
231
+
232
+
233
+ ## Whether the scene at [param scene_path] inherits [param changed], through any number of scenes.
234
+ static func _inherits(scene_path: String, changed: String, texts: Dictionary) -> bool:
235
+ var seen: Array[String] = [scene_path]
236
+ var base: String = _base_of(str(texts.get(scene_path, "")))
237
+ while not base.is_empty() and not seen.has(base):
238
+ if base == changed:
239
+ return true
240
+ seen.append(base)
241
+ base = _base_of(str(texts.get(base, "")))
242
+ return false
243
+
244
+
245
+ ## The scene the root of [param text] inherits, or "": a root entry naming an instance and no parent.
246
+ static func _base_of(text: String) -> String:
247
+ for line: String in text.split("\n"):
248
+ if not line.begins_with("[node "):
249
+ continue
250
+ var instanced: RegExMatch = RegEx.create_from_string('instance=ExtResource\\("([^"]+)"\\)').search(
251
+ line
252
+ )
253
+ if instanced == null or line.contains(" parent="):
254
+ return ""
255
+ var named: RegExMatch = (
256
+ RegEx
257
+ . create_from_string('\\[ext_resource [^\\n]*path="([^"]+)" id="%s"\\]' % instanced.get_string(1))
258
+ . search(text)
259
+ )
260
+ return named.get_string(1) if named != null else ""
261
+ return ""
262
+
263
+
264
+ ## Into [param moves], the path below each instance of [param changed] in [param entry]'s tree that
265
+ ## [param old_path] was at, with where it is now, or "" when [param new_path] is.
266
+ static func _instances_of(
267
+ entry: Dictionary, changed: String, old_path: String, new_path: String, moves: Dictionary
268
+ ) -> void:
269
+ if str(entry.get("instanceOf", "")) == changed:
270
+ var at: String = str(entry["path"])
271
+ moves[_under(at, old_path)] = "" if new_path.is_empty() else _under(at, new_path)
272
+ var children: Array = entry.get("children", [])
273
+ for child: Variant in children:
274
+ if child is Dictionary:
275
+ var below: Dictionary = child
276
+ _instances_of(below, changed, old_path, new_path, moves)
277
+
278
+
279
+ ## [param text] with every path [param moves] names, and every path below one, moved.
280
+ ##
281
+ ## Beside a path the saver writes the unique_ids of the nodes along it, which the loader follows when
282
+ ## the path has vanished. A rename leaves those ids as they were; with [param reparented] they run
283
+ ## through the old parent, which still exists, so a recovery would find the wrong node. They are left
284
+ ## out of each path moved, as a hand-written scene leaves them, and the next save writes them anew.
285
+ static func _rewritten(text: String, moves: Dictionary, reparented: bool) -> String:
286
+ if moves.is_empty():
287
+ return text
288
+ var lines: PackedStringArray = text.split("\n")
289
+ for at: int in lines.size():
290
+ var line: String = lines[at]
291
+ if line.begins_with("[editable "):
292
+ line = _attribute_moved(line, "path", "", moves, reparented)
293
+ elif line.begins_with("[connection "):
294
+ line = _attribute_moved(line, "from", "from_uid_path", moves, reparented)
295
+ line = _attribute_moved(line, "to", "to_uid_path", moves, reparented)
296
+ elif line.begins_with("[node ") and line.contains(' parent="'):
297
+ var parent: String = _attribute(line, "parent")
298
+ var whole: String = _under(parent, _attribute(line, "name"))
299
+ if moves.has(whole):
300
+ var moved: String = moves[whole]
301
+ var moved_parent: String = moved.get_base_dir()
302
+ line = _attribute_set(line, "name", moved.get_file())
303
+ var placed: String = _attribute_set(
304
+ line, "parent", "." if moved_parent.is_empty() else moved_parent
305
+ )
306
+ line = _without_ids(placed, "parent_id_path") if placed != line and reparented else placed
307
+ else:
308
+ line = _attribute_moved(line, "parent", "parent_id_path", moves, reparented)
309
+ lines[at] = line
310
+ return "\n".join(lines)
311
+
312
+
313
+ ## [param path] below [param at], a path in the text's own spelling, "." for the root.
314
+ static func _under(at: String, path: String) -> String:
315
+ return path if at == "." else at + "/" + path
316
+
317
+
318
+ static func _attribute(line: String, attribute: String) -> String:
319
+ var found: RegExMatch = RegEx.create_from_string(' %s="([^"]*)"' % attribute).search(line)
320
+ return found.get_string(1) if found != null else ""
321
+
322
+
323
+ static func _attribute_set(line: String, attribute: String, value: String) -> String:
324
+ var found: RegExMatch = RegEx.create_from_string(' %s="([^"]*)"' % attribute).search(line)
325
+ if found == null:
326
+ return line
327
+ return line.substr(0, found.get_start(1)) + value + line.substr(found.get_end(1))
328
+
329
+
330
+ ## [param line] with the path under [param attribute] moved by [param moves], and, when that moved it
331
+ ## and the move was a reparent, without the ids under [param ids] that went along the old path.
332
+ static func _attribute_moved(
333
+ line: String, attribute: String, ids: String, moves: Dictionary, reparented: bool
334
+ ) -> String:
335
+ var found: RegExMatch = RegEx.create_from_string(' %s="([^"]*)"' % attribute).search(line)
336
+ if found == null:
337
+ return line
338
+ var moved: String = Paths.moved(found.get_string(1), moves)
339
+ if moved == found.get_string(1):
340
+ return line
341
+ var placed: String = _attribute_set(line, attribute, moved)
342
+ return _without_ids(placed, ids) if reparented and not ids.is_empty() else placed
343
+
344
+
345
+ static func _without_ids(line: String, ids: String) -> String:
346
+ return RegEx.create_from_string(" %s=PackedInt32Array\\([^)]*\\)" % ids).sub(line, "")
@@ -0,0 +1 @@
1
+ uid://ccgs03cix8rdw