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.
- package/build/cli.js +84 -26
- package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -4
- package/build/godot/addons/gdharness_editor/file_reads.gd +134 -10
- package/build/godot/addons/gdharness_editor/headless_check.gd +34 -4
- package/build/godot/addons/gdharness_editor/reading.gd +25 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +209 -15
- package/build/godot/addons/gdharness_editor/serialisation.gd +5 -1
- 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 +90 -48
- package/build/godot/addons/gdharness_runtime/reading.gd +25 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +5 -1
- package/build/godot/operations/animation_reads.gd +7 -4
- package/build/godot/operations/editor_tools.gd +19 -1
- package/build/godot/operations/file_reads.gd +134 -10
- package/build/godot/operations/reading.gd +25 -0
- package/build/godot/operations/serialisation.gd +5 -1
- package/build/index.js +84 -26
- package/package.json +1 -1
|
@@ -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
|