gdharness 1.2.29 → 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.
@@ -5,6 +5,7 @@ extends Node
5
5
 
6
6
  const Read = preload("../reading.gd")
7
7
  const PropertyValues = preload("../property_values.gd")
8
+ const ResourceTools = preload("resource_tools.gd")
8
9
  const SceneFile = preload("../scene_file.gd")
9
10
  const Serialisation = preload("../serialisation.gd")
10
11
 
@@ -101,8 +102,8 @@ func _kept(scene: SceneFile, saved: Node, node: Node, names: Array) -> String:
101
102
  return "%s is not in the scene as it would be saved." % at
102
103
  for key: Variant in names:
103
104
  var property: String = str(key)
104
- var loaded: Variant = copy.get(property)
105
- if not scene.kept(saved, loaded, node.get(property)):
105
+ var loaded: Variant = _properties.read_at(copy, property)
106
+ if not scene.kept(saved, loaded, _properties.read_at(node, property)):
106
107
  return (
107
108
  (
108
109
  "%s.%s would load from the file as %s: the scene does not keep it, because the property "
@@ -601,7 +602,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
601
602
 
602
603
  var held: Dictionary = {}
603
604
  for key: Variant in properties:
604
- held[str(key)] = _readable(node, node.get(str(key)))
605
+ held[str(key)] = _readable(node, _properties.read_at(node, str(key)))
605
606
  var new_path: String = scene.path_of(node)
606
607
  var written: Dictionary = scene.write(
607
608
  func(saved: Node) -> String: return _kept(scene, saved, node, properties.keys())
@@ -633,6 +634,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
633
634
  # variable read as changed; and a value this scene sets back to the default was hidden, so the
634
635
  # names this scene's file sets are always answered.
635
636
  var props: Dictionary = _properties_of(node, include_defaults, scene.set_here(node))
637
+ var cells: Dictionary = ResourceTools.cells_of(node)
636
638
  scene.close()
637
639
  var answer: Dictionary = {
638
640
  "ok": true,
@@ -640,6 +642,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
640
642
  "properties": props,
641
643
  "unsavedInEditor": SceneFile.unsaved(scene_path)
642
644
  }
645
+ answer.merge(cells)
643
646
  # An empty answer read as a node with nothing on it, rather than one with nothing changed.
644
647
  if props.is_empty() and not include_defaults:
645
648
  answer["note"] = (
@@ -757,6 +760,10 @@ func connect_signal(args: Dictionary) -> Dictionary:
757
760
  # asked for. Left alone, the answer gave the flags asked for and the file kept the old ones.
758
761
  var callable: Callable = Callable(target, method_name)
759
762
  var method_found: bool = target.has_method(method_name)
763
+ var flags_before: int = -1
764
+ for connection: Dictionary in source.get_signal_connection_list(signal_name):
765
+ if connection.get("callable") == callable:
766
+ flags_before = Read.as_int(connection.get("flags", 0))
760
767
  if source.is_connected(signal_name, callable):
761
768
  source.disconnect(signal_name, callable)
762
769
  var connect_result: Error = source.connect(signal_name, callable, flags)
@@ -776,7 +783,7 @@ func connect_signal(args: Dictionary) -> Dictionary:
776
783
  if not written.is_empty():
777
784
  return written
778
785
 
779
- return {
786
+ var answer: Dictionary = {
780
787
  "ok": true,
781
788
  "sourceNodePath": from,
782
789
  "signalName": signal_name,
@@ -788,6 +795,14 @@ func connect_signal(args: Dictionary) -> Dictionary:
788
795
  # next; a caller told nothing learnt of a misspelt one only when the signal fired.
789
796
  "methodFound": method_found
790
797
  }
798
+ # A second connect of the same pair read exactly like the first, so a caller could not tell a
799
+ # connection made from one that was already there, and a list showing one where two were made
800
+ # looked like a lost write.
801
+ if flags_before >= 0:
802
+ answer["alreadyConnected"] = true
803
+ if flags_before != Read.as_int(saved_flags[0]):
804
+ answer["flagsBefore"] = _flag_names(flags_before)
805
+ return answer
791
806
 
792
807
 
793
808
  ## The two ends of a connection in [param scene], as [source, target], or [null, why not].
@@ -61,65 +61,13 @@ func set_property(params: Dictionary) -> Dictionary:
61
61
  if not Paths.can_read(holder, named):
62
62
  return {"type": "error", "message": Paths.nothing_there(holder, named, str(reached["called"]))}
63
63
  var old_value: Variant = Paths.read_under(holder, named)
64
- # The same rule the call path holds: a value that cannot become what the property holds is
65
- # refused rather than written. Writing it means the engine picks something, and what it picks
66
- # for a word where a number goes is zero, which the answer then reports as the new value.
67
- #
68
- # A slot declaring nothing, an untyped or Variant property or an element of an untyped list, takes
69
- # anything, and the engine stores what it is given there. What it holds now still says how a value
70
- # of the same kind is meant, 5 into an int staying an int, and whether a path names an object, but
71
- # a value of another kind is stored as sent: a word into one holding a number is the word, and a
72
- # number into one holding a word is the number, not its text. The text null empties it when it
73
- # holds an object or nothing, as a wait reads it on the same slot.
74
64
  var slot: Dictionary = Values.slot_declared(holder, named)
75
65
  var declared: int = slot["type"]
76
66
  var declared_script: Script = slot["script"]
77
- var wanted: int = declared if declared != TYPE_NIL else typeof(old_value)
78
- var holds_objects: bool = (
79
- declared == TYPE_OBJECT or (declared == TYPE_NIL and (old_value == null or old_value is Object))
80
- )
81
- var given: Variant
82
- if holds_objects and names_no_object(value):
83
- given = null
84
- elif wanted == TYPE_OBJECT and (value is String or value is Dictionary):
85
- var named_object: Dictionary = _object_named(
86
- node, node_path, value, str(slot["class"]), declared_script
87
- )
88
- if named_object.has("message"):
89
- return {
90
- "type": "error",
91
- "message": "%s.%s holds an object: %s" % [reached["called"], named, named_object["message"]]
92
- }
93
- given = named_object["object"]
94
- else:
95
- var as_sent: Variant = _values.fitted(value, TYPE_NIL)
96
- if declared == TYPE_NIL and not Values.acceptable(as_sent, wanted):
97
- given = as_sent
98
- else:
99
- given = _values.fitted(value, wanted)
100
- if not Values.acceptable(given, wanted):
101
- return {
102
- "type": "error",
103
- "message":
104
- (
105
- "%s.%s holds %s and the value given is %s, which cannot become one.%s"
106
- % [
107
- reached["called"],
108
- named,
109
- type_string(wanted),
110
- type_string(typeof(given)),
111
- _written_as(wanted)
112
- ]
113
- )
114
- }
115
- var typed: Dictionary = _values.typed_like(given, old_value, _element_object.bind(node, node_path))
116
- if typed.has("message"):
117
- return {
118
- "type": "error",
119
- "message":
120
- "%s.%s is a typed %s: %s." % [reached["called"], named, _sort_of(old_value), typed["message"]]
121
- }
122
- given = typed["value"]
67
+ var fitting: Dictionary = _value_for(value, slot, old_value, reached, node, node_path)
68
+ if fitting.has("message"):
69
+ return fitting
70
+ var given: Variant = fitting["value"]
123
71
  # A component of a vector or a transform is written into a copy, which is put back where it was
124
72
  # read from, setter and all; the read back is then taken off the game again, since the copy here
125
73
  # says only what was written into it.
@@ -201,6 +149,131 @@ func set_property(params: Dictionary) -> Dictionary:
201
149
  return answer
202
150
 
203
151
 
152
+ ## What [param sent] becomes as argument [param index] of the method [param named] on the holder
153
+ ## [param reached] ends at, as [method Paths.walk_to] answered it, as `{"value": ...}`, or the refusal
154
+ ## saying why it cannot become one: a number the parameter would have to cut, a value that cannot be
155
+ ## converted, or a list or map that cannot be built as the typed one the parameter takes.
156
+ func _argument_for(
157
+ sent: Variant, index: int, reached: Dictionary, named: String, node: Node, node_path: String
158
+ ) -> Dictionary:
159
+ var holder: Object = reached["holder"]
160
+ var called: String = reached["called"]
161
+ var wants: int = _values.parameter_type(holder, named, index)
162
+ if Values.cuts(sent, wants):
163
+ return {
164
+ "type": "error",
165
+ "message":
166
+ (
167
+ "%s.%s takes %s as argument %d and was given %s, which it would have to be cut to fit"
168
+ % [called, named, type_string(wants), index + 1, JSON.stringify(sent)]
169
+ )
170
+ }
171
+ var given: Variant = _values.fitted(sent, wants)
172
+ if not Values.acceptable(given, wants):
173
+ return {
174
+ "type": "error",
175
+ "message":
176
+ (
177
+ "%s.%s takes %s as argument %d and was given %s, which cannot be converted.%s"
178
+ % [
179
+ called,
180
+ named,
181
+ type_string(wants),
182
+ index + 1,
183
+ type_string(typeof(given)),
184
+ _written_as(wants)
185
+ ]
186
+ )
187
+ }
188
+ # A typed list or map is built as one, since a plain one handed to such a parameter raises
189
+ # inside the game, which is what every check here exists to stop.
190
+ var container: Variant = _values.parameter_container(holder, named, index)
191
+ if container == null:
192
+ return {"value": given}
193
+ var typed: Dictionary = _values.typed_like(given, container, _element_object.bind(node, node_path))
194
+ if typed.has("message"):
195
+ return {
196
+ "type": "error",
197
+ "message":
198
+ (
199
+ "%s.%s takes a typed %s as argument %d: %s."
200
+ % [called, named, _sort_of(container), index + 1, typed["message"]]
201
+ )
202
+ }
203
+ return {"value": typed["value"]}
204
+
205
+
206
+ ## What [param value] becomes written into the slot [param reached] ends on, as
207
+ ## [method Paths.walk_to] answered it, as `{"value": ...}`, or the refusal saying why it cannot become
208
+ ## one.
209
+ ##
210
+ ## The same rule the call path holds: a value that cannot become what the property holds is
211
+ ## refused rather than written. Writing it means the engine picks something, and what it picks
212
+ ## for a word where a number goes is zero, which the answer then reports as the new value. So is a
213
+ ## number a whole number or a truth would have to cut, since converting one truncates.
214
+ ##
215
+ ## A slot declaring nothing, an untyped or Variant property or an element of an untyped list, takes
216
+ ## anything, and the engine stores what it is given there. What it holds now still says how a value
217
+ ## of the same kind is meant, 5 into an int staying an int, and whether a path names an object, but
218
+ ## a value of another kind is stored as sent: a word into one holding a number is the word, a
219
+ ## fraction into one holding a whole number is the fraction, and a number into one holding a word is
220
+ ## the number, not its text. The text null empties it when it holds an object or nothing, as a wait
221
+ ## reads it on the same slot.
222
+ func _value_for(
223
+ value: Variant, slot: Dictionary, old_value: Variant, reached: Dictionary, node: Node, node_path: String
224
+ ) -> Dictionary:
225
+ var named: String = reached["name"]
226
+ var called: String = reached["called"]
227
+ var declared: int = slot["type"]
228
+ var declared_script: Script = slot["script"]
229
+ var wanted: int = declared if declared != TYPE_NIL else typeof(old_value)
230
+ var holds_objects: bool = (
231
+ declared == TYPE_OBJECT or (declared == TYPE_NIL and (old_value == null or old_value is Object))
232
+ )
233
+ if holds_objects and names_no_object(value):
234
+ return {"value": null}
235
+ if wanted == TYPE_OBJECT and (value is String or value is Dictionary):
236
+ var named_object: Dictionary = _object_named(
237
+ node, node_path, value, str(slot["class"]), declared_script
238
+ )
239
+ if named_object.has("message"):
240
+ return {
241
+ "type": "error",
242
+ "message": "%s.%s holds an object: %s" % [called, named, named_object["message"]]
243
+ }
244
+ return {"value": named_object["object"]}
245
+ var as_sent: Variant = _values.fitted(value, TYPE_NIL)
246
+ var cut: bool = Values.cuts(value, wanted)
247
+ var given: Variant = as_sent
248
+ if declared != TYPE_NIL and cut:
249
+ return {
250
+ "type": "error",
251
+ "message":
252
+ (
253
+ "%s.%s holds %s and the value given is %s, which it would have to be cut to fit"
254
+ % [called, named, type_string(wanted), JSON.stringify(value)]
255
+ )
256
+ }
257
+ if declared != TYPE_NIL or (not cut and Values.acceptable(as_sent, wanted)):
258
+ given = _values.fitted(value, wanted)
259
+ if not Values.acceptable(given, wanted):
260
+ return {
261
+ "type": "error",
262
+ "message":
263
+ (
264
+ "%s.%s holds %s and the value given is %s, which cannot become one.%s"
265
+ % [called, named, type_string(wanted), type_string(typeof(given)), _written_as(wanted)]
266
+ )
267
+ }
268
+ var typed: Dictionary = _values.typed_like(given, old_value, _element_object.bind(node, node_path))
269
+ if typed.has("message"):
270
+ return {
271
+ "type": "error",
272
+ "message": "%s.%s is a typed %s: %s." % [called, named, _sort_of(old_value), typed["message"]]
273
+ }
274
+ return {"value": typed["value"]}
275
+
276
+
204
277
  ## Whether [param given] is the value [param held] already is, at the precision the property keeps.
205
278
  ##
206
279
  ## A float arrives as 64 bits and most engine properties keep 32: Camera3D.h_offset given -1.1
@@ -254,6 +327,12 @@ func call_method(params: Dictionary) -> Dictionary:
254
327
  var spelled: String = str(reached["name"])
255
328
  if Paths.method_of(spelled).is_empty():
256
329
  spelled += "()"
330
+ # A vector or a transform has no element to call a method on: it is read by its components.
331
+ if Paths.composite(reached["holder"]):
332
+ return {
333
+ "type": "error",
334
+ "message": Paths.nothing_there(reached["holder"], spelled, str(reached["called"]))
335
+ }
257
336
  if args.is_empty() and Paths.can_read(reached["holder"], spelled):
258
337
  var read_from: int = Time.get_ticks_usec()
259
338
  var answered: Variant = Paths.read_under(reached["holder"], spelled)
@@ -320,41 +399,10 @@ func call_method(params: Dictionary) -> Dictionary:
320
399
  by_engine_class[index + 1] = [declared, Values.class_of(resolved)]
321
400
  deserialized_args.append(named_object["object"])
322
401
  continue
323
- var given: Variant = _values.fitted(args[index], wants)
324
- if not Values.acceptable(given, wants):
325
- return {
326
- "type": "error",
327
- "message":
328
- (
329
- "%s.%s takes %s as argument %d and was given %s, which cannot be converted.%s"
330
- % [
331
- reached["called"],
332
- named,
333
- type_string(wants),
334
- index + 1,
335
- type_string(typeof(given)),
336
- _written_as(wants)
337
- ]
338
- )
339
- }
340
- # A typed list or map is built as one, since a plain one handed to such a parameter raises
341
- # inside the game, which is what every check here exists to stop.
342
- var container: Variant = _values.parameter_container(holder, named, index)
343
- if container != null:
344
- var typed: Dictionary = _values.typed_like(
345
- given, container, _element_object.bind(node, node_path)
346
- )
347
- if typed.has("message"):
348
- return {
349
- "type": "error",
350
- "message":
351
- (
352
- "%s.%s takes a typed %s as argument %d: %s."
353
- % [reached["called"], named, _sort_of(container), index + 1, typed["message"]]
354
- )
355
- }
356
- given = typed["value"]
357
- deserialized_args.append(given)
402
+ var argument: Dictionary = _argument_for(args[index], index, reached, named, node, node_path)
403
+ if argument.has("message"):
404
+ return argument
405
+ deserialized_args.append(argument["value"])
358
406
 
359
407
  # Timed around the call alone, since the bridge's round trip is a second or more and swamps what
360
408
  # one click's work costs; the frame metrics read whichever frame is last, which is not this one.
@@ -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"
@@ -134,6 +134,28 @@ static func acceptable(value: Variant, type: int) -> bool:
134
134
  return Serialisation.acceptable(value, type)
135
135
 
136
136
 
137
+ ## Whether [param given] would have to be cut to become a [param type]: a fraction made a whole
138
+ ## number, or a number other than 0 or 1 made a truth.
139
+ ##
140
+ ## A number arrives from JSON as a float, so one is converted to what the slot holds, and the
141
+ ## conversion truncates: 2.5 written into an int was stored as 2 and answered as written, and 0.2 on
142
+ ## a bool became true. A number written as text counts as the number.
143
+ static func cuts(given: Variant, type: int) -> bool:
144
+ if type != TYPE_INT and type != TYPE_BOOL:
145
+ return false
146
+ var number: Variant = given
147
+ if given is String:
148
+ var text: String = given
149
+ number = Read.json_or_null(text)
150
+ if typeof(number) != TYPE_FLOAT and typeof(number) != TYPE_INT:
151
+ return false
152
+ var value: float = Read.as_float(number)
153
+ var whole: bool = value == floorf(value)
154
+ if type == TYPE_INT:
155
+ return not whole
156
+ return not (whole and (value == 0.0 or value == 1.0))
157
+
158
+
137
159
  ## How a value of [param type] is written to be read as one, or empty when it is not built from fields.
138
160
  static func written_as(type: int) -> String:
139
161
  return Serialisation.written_as(type)
@@ -167,52 +189,6 @@ func parameter_class(object: Object, method: String, index: int) -> String:
167
189
  return ""
168
190
 
169
191
 
170
- ## The components a path reads off [param value], by the names GDScript gives them, or none for a
171
- ## value that has none: a vector's axes, a colour's channels, a rectangle's corners and size, a
172
- ## transform's basis and origin. A value of one of these is a copy wherever it is read, so a write
173
- ## into a component is a write into the copy until it is put back where it was read from.
174
- static func components_of(value: Variant) -> PackedStringArray:
175
- match typeof(value):
176
- TYPE_VECTOR2, TYPE_VECTOR2I:
177
- return PackedStringArray(["x", "y"])
178
- TYPE_VECTOR3, TYPE_VECTOR3I:
179
- return PackedStringArray(["x", "y", "z"])
180
- TYPE_VECTOR4, TYPE_VECTOR4I, TYPE_QUATERNION:
181
- return PackedStringArray(["x", "y", "z", "w"])
182
- TYPE_COLOR:
183
- return PackedStringArray(
184
- [
185
- "r",
186
- "g",
187
- "b",
188
- "a",
189
- "r8",
190
- "g8",
191
- "b8",
192
- "a8",
193
- "h",
194
- "s",
195
- "v",
196
- "ok_hsl_h",
197
- "ok_hsl_s",
198
- "ok_hsl_l"
199
- ]
200
- )
201
- TYPE_RECT2, TYPE_RECT2I, TYPE_AABB:
202
- return PackedStringArray(["position", "size", "end"])
203
- TYPE_PLANE:
204
- return PackedStringArray(["normal", "x", "y", "z", "d"])
205
- TYPE_BASIS:
206
- return PackedStringArray(["x", "y", "z"])
207
- TYPE_TRANSFORM2D:
208
- return PackedStringArray(["x", "y", "origin"])
209
- TYPE_TRANSFORM3D:
210
- return PackedStringArray(["basis", "origin"])
211
- TYPE_PROJECTION:
212
- return PackedStringArray(["x", "y", "z", "w"])
213
- return PackedStringArray()
214
-
215
-
216
192
  ## What the slot [param named] on [param holder] declares it holds, as `{"type": int, "class":
217
193
  ## String, "script": Script or null}`: a property's declaration, or the element type of a typed list
218
194
  ## or the value type of a typed map. TYPE_NIL when nothing is declared, which is also what a
@@ -228,7 +204,7 @@ static func components_of(value: Variant) -> PackedStringArray:
228
204
  static func slot_declared(holder: Variant, named: String) -> Dictionary:
229
205
  # A component holds one kind of value and no other: a vector's axis is a number, its integer
230
206
  # form's an integer, a rectangle's size a vector of the same form.
231
- if named in components_of(holder):
207
+ if named in Serialisation.components_of(holder):
232
208
  var component: Variant = holder[named]
233
209
  return {"type": typeof(component), "class": "", "script": null}
234
210
  if holder is Array:
@@ -312,19 +312,7 @@ static func _in_order(compare: String, limit: float, current: Variant) -> bool:
312
312
  ## conversion truncates: 3.5 waited on an int became 3 and 0.2 on a bool became true, and a property
313
313
  ## already holding that answered met at once, about a state nobody asked for.
314
314
  static func _cut_to_fit(given: Variant, type: int, node_path: String, property: String) -> String:
315
- if type != TYPE_INT and type != TYPE_BOOL:
316
- return ""
317
- var number: Variant = given
318
- if given is String:
319
- var text: String = given
320
- number = Read.json_or_null(text)
321
- if typeof(number) != TYPE_FLOAT and typeof(number) != TYPE_INT:
322
- return ""
323
- var value: float = Read.as_float(number)
324
- var whole: bool = value == floorf(value)
325
- if type == TYPE_INT and whole:
326
- return ""
327
- if type == TYPE_BOOL and whole and (value == 0.0 or value == 1.0):
315
+ if not Values.cuts(given, type):
328
316
  return ""
329
317
  return (
330
318
  (
@@ -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: