gdharness 1.2.30 → 1.2.31
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 +78 -22
- package/build/godot/addons/gdharness_editor/property_values.gd +80 -54
- package/build/godot/addons/gdharness_editor/serialisation.gd +46 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +78 -3
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +19 -4
- 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/project_config.gd +16 -6
- package/build/godot/operations/serialisation.gd +46 -0
- package/build/index.js +78 -22
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ extends RefCounted
|
|
|
4
4
|
## run off the game the node keeps. Every op that reads, writes, calls or waits on a game's state
|
|
5
5
|
## walks the same way, so the walk and what it says when a step is not there live in one place.
|
|
6
6
|
|
|
7
|
+
const Serialisation = preload("serialisation.gd")
|
|
7
8
|
const Values = preload("runtime_values.gd")
|
|
8
9
|
|
|
9
10
|
## The calls a path makes on a list or a map: those that take no arguments and change nothing.
|
|
@@ -95,7 +96,7 @@ const READS_ONLY: String = ", and a path, which a wait reads every frame, only c
|
|
|
95
96
|
##
|
|
96
97
|
## A step into a vector, a colour, a rectangle or a transform reads one of its components,
|
|
97
98
|
## "size:x" or "global_transform:origin:y", by the names GDScript gives them; see
|
|
98
|
-
## [method
|
|
99
|
+
## [method Serialisation.components_of].
|
|
99
100
|
##
|
|
100
101
|
## Answers with a `message` instead when a step along the way is not there or holds something that
|
|
101
102
|
## is not an object, naming the step rather than the whole path: "/root/Main:_game has no property
|
|
@@ -184,7 +185,7 @@ static func can_hold(value: Variant) -> bool:
|
|
|
184
185
|
## Whether [param value] is read by its components, a vector or a colour or a transform, and is a
|
|
185
186
|
## copy wherever it is read from.
|
|
186
187
|
static func composite(value: Variant) -> bool:
|
|
187
|
-
return not
|
|
188
|
+
return not Serialisation.components_of(value).is_empty()
|
|
188
189
|
|
|
189
190
|
|
|
190
191
|
## Whether [param value] is a packed list. Stepped into the way a list is, by index, and a write into
|
|
@@ -234,7 +235,7 @@ static func can_read(holder: Variant, named: String) -> bool:
|
|
|
234
235
|
return args.is_empty() and method in MAP_CALLS
|
|
235
236
|
return not _key_in(map, named).is_empty()
|
|
236
237
|
if composite(holder):
|
|
237
|
-
return method.is_empty() and named in
|
|
238
|
+
return method.is_empty() and named in Serialisation.components_of(holder)
|
|
238
239
|
if holder is Object:
|
|
239
240
|
var object: Object = holder
|
|
240
241
|
if not method.is_empty():
|
|
@@ -586,7 +587,7 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
586
587
|
if composite(holder):
|
|
587
588
|
var kind: String = type_string(typeof(holder))
|
|
588
589
|
var sort: String = ("an " if kind.left(1) in ["A", "E", "I", "O", "U"] else "a ") + kind
|
|
589
|
-
var has: String = ", ".join(
|
|
590
|
+
var has: String = ", ".join(Serialisation.components_of(holder))
|
|
590
591
|
if not container_call.is_empty():
|
|
591
592
|
return (
|
|
592
593
|
"%s is %s, which a path reads by its components, %s, and makes no call on"
|
|
@@ -189,52 +189,6 @@ func parameter_class(object: Object, method: String, index: int) -> String:
|
|
|
189
189
|
return ""
|
|
190
190
|
|
|
191
191
|
|
|
192
|
-
## The components a path reads off [param value], by the names GDScript gives them, or none for a
|
|
193
|
-
## value that has none: a vector's axes, a colour's channels, a rectangle's corners and size, a
|
|
194
|
-
## transform's basis and origin. A value of one of these is a copy wherever it is read, so a write
|
|
195
|
-
## into a component is a write into the copy until it is put back where it was read from.
|
|
196
|
-
static func components_of(value: Variant) -> PackedStringArray:
|
|
197
|
-
match typeof(value):
|
|
198
|
-
TYPE_VECTOR2, TYPE_VECTOR2I:
|
|
199
|
-
return PackedStringArray(["x", "y"])
|
|
200
|
-
TYPE_VECTOR3, TYPE_VECTOR3I:
|
|
201
|
-
return PackedStringArray(["x", "y", "z"])
|
|
202
|
-
TYPE_VECTOR4, TYPE_VECTOR4I, TYPE_QUATERNION:
|
|
203
|
-
return PackedStringArray(["x", "y", "z", "w"])
|
|
204
|
-
TYPE_COLOR:
|
|
205
|
-
return PackedStringArray(
|
|
206
|
-
[
|
|
207
|
-
"r",
|
|
208
|
-
"g",
|
|
209
|
-
"b",
|
|
210
|
-
"a",
|
|
211
|
-
"r8",
|
|
212
|
-
"g8",
|
|
213
|
-
"b8",
|
|
214
|
-
"a8",
|
|
215
|
-
"h",
|
|
216
|
-
"s",
|
|
217
|
-
"v",
|
|
218
|
-
"ok_hsl_h",
|
|
219
|
-
"ok_hsl_s",
|
|
220
|
-
"ok_hsl_l"
|
|
221
|
-
]
|
|
222
|
-
)
|
|
223
|
-
TYPE_RECT2, TYPE_RECT2I, TYPE_AABB:
|
|
224
|
-
return PackedStringArray(["position", "size", "end"])
|
|
225
|
-
TYPE_PLANE:
|
|
226
|
-
return PackedStringArray(["normal", "x", "y", "z", "d"])
|
|
227
|
-
TYPE_BASIS:
|
|
228
|
-
return PackedStringArray(["x", "y", "z"])
|
|
229
|
-
TYPE_TRANSFORM2D:
|
|
230
|
-
return PackedStringArray(["x", "y", "origin"])
|
|
231
|
-
TYPE_TRANSFORM3D:
|
|
232
|
-
return PackedStringArray(["basis", "origin"])
|
|
233
|
-
TYPE_PROJECTION:
|
|
234
|
-
return PackedStringArray(["x", "y", "z", "w"])
|
|
235
|
-
return PackedStringArray()
|
|
236
|
-
|
|
237
|
-
|
|
238
192
|
## What the slot [param named] on [param holder] declares it holds, as `{"type": int, "class":
|
|
239
193
|
## String, "script": Script or null}`: a property's declaration, or the element type of a typed list
|
|
240
194
|
## or the value type of a typed map. TYPE_NIL when nothing is declared, which is also what a
|
|
@@ -250,7 +204,7 @@ static func components_of(value: Variant) -> PackedStringArray:
|
|
|
250
204
|
static func slot_declared(holder: Variant, named: String) -> Dictionary:
|
|
251
205
|
# A component holds one kind of value and no other: a vector's axis is a number, its integer
|
|
252
206
|
# form's an integer, a rectangle's size a vector of the same form.
|
|
253
|
-
if named in components_of(holder):
|
|
207
|
+
if named in Serialisation.components_of(holder):
|
|
254
208
|
var component: Variant = holder[named]
|
|
255
209
|
return {"type": typeof(component), "class": "", "script": null}
|
|
256
210
|
if holder is Array:
|
|
@@ -246,6 +246,52 @@ func deserialize_value(value: Variant) -> Variant:
|
|
|
246
246
|
return call(builder, fields)
|
|
247
247
|
|
|
248
248
|
|
|
249
|
+
## The components a path reads off [param value], by the names GDScript gives them, or none for a
|
|
250
|
+
## value that has none: a vector's axes, a colour's channels, a rectangle's corners and size, a
|
|
251
|
+
## transform's basis and origin. A value of one of these is a copy wherever it is read, so a write
|
|
252
|
+
## into a component is a write into the copy until it is put back where it was read from.
|
|
253
|
+
static func components_of(value: Variant) -> PackedStringArray:
|
|
254
|
+
match typeof(value):
|
|
255
|
+
TYPE_VECTOR2, TYPE_VECTOR2I:
|
|
256
|
+
return PackedStringArray(["x", "y"])
|
|
257
|
+
TYPE_VECTOR3, TYPE_VECTOR3I:
|
|
258
|
+
return PackedStringArray(["x", "y", "z"])
|
|
259
|
+
TYPE_VECTOR4, TYPE_VECTOR4I, TYPE_QUATERNION:
|
|
260
|
+
return PackedStringArray(["x", "y", "z", "w"])
|
|
261
|
+
TYPE_COLOR:
|
|
262
|
+
return PackedStringArray(
|
|
263
|
+
[
|
|
264
|
+
"r",
|
|
265
|
+
"g",
|
|
266
|
+
"b",
|
|
267
|
+
"a",
|
|
268
|
+
"r8",
|
|
269
|
+
"g8",
|
|
270
|
+
"b8",
|
|
271
|
+
"a8",
|
|
272
|
+
"h",
|
|
273
|
+
"s",
|
|
274
|
+
"v",
|
|
275
|
+
"ok_hsl_h",
|
|
276
|
+
"ok_hsl_s",
|
|
277
|
+
"ok_hsl_l"
|
|
278
|
+
]
|
|
279
|
+
)
|
|
280
|
+
TYPE_RECT2, TYPE_RECT2I, TYPE_AABB:
|
|
281
|
+
return PackedStringArray(["position", "size", "end"])
|
|
282
|
+
TYPE_PLANE:
|
|
283
|
+
return PackedStringArray(["normal", "x", "y", "z", "d"])
|
|
284
|
+
TYPE_BASIS:
|
|
285
|
+
return PackedStringArray(["x", "y", "z"])
|
|
286
|
+
TYPE_TRANSFORM2D:
|
|
287
|
+
return PackedStringArray(["x", "y", "origin"])
|
|
288
|
+
TYPE_TRANSFORM3D:
|
|
289
|
+
return PackedStringArray(["basis", "origin"])
|
|
290
|
+
TYPE_PROJECTION:
|
|
291
|
+
return PackedStringArray(["x", "y", "z", "w"])
|
|
292
|
+
return PackedStringArray()
|
|
293
|
+
|
|
294
|
+
|
|
249
295
|
## Whether [param entries] is a list of [key, value] pairs, as a Dictionary tag carries them.
|
|
250
296
|
static func pairs(entries: Variant) -> bool:
|
|
251
297
|
if not entries is Array:
|
|
@@ -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
|
|
@@ -117,12 +117,7 @@ func set_project_setting(params: Dictionary) -> Dictionary:
|
|
|
117
117
|
var wanted: int = _wanted_type(setting_path, old_value if had_value else null)
|
|
118
118
|
if wanted > TYPE_NIL and final_value != null and typeof(final_value) != wanted:
|
|
119
119
|
if not _converts_faithfully(final_value, wanted):
|
|
120
|
-
return _log.failure(
|
|
121
|
-
(
|
|
122
|
-
"%s is declared as %s, and %s (%s) cannot be written as that without losing something."
|
|
123
|
-
% [setting_path, type_string(wanted), str(final_value), type_string(typeof(final_value))]
|
|
124
|
-
)
|
|
125
|
-
)
|
|
120
|
+
return _log.failure(_cannot_hold(setting_path, wanted, final_value))
|
|
126
121
|
final_value = type_convert(final_value, wanted)
|
|
127
122
|
|
|
128
123
|
var answer: Dictionary = {
|
|
@@ -174,6 +169,21 @@ func _converts_faithfully(value: Variant, to: int) -> bool:
|
|
|
174
169
|
return type_convert(type_convert(value, to), typeof(value)) == value
|
|
175
170
|
|
|
176
171
|
|
|
172
|
+
# Why [param value] cannot be written to a setting declared as [param wanted], in the words the
|
|
173
|
+
# editor's property writes use for the same two cases. One sentence for both read "without losing
|
|
174
|
+
# something" about a word given for a number, where nothing would be lost: it cannot become one.
|
|
175
|
+
static func _cannot_hold(setting_path: String, wanted: int, value: Variant) -> String:
|
|
176
|
+
var numbers: Array[int] = [TYPE_INT, TYPE_FLOAT]
|
|
177
|
+
var declared: String = "%s is declared as %s" % [setting_path, type_string(wanted)]
|
|
178
|
+
var cost: String = " Setting it would save a different value than the one asked for."
|
|
179
|
+
if wanted == TYPE_INT and typeof(value) == TYPE_FLOAT:
|
|
180
|
+
return "%s, which holds whole numbers, and %s would be cut to fit.%s" % [declared, str(value), cost]
|
|
181
|
+
if wanted in numbers and typeof(value) in numbers:
|
|
182
|
+
return "%s, and %s is not the same number as one.%s" % [declared, str(value), cost]
|
|
183
|
+
var given: String = type_string(typeof(value))
|
|
184
|
+
return "%s, and %s (%s) cannot become one." % [declared, JSON.stringify(value), given]
|
|
185
|
+
|
|
186
|
+
|
|
177
187
|
# Add an autoload singleton
|
|
178
188
|
func add_autoload(params: Dictionary) -> Dictionary:
|
|
179
189
|
var name: String = str(params.get("name", ""))
|
|
@@ -246,6 +246,52 @@ func deserialize_value(value: Variant) -> Variant:
|
|
|
246
246
|
return call(builder, fields)
|
|
247
247
|
|
|
248
248
|
|
|
249
|
+
## The components a path reads off [param value], by the names GDScript gives them, or none for a
|
|
250
|
+
## value that has none: a vector's axes, a colour's channels, a rectangle's corners and size, a
|
|
251
|
+
## transform's basis and origin. A value of one of these is a copy wherever it is read, so a write
|
|
252
|
+
## into a component is a write into the copy until it is put back where it was read from.
|
|
253
|
+
static func components_of(value: Variant) -> PackedStringArray:
|
|
254
|
+
match typeof(value):
|
|
255
|
+
TYPE_VECTOR2, TYPE_VECTOR2I:
|
|
256
|
+
return PackedStringArray(["x", "y"])
|
|
257
|
+
TYPE_VECTOR3, TYPE_VECTOR3I:
|
|
258
|
+
return PackedStringArray(["x", "y", "z"])
|
|
259
|
+
TYPE_VECTOR4, TYPE_VECTOR4I, TYPE_QUATERNION:
|
|
260
|
+
return PackedStringArray(["x", "y", "z", "w"])
|
|
261
|
+
TYPE_COLOR:
|
|
262
|
+
return PackedStringArray(
|
|
263
|
+
[
|
|
264
|
+
"r",
|
|
265
|
+
"g",
|
|
266
|
+
"b",
|
|
267
|
+
"a",
|
|
268
|
+
"r8",
|
|
269
|
+
"g8",
|
|
270
|
+
"b8",
|
|
271
|
+
"a8",
|
|
272
|
+
"h",
|
|
273
|
+
"s",
|
|
274
|
+
"v",
|
|
275
|
+
"ok_hsl_h",
|
|
276
|
+
"ok_hsl_s",
|
|
277
|
+
"ok_hsl_l"
|
|
278
|
+
]
|
|
279
|
+
)
|
|
280
|
+
TYPE_RECT2, TYPE_RECT2I, TYPE_AABB:
|
|
281
|
+
return PackedStringArray(["position", "size", "end"])
|
|
282
|
+
TYPE_PLANE:
|
|
283
|
+
return PackedStringArray(["normal", "x", "y", "z", "d"])
|
|
284
|
+
TYPE_BASIS:
|
|
285
|
+
return PackedStringArray(["x", "y", "z"])
|
|
286
|
+
TYPE_TRANSFORM2D:
|
|
287
|
+
return PackedStringArray(["x", "y", "origin"])
|
|
288
|
+
TYPE_TRANSFORM3D:
|
|
289
|
+
return PackedStringArray(["basis", "origin"])
|
|
290
|
+
TYPE_PROJECTION:
|
|
291
|
+
return PackedStringArray(["x", "y", "z", "w"])
|
|
292
|
+
return PackedStringArray()
|
|
293
|
+
|
|
294
|
+
|
|
249
295
|
## Whether [param entries] is a list of [key, value] pairs, as a Dictionary tag carries them.
|
|
250
296
|
static func pairs(entries: Variant) -> bool:
|
|
251
297
|
if not entries is Array:
|