gdharness 0.13.2 → 0.13.4

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.
@@ -0,0 +1,560 @@
1
+ extends RefCounted
2
+
3
+ ## Godot values as JSON can carry them, and JSON back into the values the engine wants.
4
+ ##
5
+ ## This file is the one beside the operations, and `bun run sync:gd` copies it into each addon,
6
+ ## because an addon is installed as a directory and cannot preload out of one. The copies are
7
+ ## byte for byte this file and a fixture says so. Three spellings of one value is what there was
8
+ ## before: the same Vector2 came back three ways depending on which tool was asked, and a caller
9
+ ## reading a property off the editor could not feed it back to the game.
10
+
11
+ const Read = preload("reading.gd")
12
+
13
+ # What each Godot type becomes on the wire, as one table you can read rather than an order you
14
+ # have to trust. Keyed on typeof() rather than written as a chain of `is` tests, because the
15
+ # order mattered: Resource had to be tested before Object or every resource came back as a bare
16
+ # class name with its path dropped, and nothing but a comment said so.
17
+ #
18
+ # Method names rather than Callables: a table of Callables bound to this object is a reference
19
+ # cycle that nothing breaks, so the object outlives the run and the engine reports its script as
20
+ # a resource still in use at exit, on the same stderr the server reads failures off.
21
+ #
22
+ # Every type the engine has that JSON cannot carry is in here, and a fixture walks the engine's own
23
+ # type list to say so, because what this table used to be was the types somebody had thought of.
24
+ # JSON does not refuse what it cannot carry: it writes the value's own text and moves on, so a
25
+ # Polygon2D's points came back as the string "[(1.0, 2.0), (3.0, 4.0)]" and a Quaternion as
26
+ # "(0, 0, 0, 1)". Both read as an answer, neither can be read back, and nothing anywhere said the
27
+ # value had been flattened on the way out.
28
+ #
29
+ # The types JSON does carry exactly are left alone, PackedStringArray and the numeric packed arrays
30
+ # among them, because a plugin list reads better as ["res://addons/x/plugin.cfg"] than as a tagged
31
+ # wrapper around one, and type_convert restores the exact type wherever the receiver knows which
32
+ # type it wanted.
33
+ const SERIALISERS: Dictionary = {
34
+ TYPE_NIL: "_serialize_nil",
35
+ TYPE_VECTOR2: "_serialize_vector2",
36
+ TYPE_VECTOR2I: "_serialize_vector2i",
37
+ TYPE_VECTOR3: "_serialize_vector3",
38
+ TYPE_VECTOR3I: "_serialize_vector3i",
39
+ TYPE_VECTOR4: "_serialize_vector4",
40
+ TYPE_VECTOR4I: "_serialize_vector4i",
41
+ TYPE_RECT2: "_serialize_rect2",
42
+ TYPE_RECT2I: "_serialize_rect2i",
43
+ TYPE_PLANE: "_serialize_plane",
44
+ TYPE_QUATERNION: "_serialize_quaternion",
45
+ TYPE_AABB: "_serialize_aabb",
46
+ TYPE_BASIS: "_serialize_basis",
47
+ TYPE_TRANSFORM2D: "_serialize_transform2d",
48
+ TYPE_TRANSFORM3D: "_serialize_transform3d",
49
+ TYPE_PROJECTION: "_serialize_projection",
50
+ TYPE_COLOR: "_serialize_color",
51
+ TYPE_NODE_PATH: "_serialize_node_path",
52
+ TYPE_RID: "_serialize_rid",
53
+ TYPE_OBJECT: "_serialize_object",
54
+ TYPE_CALLABLE: "_serialize_callable",
55
+ TYPE_SIGNAL: "_serialize_signal",
56
+ TYPE_DICTIONARY: "_serialize_dictionary",
57
+ TYPE_ARRAY: "_serialize_array",
58
+ TYPE_PACKED_BYTE_ARRAY: "_serialize_bytes",
59
+ TYPE_PACKED_VECTOR2_ARRAY: "_serialize_vector2s",
60
+ TYPE_PACKED_VECTOR3_ARRAY: "_serialize_vector3s",
61
+ TYPE_PACKED_VECTOR4_ARRAY: "_serialize_vector4s",
62
+ TYPE_PACKED_COLOR_ARRAY: "_serialize_colors",
63
+ }
64
+
65
+ # What each tag builds back into. The other half of the table above, and separate from it because
66
+ # they are keyed differently: one by the type of a value in hand, the other by the name in a
67
+ # dictionary that arrived from somewhere else.
68
+ const BUILDERS: Dictionary = {
69
+ "Vector2": "_build_vector2",
70
+ "Vector2i": "_build_vector2i",
71
+ "Vector3": "_build_vector3",
72
+ "Vector3i": "_build_vector3i",
73
+ "Vector4": "_build_vector4",
74
+ "Vector4i": "_build_vector4i",
75
+ "Rect2": "_build_rect2",
76
+ "Rect2i": "_build_rect2i",
77
+ "Plane": "_build_plane",
78
+ "Quaternion": "_build_quaternion",
79
+ "AABB": "_build_aabb",
80
+ "Basis": "_build_basis",
81
+ "Transform2D": "_build_transform2d",
82
+ "Transform3D": "_build_transform3d",
83
+ "Projection": "_build_projection",
84
+ "Color": "_build_color",
85
+ "NodePath": "_build_node_path",
86
+ "PackedByteArray": "_build_bytes",
87
+ }
88
+
89
+ # What each tag has to carry before it is built. A tag naming a type whose keys are not there is a
90
+ # tag somebody wrote by mistake or wrote for a different shape, and building one out of defaults
91
+ # answers with a Transform2D of zeros where the caller meant something else entirely. Unbuilt, it
92
+ # comes back as it arrived, and whoever asked can read it against the type they were expecting.
93
+ const REQUIRED: Dictionary = {
94
+ "Vector2": [["x", "y"]],
95
+ "Vector2i": [["x", "y"]],
96
+ "Vector3": [["x", "y", "z"]],
97
+ "Vector3i": [["x", "y", "z"]],
98
+ "Vector4": [["x", "y", "z", "w"]],
99
+ "Vector4i": [["x", "y", "z", "w"]],
100
+ "Rect2": [["position", "size"], ["x", "y", "width", "height"]],
101
+ "Rect2i": [["position", "size"], ["x", "y", "width", "height"]],
102
+ "Plane": [["normal", "d"]],
103
+ "Quaternion": [["x", "y", "z", "w"]],
104
+ "AABB": [["position", "size"]],
105
+ "Basis": [["x", "y", "z"]],
106
+ "Transform2D": [["x", "y", "origin"]],
107
+ "Transform3D": [["basis", "origin"]],
108
+ "Projection": [["x", "y", "z", "w"]],
109
+ "Color": [["r", "g", "b"]],
110
+ "NodePath": [["path"]],
111
+ "PackedByteArray": [["base64"]],
112
+ }
113
+
114
+
115
+ # Converts a Godot value into something JSON can carry. A type with no entry in the table
116
+ # passes through as itself, which is what the JSON-native ones want.
117
+ #
118
+ # A property that holds no object is Object-typed and null rather than nil, which is most of what
119
+ # a node's property list is, and an object that has been freed is the same shape again. Both are
120
+ # refused here rather than inside _serialize_object, because a freed one never reaches it: the
121
+ # call itself fails on the argument, with "previously freed is not a subclass of the expected
122
+ # argument class", and a guard behind that is a guard that never runs.
123
+ func serialize_value(value: Variant) -> Variant:
124
+ if typeof(value) == TYPE_OBJECT and not is_instance_valid(value):
125
+ return null
126
+ var serialiser: String = SERIALISERS.get(typeof(value), "")
127
+ return call(serialiser, value) if not serialiser.is_empty() else value
128
+
129
+
130
+ # Rebuilds a Godot value from the shape serialize_value gave it.
131
+ #
132
+ # A tag with no builder is left as the dictionary it arrived as. Some of them describe something
133
+ # that cannot be rebuilt at all, a Callable or a live object, and answering with a default-built
134
+ # one of those would be inventing a value rather than reporting one.
135
+ func deserialize_value(value: Variant) -> Variant:
136
+ if value == null:
137
+ return null
138
+ if value is Array:
139
+ var items: Array = value
140
+ var rebuilt: Array = []
141
+ for item: Variant in items:
142
+ rebuilt.append(deserialize_value(item))
143
+ return rebuilt
144
+ if not value is Dictionary:
145
+ return value
146
+
147
+ var fields: Dictionary = value
148
+ if not fields.has("_type"):
149
+ var mapped: Dictionary = {}
150
+ for key: Variant in fields:
151
+ mapped[key] = deserialize_value(fields[key])
152
+ return mapped
153
+
154
+ # Each component read as the type the constructor wants before it is handed over. These come
155
+ # out of JSON as Variant, and a project holding `unsafe_call_argument` at error level refuses
156
+ # to compile a script that gives a Variant to a typed parameter: the operations are compiled
157
+ # under the target project's warning levels, so one such project lost every headless call.
158
+ var tag: String = str(fields["_type"])
159
+ var builder: String = BUILDERS.get(tag, "")
160
+ var needed: Array = REQUIRED.get(tag, [])
161
+ if builder.is_empty() or not _carries(fields, needed):
162
+ return fields
163
+ return call(builder, fields)
164
+
165
+
166
+ ## Whether [param fields] carries every key of any one of [param alternatives].
167
+ ##
168
+ ## More than one spelling because a rectangle has two: the two corners it is answered with, and the
169
+ ## four numbers the editor's own tools answered with before the serialisers were made one, which a
170
+ ## caller may have kept.
171
+ func _carries(fields: Dictionary, alternatives: Array) -> bool:
172
+ for alternative: Variant in alternatives:
173
+ var keys: Array = alternative
174
+ if keys.all(func(key: Variant) -> bool: return fields.has(key)):
175
+ return true
176
+ return false
177
+
178
+
179
+ func _serialize_nil(_value: Variant) -> Variant:
180
+ return null
181
+
182
+
183
+ func _serialize_vector2(value: Vector2) -> Dictionary:
184
+ return {"x": value.x, "y": value.y, "_type": "Vector2"}
185
+
186
+
187
+ func _serialize_vector2i(value: Vector2i) -> Dictionary:
188
+ return {"x": value.x, "y": value.y, "_type": "Vector2i"}
189
+
190
+
191
+ func _serialize_vector3(value: Vector3) -> Dictionary:
192
+ return {"x": value.x, "y": value.y, "z": value.z, "_type": "Vector3"}
193
+
194
+
195
+ func _serialize_vector3i(value: Vector3i) -> Dictionary:
196
+ return {"x": value.x, "y": value.y, "z": value.z, "_type": "Vector3i"}
197
+
198
+
199
+ func _serialize_vector4(value: Vector4) -> Dictionary:
200
+ return {"x": value.x, "y": value.y, "z": value.z, "w": value.w, "_type": "Vector4"}
201
+
202
+
203
+ func _serialize_vector4i(value: Vector4i) -> Dictionary:
204
+ return {"x": value.x, "y": value.y, "z": value.z, "w": value.w, "_type": "Vector4i"}
205
+
206
+
207
+ func _serialize_rect2(value: Rect2) -> Dictionary:
208
+ return {
209
+ "position": serialize_value(value.position), "size": serialize_value(value.size), "_type": "Rect2"
210
+ }
211
+
212
+
213
+ func _serialize_rect2i(value: Rect2i) -> Dictionary:
214
+ return {
215
+ "position": serialize_value(value.position), "size": serialize_value(value.size), "_type": "Rect2i"
216
+ }
217
+
218
+
219
+ func _serialize_plane(value: Plane) -> Dictionary:
220
+ return {"normal": serialize_value(value.normal), "d": value.d, "_type": "Plane"}
221
+
222
+
223
+ func _serialize_quaternion(value: Quaternion) -> Dictionary:
224
+ return {"x": value.x, "y": value.y, "z": value.z, "w": value.w, "_type": "Quaternion"}
225
+
226
+
227
+ func _serialize_aabb(value: AABB) -> Dictionary:
228
+ return {"position": serialize_value(value.position), "size": serialize_value(value.size), "_type": "AABB"}
229
+
230
+
231
+ func _serialize_basis(value: Basis) -> Dictionary:
232
+ return {
233
+ "x": serialize_value(value.x),
234
+ "y": serialize_value(value.y),
235
+ "z": serialize_value(value.z),
236
+ "_type": "Basis"
237
+ }
238
+
239
+
240
+ func _serialize_transform2d(value: Transform2D) -> Dictionary:
241
+ return {
242
+ "origin": serialize_value(value.origin),
243
+ "x": serialize_value(value.x),
244
+ "y": serialize_value(value.y),
245
+ "_type": "Transform2D"
246
+ }
247
+
248
+
249
+ func _serialize_transform3d(value: Transform3D) -> Dictionary:
250
+ return {
251
+ "origin": serialize_value(value.origin), "basis": serialize_value(value.basis), "_type": "Transform3D"
252
+ }
253
+
254
+
255
+ func _serialize_projection(value: Projection) -> Dictionary:
256
+ return {
257
+ "x": serialize_value(value.x),
258
+ "y": serialize_value(value.y),
259
+ "z": serialize_value(value.z),
260
+ "w": serialize_value(value.w),
261
+ "_type": "Projection"
262
+ }
263
+
264
+
265
+ func _serialize_color(value: Color) -> Dictionary:
266
+ return {"r": value.r, "g": value.g, "b": value.b, "a": value.a, "_type": "Color"}
267
+
268
+
269
+ func _serialize_node_path(value: NodePath) -> Dictionary:
270
+ return {"path": str(value), "_type": "NodePath"}
271
+
272
+
273
+ # An id rather than nothing, because two properties holding the same RID are worth telling apart
274
+ # even though neither can be handed back to the engine.
275
+ func _serialize_rid(value: RID) -> Dictionary:
276
+ return {"id": value.get_id(), "_type": "RID"}
277
+
278
+
279
+ # A bound method, said in the terms a caller could act on: what it is on, and what it is called.
280
+ # It cannot be rebuilt from here, and saying so is the answer rather than a silent null.
281
+ func _serialize_callable(value: Callable) -> Dictionary:
282
+ var described: Dictionary = {"method": str(value.get_method()), "_type": "Callable"}
283
+ var holder: Object = value.get_object()
284
+ if holder != null:
285
+ described["object"] = _describe_object(holder)
286
+ return described
287
+
288
+
289
+ func _serialize_signal(value: Signal) -> Dictionary:
290
+ var described: Dictionary = {"name": str(value.get_name()), "_type": "Signal"}
291
+ var holder: Object = value.get_object()
292
+ if holder != null:
293
+ described["object"] = _describe_object(holder)
294
+ return described
295
+
296
+
297
+ func _serialize_dictionary(value: Dictionary) -> Dictionary:
298
+ var serialised: Dictionary = {}
299
+ for key: Variant in value:
300
+ serialised[str(key)] = serialize_value(value[key])
301
+ return serialised
302
+
303
+
304
+ func _serialize_array(value: Array) -> Array:
305
+ return value.map(serialize_value)
306
+
307
+
308
+ # Base64 rather than a list of numbers. A byte array is a texture, a sound or a save file often
309
+ # enough that the list would be the whole answer, megabytes of it, and unreadable either way.
310
+ func _serialize_bytes(value: PackedByteArray) -> Dictionary:
311
+ return {"base64": Marshalls.raw_to_base64(value), "size": value.size(), "_type": "PackedByteArray"}
312
+
313
+
314
+ # The packed vector and colour arrays, each element serialised the way one of them on its own is.
315
+ # A plain list rather than a tagged wrapper, so a polygon reads as the points it is, and because
316
+ # the receiver of a write knows the type it wanted and converts to it. One function each rather
317
+ # than one taking a Variant, because a Variant handed to the element serialiser is the unsafe
318
+ # argument a project with every warning at error level refuses to compile.
319
+ func _serialize_vector2s(value: PackedVector2Array) -> Array:
320
+ var serialised: Array = []
321
+ for element: Vector2 in value:
322
+ serialised.append(_serialize_vector2(element))
323
+ return serialised
324
+
325
+
326
+ func _serialize_vector3s(value: PackedVector3Array) -> Array:
327
+ var serialised: Array = []
328
+ for element: Vector3 in value:
329
+ serialised.append(_serialize_vector3(element))
330
+ return serialised
331
+
332
+
333
+ func _serialize_vector4s(value: PackedVector4Array) -> Array:
334
+ var serialised: Array = []
335
+ for element: Vector4 in value:
336
+ serialised.append(_serialize_vector4(element))
337
+ return serialised
338
+
339
+
340
+ func _serialize_colors(value: PackedColorArray) -> Array:
341
+ var serialised: Array = []
342
+ for element: Color in value:
343
+ serialised.append(_serialize_color(element))
344
+ return serialised
345
+
346
+
347
+ # A pathless Resource says so rather than inventing an empty path for the caller to load.
348
+ func _serialize_object(value: Object) -> Variant:
349
+ if value is Resource:
350
+ var resource: Resource = value
351
+ if resource.resource_path.is_empty():
352
+ return {"class": resource.get_class(), "_type": "Resource"}
353
+ return {"path": resource.resource_path, "class": resource.get_class(), "_type": "Resource"}
354
+ if value is Node:
355
+ var node: Node = value
356
+ if node.is_inside_tree():
357
+ return {"path": str(node.get_path()), "class": node.get_class(), "_type": "Node"}
358
+ return _describe_object(value)
359
+
360
+
361
+ ## A plain object, said in enough detail to tell it from the one beside it.
362
+ ##
363
+ ## `{"class": "RefCounted"}` was all of it, so a roster of twelve people came back as the word
364
+ ## RefCounted twelve times: correct, useless, and indistinguishable from a list of twelve of
365
+ ## anything else. The state worth looking at in a game is lists of these, so each one carries what
366
+ ## it is (the `class_name` its script declares, or the script's path when it has no name), what it
367
+ ## says about itself when the game has given it a `_to_string`, and an id that is at least
368
+ ## different per object. None of it is invented: every field is left out when there is nothing to
369
+ ## put in it, because a key holding "" is a worse answer than a key that is not there.
370
+ func _describe_object(value: Object) -> Dictionary:
371
+ var described: Dictionary = {"class": value.get_class(), "_type": "Object"}
372
+ # Asked as a Variant and narrowed, because get_script answers one and an object without a
373
+ # script answers null: casting that to Script is the unsafe cast the engine refuses to compile.
374
+ var attached: Variant = value.get_script()
375
+ if attached is Script:
376
+ var script: Script = attached
377
+ var declared: String = String(script.get_global_name())
378
+ if not declared.is_empty():
379
+ described["script_class"] = declared
380
+ elif not script.resource_path.is_empty():
381
+ described["script"] = script.resource_path
382
+ # Only when the game wrote one. The default is "<RefCounted#31098236>", which is the id again
383
+ # in a costume and reads as if the object had been asked and had nothing to say.
384
+ if value.has_method("_to_string"):
385
+ described["says"] = value.to_string()
386
+ described["id"] = value.get_instance_id()
387
+ return described
388
+
389
+
390
+ func _build_vector2(fields: Dictionary) -> Vector2:
391
+ return Vector2(Read.as_float(fields.get("x", 0)), Read.as_float(fields.get("y", 0)))
392
+
393
+
394
+ func _build_vector2i(fields: Dictionary) -> Vector2i:
395
+ return Vector2i(Read.as_int(fields.get("x", 0)), Read.as_int(fields.get("y", 0)))
396
+
397
+
398
+ func _build_vector3(fields: Dictionary) -> Vector3:
399
+ return Vector3(
400
+ Read.as_float(fields.get("x", 0)),
401
+ Read.as_float(fields.get("y", 0)),
402
+ Read.as_float(fields.get("z", 0))
403
+ )
404
+
405
+
406
+ func _build_vector3i(fields: Dictionary) -> Vector3i:
407
+ return Vector3i(
408
+ Read.as_int(fields.get("x", 0)), Read.as_int(fields.get("y", 0)), Read.as_int(fields.get("z", 0))
409
+ )
410
+
411
+
412
+ func _build_vector4(fields: Dictionary) -> Vector4:
413
+ return Vector4(
414
+ Read.as_float(fields.get("x", 0)),
415
+ Read.as_float(fields.get("y", 0)),
416
+ Read.as_float(fields.get("z", 0)),
417
+ Read.as_float(fields.get("w", 0))
418
+ )
419
+
420
+
421
+ func _build_vector4i(fields: Dictionary) -> Vector4i:
422
+ return Vector4i(
423
+ Read.as_int(fields.get("x", 0)),
424
+ Read.as_int(fields.get("y", 0)),
425
+ Read.as_int(fields.get("z", 0)),
426
+ Read.as_int(fields.get("w", 0))
427
+ )
428
+
429
+
430
+ ## A rectangle from its two corners, or from the four numbers this tool answered with before the
431
+ ## serialisers were made one, so an answer somebody kept can still be written back.
432
+ func _build_rect2(fields: Dictionary) -> Rect2:
433
+ if not fields.has("position"):
434
+ return Rect2(
435
+ Read.as_float(fields.get("x", 0)),
436
+ Read.as_float(fields.get("y", 0)),
437
+ Read.as_float(fields.get("width", 0)),
438
+ Read.as_float(fields.get("height", 0))
439
+ )
440
+ return Rect2(_vector2_at(fields, "position"), _vector2_at(fields, "size"))
441
+
442
+
443
+ func _build_rect2i(fields: Dictionary) -> Rect2i:
444
+ if not fields.has("position"):
445
+ return Rect2i(
446
+ Read.as_int(fields.get("x", 0)),
447
+ Read.as_int(fields.get("y", 0)),
448
+ Read.as_int(fields.get("width", 0)),
449
+ Read.as_int(fields.get("height", 0))
450
+ )
451
+ return Rect2i(_vector2i_at(fields, "position"), _vector2i_at(fields, "size"))
452
+
453
+
454
+ func _build_plane(fields: Dictionary) -> Plane:
455
+ return Plane(_vector3_at(fields, "normal"), Read.as_float(fields.get("d", 0)))
456
+
457
+
458
+ func _build_quaternion(fields: Dictionary) -> Quaternion:
459
+ return Quaternion(
460
+ Read.as_float(fields.get("x", 0)),
461
+ Read.as_float(fields.get("y", 0)),
462
+ Read.as_float(fields.get("z", 0)),
463
+ Read.as_float(fields.get("w", 0))
464
+ )
465
+
466
+
467
+ func _build_aabb(fields: Dictionary) -> AABB:
468
+ return AABB(_vector3_at(fields, "position"), _vector3_at(fields, "size"))
469
+
470
+
471
+ func _build_basis(fields: Dictionary) -> Basis:
472
+ return Basis(
473
+ _vector3_at(fields, "x", Vector3.RIGHT),
474
+ _vector3_at(fields, "y", Vector3.UP),
475
+ _vector3_at(fields, "z", Vector3.BACK)
476
+ )
477
+
478
+
479
+ func _build_transform2d(fields: Dictionary) -> Transform2D:
480
+ return Transform2D(
481
+ _vector2_at(fields, "x", Vector2.RIGHT),
482
+ _vector2_at(fields, "y", Vector2.DOWN),
483
+ _vector2_at(fields, "origin")
484
+ )
485
+
486
+
487
+ func _build_transform3d(fields: Dictionary) -> Transform3D:
488
+ var held: Variant = fields.get("basis", null)
489
+ if not held is Dictionary:
490
+ return Transform3D(Basis.IDENTITY, _vector3_at(fields, "origin"))
491
+ var columns: Dictionary = held
492
+ return Transform3D(_build_basis(columns), _vector3_at(fields, "origin"))
493
+
494
+
495
+ func _build_projection(fields: Dictionary) -> Projection:
496
+ return Projection(
497
+ _vector4_at(fields, "x"), _vector4_at(fields, "y"), _vector4_at(fields, "z"), _vector4_at(fields, "w")
498
+ )
499
+
500
+
501
+ func _build_color(fields: Dictionary) -> Color:
502
+ return Color(
503
+ Read.as_float(fields.get("r", 0)),
504
+ Read.as_float(fields.get("g", 0)),
505
+ Read.as_float(fields.get("b", 0)),
506
+ Read.as_float(fields.get("a", 1), 1.0)
507
+ )
508
+
509
+
510
+ func _build_node_path(fields: Dictionary) -> NodePath:
511
+ return NodePath(str(fields.get("path", "")))
512
+
513
+
514
+ func _build_bytes(fields: Dictionary) -> PackedByteArray:
515
+ return Marshalls.base64_to_raw(str(fields.get("base64", "")))
516
+
517
+
518
+ ## One component of a compound value, as the type its constructor wants.
519
+ ##
520
+ ## Their own functions because a constructor refuses a Variant under `unsafe_call_argument`, and
521
+ ## because a component that is not there at all has to answer with a default rather than take the
522
+ ## whole build down with it.
523
+ ##
524
+ ## The component is built from its keys rather than deserialised, so it does not matter whether it
525
+ ## carries a tag. A caller writing a transform by hand writes {"x": {"x": 1, "y": 0}}, and that is
526
+ ## the same value as the tagged form this answers with.
527
+ ##
528
+ ## [param fallback] is what a missing component is, which is the zero vector everywhere except the
529
+ ## columns of a basis: a matrix half given should come out the identity it was being varied from
530
+ ## rather than the degenerate one that collapses everything drawn with it.
531
+ func _vector2_at(fields: Dictionary, key: String, fallback: Vector2 = Vector2.ZERO) -> Vector2:
532
+ var held: Variant = fields.get(key, null)
533
+ if not held is Dictionary:
534
+ return fallback
535
+ var component: Dictionary = held
536
+ return _build_vector2(component)
537
+
538
+
539
+ func _vector2i_at(fields: Dictionary, key: String, fallback: Vector2i = Vector2i.ZERO) -> Vector2i:
540
+ var held: Variant = fields.get(key, null)
541
+ if not held is Dictionary:
542
+ return fallback
543
+ var component: Dictionary = held
544
+ return _build_vector2i(component)
545
+
546
+
547
+ func _vector3_at(fields: Dictionary, key: String, fallback: Vector3 = Vector3.ZERO) -> Vector3:
548
+ var held: Variant = fields.get(key, null)
549
+ if not held is Dictionary:
550
+ return fallback
551
+ var component: Dictionary = held
552
+ return _build_vector3(component)
553
+
554
+
555
+ func _vector4_at(fields: Dictionary, key: String, fallback: Vector4 = Vector4.ZERO) -> Vector4:
556
+ var held: Variant = fields.get(key, null)
557
+ if not held is Dictionary:
558
+ return fallback
559
+ var component: Dictionary = held
560
+ return _build_vector4(component)
@@ -0,0 +1 @@
1
+ uid://c2vuamvo4qzs5
@@ -1,6 +1,11 @@
1
1
  extends RefCounted
2
2
 
3
- # A value out of a dictionary that came from JSON, as the type the operation wants.
3
+ # A value out of a dictionary that came from JSON, as the type the caller wants.
4
+ #
5
+ # This file is the one beside the operations, and `bun run sync:gd` copies it into each addon,
6
+ # because an addon is installed as a directory and cannot preload out of one. The runtime addon's
7
+ # copy ships inside exported games and the editor addon's does not, which is the other reason
8
+ # neither of them reaches into the other.
4
9
  #
5
10
  # `int(value)`, `float(value)` and `bool(value)` take a Variant, and a project holding
6
11
  # `unsafe_call_argument` at error level will not compile a script that hands one over. These