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.
@@ -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 Values.components_of].
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 Values.components_of(value).is_empty()
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 Values.components_of(holder)
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(Values.components_of(holder))
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
- return {"bus_count": AudioServer.bus_count, "buses": buses, "layout": _layout_path()}
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: String = _base_of(path, header, skipped)
97
- if base.is_empty():
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: the nearest ancestor with a class_name, else the native
113
- # class at the top of the chain. A script that extends by path is followed to its own header.
114
- func _base_of(path: String, header: Dictionary, skipped: Array[Dictionary]) -> String:
115
- var visited: Array[String] = [path]
116
- var current: Dictionary = header
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
- var extends_what: String = str(current.get("extends", ""))
119
- if extends_what.is_empty():
130
+ if extends_what == null:
120
131
  return "RefCounted"
121
- if not (extends_what.begins_with('"') or extends_what.begins_with("'")):
122
- return extends_what
123
- var parent_path: String = extends_what.substr(1, extends_what.length() - 2)
124
- if not parent_path.begins_with("res://"):
125
- parent_path = visited[visited.size() - 1].get_base_dir().path_join(parent_path)
126
- if not FileAccess.file_exists(parent_path):
127
- skipped.append({"path": path, "reason": "extends a script that does not exist: " + parent_path})
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
- if parent_path in visited:
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
- visited.append(parent_path)
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 declarations at the top of a script: class_name, what it extends (a class name, or a
141
- # quoted path), and the @tool, @abstract and @icon annotations. Reading stops at the first
142
- # statement that is none of those, which is where the body begins.
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+(\\S+)")
150
- while not file.eof_reached():
151
- var rest: String = file.get_line().strip_edges()
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
- if not declared.get_string(2).is_empty():
171
- header["extends"] = declared.get_string(2)
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
- methods.append(
322
- {
323
- "name": m.get("name", ""),
324
- "args": args,
325
- "return":
323
+ (
324
+ methods
325
+ . append(
326
326
  {
327
- "type": return_info.get("type", 0),
328
- "type_name": _type_name(return_info, "void"),
329
- "class_name": return_info.get("class_name", "")
330
- },
331
- "flags": m.get("flags", 0),
332
- "default_args": m.get("default_args", [])
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: