gdharness 1.2.30 → 1.2.32
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 +7197 -6159
- package/build/godot/addons/gdharness_editor/bridge_client.gd +14 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +237 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/plugin.gd +27 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +80 -54
- package/build/godot/addons/gdharness_editor/scene_file.gd +38 -1
- package/build/godot/addons/gdharness_editor/serialisation.gd +46 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +22 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +64 -1
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +82 -44
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +178 -0
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +73 -124
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +39 -0
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd +285 -0
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +151 -106
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +112 -0
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +5 -4
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +1 -47
- package/build/godot/addons/gdharness_runtime/serialisation.gd +46 -0
- package/build/godot/operations/audio_buses.gd +14 -1
- package/build/godot/operations/class_cache.gd +146 -35
- package/build/godot/operations/classdb_queries.gd +19 -12
- package/build/godot/operations/file_reads.gd +237 -0
- package/build/godot/operations/gdscript_authoring.gd +136 -19
- package/build/godot/operations/godot_operations.gd +24 -0
- package/build/godot/operations/project_config.gd +16 -6
- package/build/godot/operations/property_values.gd +767 -0
- package/build/godot/operations/rename_checks.gd +5 -2
- package/build/godot/operations/serialisation.gd +46 -0
- package/build/index.js +4896 -3872
- package/build/keeper.js +184 -71
- package/package.json +1 -1
|
@@ -83,7 +83,20 @@ func get_audio_buses(_params: Dictionary) -> Dictionary:
|
|
|
83
83
|
var buses: Array[Dictionary] = []
|
|
84
84
|
for i: int in range(AudioServer.bus_count):
|
|
85
85
|
buses.append(_bus(i))
|
|
86
|
-
|
|
86
|
+
var layout: String = _layout_path()
|
|
87
|
+
var answer: Dictionary = {"bus_count": AudioServer.bus_count, "buses": buses, "layout": layout}
|
|
88
|
+
# The setting names a path whether or not anything is there, and a project that never changed a
|
|
89
|
+
# bus has no file: the path alone read as a layout the project keeps.
|
|
90
|
+
if not FileAccess.file_exists(layout):
|
|
91
|
+
answer["layout_on_disk"] = false
|
|
92
|
+
answer["note"] = (
|
|
93
|
+
(
|
|
94
|
+
"Nothing is at %s, so these are the engine's default buses; the first change to a bus "
|
|
95
|
+
+ "writes the layout there."
|
|
96
|
+
)
|
|
97
|
+
% layout
|
|
98
|
+
)
|
|
99
|
+
return answer
|
|
87
100
|
|
|
88
101
|
|
|
89
102
|
func set_audio_bus_effect(params: Dictionary) -> Dictionary:
|
|
@@ -93,13 +93,13 @@ func _entry_for(path: String, skipped: Array[Dictionary]) -> Dictionary:
|
|
|
93
93
|
if declared.is_empty():
|
|
94
94
|
return {}
|
|
95
95
|
|
|
96
|
-
var base:
|
|
97
|
-
if base
|
|
96
|
+
var base: Variant = _base_of(path, header, skipped)
|
|
97
|
+
if base == null:
|
|
98
98
|
return {}
|
|
99
99
|
|
|
100
100
|
# The field order is the editor's, so a rebuilt file reads as one the editor wrote.
|
|
101
101
|
return {
|
|
102
|
-
"base": StringName(base),
|
|
102
|
+
"base": StringName(str(base)),
|
|
103
103
|
"class": StringName(declared),
|
|
104
104
|
"icon": str(header.get("icon", "")),
|
|
105
105
|
"is_abstract": Read.as_bool(header.get("abstract", false)),
|
|
@@ -109,46 +109,157 @@ func _entry_for(path: String, skipped: Array[Dictionary]) -> Dictionary:
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
|
|
112
|
-
# What the editor records as the base
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
# What the editor records as the base, worked out as the engine's own class scan works it out,
|
|
113
|
+
# measured on 4.7.2 against `--import`, so a rebuilt list is the one the editor would write and a
|
|
114
|
+
# rebuild over a current list changes nothing:
|
|
115
|
+
#
|
|
116
|
+
# - nothing extended: RefCounted;
|
|
117
|
+
# - one name, a global class or a native one: that name, unresolved;
|
|
118
|
+
# - a dotted name, `Heard.Named`, an inner class of a global class: nothing, as the scan does not
|
|
119
|
+
# look names up;
|
|
120
|
+
# - a path: what that script's class records, after walking into the inner classes named after
|
|
121
|
+
# the path, `"res://heard.gd".Named`, so a parent with a class_name is passed over for its own
|
|
122
|
+
# base. A path the engine cannot open records nothing, and it opens a relative one from the
|
|
123
|
+
# project root rather than from the script's folder.
|
|
124
|
+
#
|
|
125
|
+
# Null for a script that extends itself, which is left out of the list and named in [param skipped].
|
|
126
|
+
func _base_of(path: String, header: Dictionary, skipped: Array[Dictionary]) -> Variant:
|
|
127
|
+
var visited: Array[String] = [path + ":"]
|
|
128
|
+
var extends_what: Variant = header.get("extends", null)
|
|
117
129
|
while true:
|
|
118
|
-
|
|
119
|
-
if extends_what.is_empty():
|
|
130
|
+
if extends_what == null:
|
|
120
131
|
return "RefCounted"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
var
|
|
124
|
-
if
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
var parts: Dictionary = _extends_parts(str(extends_what))
|
|
133
|
+
var parent_path: String = parts["path"]
|
|
134
|
+
var names: Array[String] = parts["names"]
|
|
135
|
+
if parent_path.is_empty():
|
|
136
|
+
return names[0] if names.size() == 1 else ""
|
|
137
|
+
var opened: String = _as_the_engine_opens(parent_path)
|
|
138
|
+
if not FileAccess.file_exists(opened):
|
|
128
139
|
return ""
|
|
129
|
-
|
|
140
|
+
var reached: String = opened + ":" + ".".join(PackedStringArray(names))
|
|
141
|
+
if reached in visited:
|
|
130
142
|
skipped.append({"path": path, "reason": "extends itself through " + parent_path})
|
|
143
|
+
return null
|
|
144
|
+
visited.append(reached)
|
|
145
|
+
var lines: PackedStringArray = FileAccess.get_file_as_string(opened).split("\n")
|
|
146
|
+
if names.is_empty():
|
|
147
|
+
extends_what = _header_in(lines).get("extends", null)
|
|
148
|
+
continue
|
|
149
|
+
var inner: Dictionary = _inner_class(lines, names)
|
|
150
|
+
if inner.is_empty():
|
|
131
151
|
return ""
|
|
132
|
-
|
|
133
|
-
current = _header_of(parent_path)
|
|
134
|
-
var parent_name: String = str(current.get("class_name", ""))
|
|
135
|
-
if not parent_name.is_empty():
|
|
136
|
-
return parent_name
|
|
152
|
+
extends_what = inner.get("extends", null)
|
|
137
153
|
return ""
|
|
138
154
|
|
|
139
155
|
|
|
140
|
-
# The
|
|
141
|
-
#
|
|
142
|
-
#
|
|
156
|
+
# The path and the class names an `extends` names, from the text after the keyword:
|
|
157
|
+
# `"res://heard.gd".Named` is the path and ["Named"], `Heard.Named` no path and two names. The
|
|
158
|
+
# reading stops at whatever follows, a colon or a comment.
|
|
159
|
+
static func _extends_parts(text: String) -> Dictionary:
|
|
160
|
+
var rest: String = text.strip_edges()
|
|
161
|
+
var path: String = ""
|
|
162
|
+
if rest.begins_with('"') or rest.begins_with("'"):
|
|
163
|
+
var closing: int = rest.find(rest[0], 1)
|
|
164
|
+
if closing == -1:
|
|
165
|
+
return {"path": "", "names": [] as Array[String]}
|
|
166
|
+
path = rest.substr(1, closing - 1)
|
|
167
|
+
rest = rest.substr(closing + 1)
|
|
168
|
+
var names: Array[String] = []
|
|
169
|
+
var name_at: RegEx = Patterns.compiled("^([A-Za-z_][A-Za-z0-9_]*)")
|
|
170
|
+
var dotted: bool = not path.is_empty()
|
|
171
|
+
while true:
|
|
172
|
+
if dotted:
|
|
173
|
+
if not rest.begins_with("."):
|
|
174
|
+
break
|
|
175
|
+
rest = rest.substr(1)
|
|
176
|
+
dotted = true
|
|
177
|
+
var found: RegExMatch = name_at.search(rest)
|
|
178
|
+
if found == null:
|
|
179
|
+
break
|
|
180
|
+
names.append(found.get_string(1))
|
|
181
|
+
rest = rest.substr(found.get_end())
|
|
182
|
+
return {"path": path, "names": names}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# The file the engine's class scan reads for an `extends` path. It opens the path as written, and
|
|
186
|
+
# the engine runs from the project root, so a relative path is read from there.
|
|
187
|
+
static func _as_the_engine_opens(path: String) -> String:
|
|
188
|
+
if path.is_absolute_path():
|
|
189
|
+
return path
|
|
190
|
+
var absolute: String = ProjectSettings.globalize_path("res://").path_join(path).simplify_path()
|
|
191
|
+
return ProjectSettings.localize_path(absolute)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# The inner class [param names] reaches in a script of [param lines], one class inside the
|
|
195
|
+
# previous, as {"found": true, "extends": the text after its keyword} with no "extends" for one that
|
|
196
|
+
# extends nothing, or {} when one of them is not there.
|
|
197
|
+
func _inner_class(lines: PackedStringArray, names: Array[String]) -> Dictionary:
|
|
198
|
+
var class_line: RegEx = Patterns.compiled("^class\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*(.*)$")
|
|
199
|
+
var inline_extends: RegEx = Patterns.compiled("^extends\\s+(.*)$")
|
|
200
|
+
var start: int = 0
|
|
201
|
+
var end: int = lines.size()
|
|
202
|
+
var depth: int = 0
|
|
203
|
+
var found: Dictionary = {}
|
|
204
|
+
for name: String in names:
|
|
205
|
+
var at: int = -1
|
|
206
|
+
found = {"found": true}
|
|
207
|
+
for index: int in range(start, end):
|
|
208
|
+
var text: String = lines[index].strip_edges()
|
|
209
|
+
if text.is_empty() or text.begins_with("#") or _indent(lines[index]) != depth:
|
|
210
|
+
continue
|
|
211
|
+
var declared: RegExMatch = class_line.search(text)
|
|
212
|
+
if declared == null or declared.get_string(1) != name:
|
|
213
|
+
continue
|
|
214
|
+
at = index
|
|
215
|
+
var inline: RegExMatch = inline_extends.search(declared.get_string(2).strip_edges())
|
|
216
|
+
if inline != null:
|
|
217
|
+
found["extends"] = inline.get_string(1)
|
|
218
|
+
break
|
|
219
|
+
if at == -1:
|
|
220
|
+
return {}
|
|
221
|
+
start = at + 1
|
|
222
|
+
var body_end: int = start
|
|
223
|
+
var body_depth: int = -1
|
|
224
|
+
for index: int in range(start, end):
|
|
225
|
+
var text: String = lines[index].strip_edges()
|
|
226
|
+
if not text.is_empty() and not text.begins_with("#"):
|
|
227
|
+
var width: int = _indent(lines[index])
|
|
228
|
+
if width <= depth:
|
|
229
|
+
break
|
|
230
|
+
if body_depth == -1:
|
|
231
|
+
body_depth = width
|
|
232
|
+
body_end = index + 1
|
|
233
|
+
end = body_end
|
|
234
|
+
depth = body_depth
|
|
235
|
+
if not found.has("extends"):
|
|
236
|
+
var body: Dictionary = _header_in(lines.slice(start, end))
|
|
237
|
+
if body.has("extends"):
|
|
238
|
+
found["extends"] = body["extends"]
|
|
239
|
+
return found
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
static func _indent(line: String) -> int:
|
|
243
|
+
return line.length() - line.lstrip(" \t").length()
|
|
244
|
+
|
|
245
|
+
|
|
143
246
|
func _header_of(path: String) -> Dictionary:
|
|
247
|
+
if not FileAccess.file_exists(path):
|
|
248
|
+
return {}
|
|
249
|
+
return _header_in(FileAccess.get_file_as_string(path).split("\n"))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# The declarations at the top of a script: class_name, what it extends (the text after the
|
|
253
|
+
# keyword, a class name or a quoted path, either followed by inner class names), and the @tool,
|
|
254
|
+
# @abstract and @icon annotations. Reading stops at the first statement that is none of those,
|
|
255
|
+
# which is where the body begins.
|
|
256
|
+
func _header_in(lines: PackedStringArray) -> Dictionary:
|
|
144
257
|
var header: Dictionary = {}
|
|
145
|
-
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
|
146
|
-
if not file:
|
|
147
|
-
return header
|
|
148
258
|
var annotation: RegEx = Patterns.compiled('^@([a-z_]+)(?:\\(\\s*(?:"([^"]*)")?[^)]*\\))?\\s*')
|
|
149
|
-
var extends_line: RegEx = Patterns.compiled("^extends\\s+(
|
|
150
|
-
|
|
151
|
-
|
|
259
|
+
var extends_line: RegEx = Patterns.compiled("^extends\\s+(.*)$")
|
|
260
|
+
var same_line: RegEx = Patterns.compiled("^\\s+extends\\s+(.*)$")
|
|
261
|
+
for line: String in lines:
|
|
262
|
+
var rest: String = line.strip_edges()
|
|
152
263
|
# Annotations may share a line with what they annotate, as in `@abstract class_name X`.
|
|
153
264
|
var annotated: RegExMatch = annotation.search(rest)
|
|
154
265
|
while annotated != null:
|
|
@@ -167,15 +278,15 @@ func _header_of(path: String) -> Dictionary:
|
|
|
167
278
|
var declared: RegExMatch = Patterns.declared_class(rest)
|
|
168
279
|
if declared != null:
|
|
169
280
|
header["class_name"] = declared.get_string(1)
|
|
170
|
-
|
|
171
|
-
|
|
281
|
+
var after: RegExMatch = same_line.search(rest.substr(declared.get_end(1)))
|
|
282
|
+
if after != null:
|
|
283
|
+
header["extends"] = after.get_string(1)
|
|
172
284
|
elif rest.begins_with("extends"):
|
|
173
285
|
var parent: RegExMatch = extends_line.search(rest)
|
|
174
286
|
if parent != null:
|
|
175
287
|
header["extends"] = parent.get_string(1)
|
|
176
288
|
else:
|
|
177
289
|
break
|
|
178
|
-
file.close()
|
|
179
290
|
return header
|
|
180
291
|
|
|
181
292
|
|
|
@@ -2,6 +2,7 @@ extends RefCounted
|
|
|
2
2
|
|
|
3
3
|
const Read = preload("reading.gd")
|
|
4
4
|
const Log = preload("logger.gd")
|
|
5
|
+
const Serialisation = preload("serialisation.gd")
|
|
5
6
|
|
|
6
7
|
# The base classes each category name stands for. Kept to the list the tool's schema offers, which
|
|
7
8
|
# a case holds: the schema offered physics3d and ui, which this refused, and physics answered the 3D
|
|
@@ -22,6 +23,7 @@ const CATEGORY_BASES: Dictionary = {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
var _log: Log
|
|
26
|
+
var _values: Serialisation = Serialisation.new()
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
func _init(p_log: Log) -> void:
|
|
@@ -318,19 +320,24 @@ func _methods_of(class_name_str: String, include_inherited: bool) -> Array[Dicti
|
|
|
318
320
|
}
|
|
319
321
|
)
|
|
320
322
|
var return_info: Dictionary = m.get("return", {})
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
"args": args,
|
|
325
|
-
"return":
|
|
323
|
+
(
|
|
324
|
+
methods
|
|
325
|
+
. append(
|
|
326
326
|
{
|
|
327
|
-
"
|
|
328
|
-
"
|
|
329
|
-
"
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
327
|
+
"name": m.get("name", ""),
|
|
328
|
+
"args": args,
|
|
329
|
+
"return":
|
|
330
|
+
{
|
|
331
|
+
"type": return_info.get("type", 0),
|
|
332
|
+
"type_name": _type_name(return_info, "void"),
|
|
333
|
+
"class_name": return_info.get("class_name", "")
|
|
334
|
+
},
|
|
335
|
+
"flags": m.get("flags", 0),
|
|
336
|
+
# As values, tagged the way every other answer tags one: JSON writes a Vector2i it is
|
|
337
|
+
# handed as its text, so TileMapLayer.set_cell's default atlas coords read "(-1, -1)".
|
|
338
|
+
"default_args": _values.serialize_value(m.get("default_args", []))
|
|
339
|
+
}
|
|
340
|
+
)
|
|
334
341
|
)
|
|
335
342
|
|
|
336
343
|
return methods
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## Scenes and resources read from their files, by the editor's tools and by a headless engine with
|
|
4
|
+
## none connected, so a read answers the same either way.
|
|
5
|
+
##
|
|
6
|
+
## A scene is read off its SceneState rather than instantiated. Instantiating outside the editor
|
|
7
|
+
## runs every script's member initialisers and _init (measured on 4.7.2, with or without -e), so a
|
|
8
|
+
## read with no editor would have run the game's code on every node; the editor gets placeholders
|
|
9
|
+
## and runs none. The state holds everything the file says, and the scenes it instances theirs,
|
|
10
|
+
## without making a node. Loading the file still does what loading does in the game: static
|
|
11
|
+
## initialisers run, and a resource with a script is made with it.
|
|
12
|
+
|
|
13
|
+
const PropertyValues = preload("property_values.gd")
|
|
14
|
+
const Read = preload("reading.gd")
|
|
15
|
+
const Serialisation = preload("serialisation.gd")
|
|
16
|
+
|
|
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.
|
|
21
|
+
static func scene_tree(scene_path: String, depth: int, include_properties: bool) -> Dictionary:
|
|
22
|
+
if not FileAccess.file_exists(scene_path):
|
|
23
|
+
return {"ok": false, "error": "Scene not found: " + scene_path}
|
|
24
|
+
# Loaded without naming the type, so a resource handed in by mistake loads as what it is and the
|
|
25
|
+
# refusal can say so, where asking for a PackedScene printed the loader's error and a backtrace.
|
|
26
|
+
var loaded: Resource = ResourceLoader.load(scene_path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
27
|
+
if loaded == null:
|
|
28
|
+
return {"ok": false, "error": "Failed to load %s as a scene" % scene_path}
|
|
29
|
+
if not loaded is PackedScene:
|
|
30
|
+
return {
|
|
31
|
+
"ok": false,
|
|
32
|
+
"error":
|
|
33
|
+
(
|
|
34
|
+
"%s is a %s, not a scene: resource_edit read reads its properties."
|
|
35
|
+
% [scene_path, loaded.get_class()]
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
var packed: PackedScene = loaded
|
|
39
|
+
var nodes: Dictionary = {}
|
|
40
|
+
var refused: String = _lay(packed.get_state(), ".", nodes, [scene_path])
|
|
41
|
+
if not refused.is_empty():
|
|
42
|
+
return {"ok": false, "error": refused}
|
|
43
|
+
if not nodes.has("."):
|
|
44
|
+
return {"ok": false, "error": "%s holds no nodes" % scene_path}
|
|
45
|
+
return {"ok": true, "tree": _entry(nodes, ".", include_properties, depth, 0)}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Lays the nodes of [param state] into [param nodes] under [param prefix], keyed by their path
|
|
49
|
+
## from the outermost root, each instanced scene laid where it is instanced and the instancing
|
|
50
|
+
## file's own entries for it applied over it. Answers a refusal, or "".
|
|
51
|
+
##
|
|
52
|
+
## [param within] is the files being laid, outermost first, so a scene that instances one of them
|
|
53
|
+
## is refused rather than laid forever; the engine refuses to load such a scene, so this is only
|
|
54
|
+
## reached through a state built some other way.
|
|
55
|
+
static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: Array) -> String:
|
|
56
|
+
var base: SceneState = state.get_base_scene_state()
|
|
57
|
+
if base != null and not nodes.has(prefix):
|
|
58
|
+
var refused: String = _lay(base, prefix, nodes, within)
|
|
59
|
+
if not refused.is_empty():
|
|
60
|
+
return refused
|
|
61
|
+
for index: int in state.get_node_count():
|
|
62
|
+
var path: String = _joined(prefix, str(state.get_node_path(index)))
|
|
63
|
+
var fresh: bool = not nodes.has(path)
|
|
64
|
+
if fresh:
|
|
65
|
+
var instance: PackedScene = state.get_node_instance(index)
|
|
66
|
+
if instance != null:
|
|
67
|
+
if within.has(instance.resource_path):
|
|
68
|
+
return "%s instances %s, which holds it" % [within.back(), instance.resource_path]
|
|
69
|
+
var refused: String = _lay(
|
|
70
|
+
instance.get_state(), path, nodes, within + [instance.resource_path]
|
|
71
|
+
)
|
|
72
|
+
if not refused.is_empty():
|
|
73
|
+
return refused
|
|
74
|
+
if not nodes.has(path):
|
|
75
|
+
return "%s holds no nodes" % instance.resource_path
|
|
76
|
+
nodes[path]["instanceOf"] = instance.resource_path
|
|
77
|
+
elif state.is_node_instance_placeholder(index):
|
|
78
|
+
nodes[path] = _made("InstancePlaceholder")
|
|
79
|
+
nodes[path]["instanceOf"] = state.get_node_instance_placeholder(index)
|
|
80
|
+
elif str(state.get_node_type(index)).is_empty():
|
|
81
|
+
# An entry for a node of an instance that no longer has it, which the engine skips too.
|
|
82
|
+
continue
|
|
83
|
+
else:
|
|
84
|
+
nodes[path] = _made(str(state.get_node_type(index)))
|
|
85
|
+
# The name is the state's even for an entry laid already, since an inherited scene names its
|
|
86
|
+
# root itself; a root instanced inside another scene is renamed by that scene once laid.
|
|
87
|
+
nodes[path]["name"] = str(state.get_node_name(index))
|
|
88
|
+
if fresh:
|
|
89
|
+
# The root of what is laid here is placed by whoever instanced it.
|
|
90
|
+
if path != prefix:
|
|
91
|
+
_place(
|
|
92
|
+
nodes,
|
|
93
|
+
_joined(prefix, str(state.get_node_path(index, true))),
|
|
94
|
+
path,
|
|
95
|
+
state.get_node_index(index)
|
|
96
|
+
)
|
|
97
|
+
var held: Dictionary = nodes[path]["properties"]
|
|
98
|
+
for property: int in state.get_node_property_count(index):
|
|
99
|
+
held[str(state.get_node_property_name(index, property))] = state.get_node_property_value(
|
|
100
|
+
index, property
|
|
101
|
+
)
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
static func _made(type: String) -> Dictionary:
|
|
106
|
+
var children: Array[String] = []
|
|
107
|
+
return {"name": "", "type": type, "properties": {}, "children": children}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## Puts [param path] among [param parent]'s children, at [param at] when the file says where (a node
|
|
111
|
+
## added inside an instance) and last when it does not.
|
|
112
|
+
static func _place(nodes: Dictionary, parent: String, path: String, at: int) -> void:
|
|
113
|
+
if not nodes.has(parent):
|
|
114
|
+
return
|
|
115
|
+
var children: Array[String] = nodes[parent]["children"]
|
|
116
|
+
if at >= 0 and at < children.size():
|
|
117
|
+
var _placed: int = children.insert(at, path)
|
|
118
|
+
else:
|
|
119
|
+
children.append(path)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## [param given], a path as a SceneState gives it ("." or "./Body/Tail"), under [param prefix].
|
|
123
|
+
static func _joined(prefix: String, given: String) -> String:
|
|
124
|
+
var relative: String = given.trim_prefix("./")
|
|
125
|
+
if relative == "." or relative.is_empty():
|
|
126
|
+
return prefix
|
|
127
|
+
if prefix == ".":
|
|
128
|
+
return relative
|
|
129
|
+
return prefix + "/" + relative
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
static func _entry(
|
|
133
|
+
nodes: Dictionary, path: String, include_properties: bool, depth: int, current_depth: int
|
|
134
|
+
) -> Dictionary:
|
|
135
|
+
var node: Dictionary = nodes[path]
|
|
136
|
+
var children: Array[Dictionary] = []
|
|
137
|
+
var data: Dictionary = {"name": node["name"], "type": node["type"], "path": path, "children": children}
|
|
138
|
+
# The root's file is the scene being read, so only another node's names a second scene.
|
|
139
|
+
if path != "." and node.has("instanceOf"):
|
|
140
|
+
data["instanceOf"] = node["instanceOf"]
|
|
141
|
+
if include_properties:
|
|
142
|
+
var given: Dictionary = node["properties"]
|
|
143
|
+
data["properties"] = _stored(str(node["type"]), given)
|
|
144
|
+
if depth >= 0 and current_depth >= depth:
|
|
145
|
+
return data
|
|
146
|
+
for child: String in node["children"]:
|
|
147
|
+
children.append(_entry(nodes, child, include_properties, depth, current_depth + 1))
|
|
148
|
+
return data
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Every property a node of [param type] stores, with the value [param given] sets or the default,
|
|
152
|
+
## read as a node holding those values would answer them.
|
|
153
|
+
##
|
|
154
|
+
## The engine's half is read off a bare node of the class with the file's values set on it, which
|
|
155
|
+
## runs no script: what a node lists depends on its values (a thread group's settings appear only
|
|
156
|
+
## once it has one) and some setters set others (a Control's anchors preset sets how it grows), so
|
|
157
|
+
## the class's defaults and the file's entries alone answered neither. The script's half is its
|
|
158
|
+
## declared defaults under the file's values.
|
|
159
|
+
static func _stored(type: String, given: Dictionary) -> Dictionary:
|
|
160
|
+
var attached: Variant = given.get("script")
|
|
161
|
+
var script: Script = attached if attached is Script else null
|
|
162
|
+
var scripted: Dictionary = {}
|
|
163
|
+
if script != null:
|
|
164
|
+
for declared: Dictionary in script.get_script_property_list():
|
|
165
|
+
var property: String = str(declared.get("name", ""))
|
|
166
|
+
if not property.is_empty() and Read.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
167
|
+
scripted[property] = script.get_property_default_value(property)
|
|
168
|
+
var values: Dictionary = {}
|
|
169
|
+
if ClassDB.class_exists(type) and ClassDB.can_instantiate(type):
|
|
170
|
+
var bare: Object = ClassDB.instantiate(type)
|
|
171
|
+
for property: Variant in given:
|
|
172
|
+
if property != "script" and not scripted.has(property):
|
|
173
|
+
bare.set(str(property), given[property])
|
|
174
|
+
for declared: Dictionary in bare.get_property_list():
|
|
175
|
+
var property: String = str(declared.get("name", ""))
|
|
176
|
+
if not property.is_empty() and Read.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
177
|
+
values[property] = bare.get(property)
|
|
178
|
+
if bare is Node:
|
|
179
|
+
bare.free()
|
|
180
|
+
for property: String in scripted:
|
|
181
|
+
values[property] = scripted[property]
|
|
182
|
+
# Given last, which keeps what a bare node cannot hold: the script itself, and a path to a node
|
|
183
|
+
# for a property that holds the node once it is in a tree.
|
|
184
|
+
values.merge(given, true)
|
|
185
|
+
var serialiser: Serialisation = Serialisation.new()
|
|
186
|
+
var answered: Dictionary = {}
|
|
187
|
+
for property: Variant in values:
|
|
188
|
+
answered[property] = serialiser.serialize_value(values[property])
|
|
189
|
+
return answered
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
## What a resource file holds, in the form modify takes, so a value read here can be written back.
|
|
193
|
+
## Read from the file rather than a cached copy, since the file is what the writes change and what
|
|
194
|
+
## the project loads.
|
|
195
|
+
static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
196
|
+
if not FileAccess.file_exists(res_path):
|
|
197
|
+
return {"ok": false, "error": "No resource at " + res_path}
|
|
198
|
+
var loaded: Resource = ResourceLoader.load(res_path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
199
|
+
if loaded == null:
|
|
200
|
+
return {"ok": false, "error": "%s did not load as a resource." % res_path}
|
|
201
|
+
# A scene's own properties are its packed bundle, which says nothing a caller can use.
|
|
202
|
+
if loaded is PackedScene:
|
|
203
|
+
return {
|
|
204
|
+
"ok": false,
|
|
205
|
+
"error":
|
|
206
|
+
"%s is a scene: scene_tree reads its nodes, and scene_node get one node's properties." % res_path
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
var script: Script = loaded.get_script()
|
|
210
|
+
var scripted: Dictionary = {}
|
|
211
|
+
if script != null:
|
|
212
|
+
for declared: Dictionary in script.get_script_property_list():
|
|
213
|
+
scripted[str(declared.get("name", ""))] = true
|
|
214
|
+
var serialiser: Serialisation = Serialisation.new()
|
|
215
|
+
var properties: Dictionary = {}
|
|
216
|
+
for p: Dictionary in loaded.get_property_list():
|
|
217
|
+
var property: String = str(p.get("name", ""))
|
|
218
|
+
if property.is_empty() or property == "script":
|
|
219
|
+
continue
|
|
220
|
+
if not (Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
221
|
+
continue
|
|
222
|
+
var value: Variant = loaded.get(property)
|
|
223
|
+
if not include_defaults:
|
|
224
|
+
var default: Variant = (
|
|
225
|
+
script.get_property_default_value(property)
|
|
226
|
+
if scripted.has(property)
|
|
227
|
+
else ClassDB.class_get_property_default_value(loaded.get_class(), property)
|
|
228
|
+
)
|
|
229
|
+
if PropertyValues.same(value, default):
|
|
230
|
+
continue
|
|
231
|
+
properties[property] = serialiser.serialize_value(value)
|
|
232
|
+
var answer: Dictionary = {
|
|
233
|
+
"ok": true, "resourcePath": res_path, "resourceType": loaded.get_class(), "properties": properties
|
|
234
|
+
}
|
|
235
|
+
if script != null:
|
|
236
|
+
answer["script"] = script.resource_path
|
|
237
|
+
return answer
|