gdharness 1.2.36 → 1.2.37
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 +314 -68
- package/build/godot/addons/gdharness_editor/headless_check.gd +374 -0
- package/build/godot/addons/gdharness_editor/headless_check.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +3 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +345 -10
- package/build/godot/addons/gdharness_editor/serialisation.gd +103 -5
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +68 -18
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +24 -13
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +9 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +3 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +103 -5
- package/build/godot/operations/editor_tools.gd +57 -0
- package/build/godot/operations/gdscript_analysis.gd +26 -2
- package/build/godot/operations/gdscript_authoring.gd +5 -2
- package/build/godot/operations/godot_operations.gd +51 -1
- package/build/godot/operations/property_values.gd +3 -0
- package/build/godot/operations/serialisation.gd +103 -5
- package/build/index.js +304 -58
- package/package.json +1 -1
|
@@ -16,7 +16,16 @@ extends RefCounted
|
|
|
16
16
|
## - [code]pack[/code] keeps only what the scene itself owns, so a property set on a node inside an
|
|
17
17
|
## instance, a node deleted from one and a node moved out of one were answered as done and were
|
|
18
18
|
## not in the file.
|
|
19
|
-
|
|
19
|
+
##
|
|
20
|
+
## A headless engine writes for a project no editor serves, and there one more thing differs: the
|
|
21
|
+
## editor gives a script that is not a tool a placeholder, and anywhere else instancing a scene runs
|
|
22
|
+
## every script in it. Measured on 4.7.2, an [code]_init[/code] setting modulate and an exported
|
|
23
|
+
## setter setting scale were both saved into the file by a write to another node, and a root whose
|
|
24
|
+
## script failed in [code]_init[/code] was saved without it. Outside the editor, then, what a node
|
|
25
|
+
## stores is put back to what the file holds wherever the tool did not ask for a change, before
|
|
26
|
+
## anything is saved, and a write is refused when that does not hold. See [HeadlessCheck].
|
|
27
|
+
|
|
28
|
+
const HeadlessCheck = preload("headless_check.gd")
|
|
20
29
|
const PropertyValues = preload("property_values.gd")
|
|
21
30
|
|
|
22
31
|
## Whether a scan asked for through [method scan_later] is still to start, which a caller waiting
|
|
@@ -27,6 +36,19 @@ var root: Node = null
|
|
|
27
36
|
var path: String = ""
|
|
28
37
|
var _base_paths: Dictionary = {}
|
|
29
38
|
var _state: SceneState = null
|
|
39
|
+
## What the file stores on each node it was opened with, by the node's instance id, so a node keeps
|
|
40
|
+
## its record when a tool moves or renames it. Only kept outside the editor.
|
|
41
|
+
var _filed: Dictionary = {}
|
|
42
|
+
## The properties a tool set on each node, by instance id: the differences it asked for.
|
|
43
|
+
var _asked: Dictionary = {}
|
|
44
|
+
## What each node a tool set properties on held before it did, by instance id.
|
|
45
|
+
var _before: Dictionary = {}
|
|
46
|
+
## Each copy a tool made, by instance id, to the instance id of the node it copies.
|
|
47
|
+
var _copied_from: Dictionary = {}
|
|
48
|
+
## The embedded resources a tool reached into through an indexed property, by their id in the file.
|
|
49
|
+
var _reached: Dictionary = {}
|
|
50
|
+
## The file as it was opened, to compare its embedded resources with what the write saves.
|
|
51
|
+
var _text: String = ""
|
|
30
52
|
|
|
31
53
|
|
|
32
54
|
## Opens [param scene_path], answering a refusal, or {} with [member root] set.
|
|
@@ -56,12 +78,59 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
|
|
|
56
78
|
for index: int in base.get_node_count():
|
|
57
79
|
_base_paths[_relative(str(base.get_node_path(index)))] = true
|
|
58
80
|
base = base.get_base_scene_state()
|
|
81
|
+
# On a read as well, which would otherwise answer what the scripts set as what the file holds.
|
|
82
|
+
if not Engine.is_editor_hint():
|
|
83
|
+
if for_writing:
|
|
84
|
+
_text = FileAccess.get_file_as_string(path)
|
|
85
|
+
for index: int in _state.get_node_count():
|
|
86
|
+
var node: Node = _node_at(str(_state.get_node_path(index)))
|
|
87
|
+
if node != null:
|
|
88
|
+
_filed[node.get_instance_id()] = _stored_at(_state, index)
|
|
89
|
+
var unsettled: String = settle()
|
|
90
|
+
if not unsettled.is_empty():
|
|
91
|
+
return refuse(("Nothing was written: " if for_writing else "Not read: ") + unsettled)
|
|
59
92
|
return {}
|
|
60
93
|
|
|
61
94
|
|
|
95
|
+
## Puts back what the scene's scripts changed on every node a tool has not asked to change, as
|
|
96
|
+
## [method write] does, answering "" or why that did not hold. Called once a node has come into the
|
|
97
|
+
## scene, so what its scripts did on the way in is not taken for the tool's own change.
|
|
98
|
+
func settle() -> String:
|
|
99
|
+
if Engine.is_editor_hint():
|
|
100
|
+
return ""
|
|
101
|
+
var packed: PackedScene = PackedScene.new()
|
|
102
|
+
var packing: Error = packed.pack(root)
|
|
103
|
+
if packing != OK:
|
|
104
|
+
return "Failed to pack %s: %s" % [path, error_string(packing)]
|
|
105
|
+
return _settled(packed)[1]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
## Takes [param names] on [param node] as the tool's own changes, which a write saves as they are.
|
|
109
|
+
## Called before the tool sets them.
|
|
110
|
+
func asked(node: Node, names: Array) -> void:
|
|
111
|
+
var id: int = node.get_instance_id()
|
|
112
|
+
var on: Dictionary = _asked.get_or_add(id, {})
|
|
113
|
+
if not _before.has(id):
|
|
114
|
+
_before[id] = HeadlessCheck.stored(node)
|
|
115
|
+
for name: Variant in names:
|
|
116
|
+
var property: String = str(name)
|
|
117
|
+
on[property] = true
|
|
118
|
+
on[property.get_slice(":", 0)] = true
|
|
119
|
+
_reached.merge(HeadlessCheck.reached(node, property))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## Takes each copy in [param copies], keyed by the node it was made from, as holding what that node's
|
|
123
|
+
## file entry holds.
|
|
124
|
+
func copied(copies: Dictionary) -> void:
|
|
125
|
+
for source: Variant in copies:
|
|
126
|
+
var original: Node = source
|
|
127
|
+
var copy: Node = copies[source]
|
|
128
|
+
_copied_from[copy.get_instance_id()] = original.get_instance_id()
|
|
129
|
+
|
|
130
|
+
|
|
62
131
|
## Why [param scene_path] cannot be written while the editor has it open, or "".
|
|
63
132
|
static func unsaved_in_editor(scene_path: String) -> String:
|
|
64
|
-
if not EditorInterface.get_unsaved_scenes().has(scene_path):
|
|
133
|
+
if not Engine.is_editor_hint() or not EditorInterface.get_unsaved_scenes().has(scene_path):
|
|
65
134
|
return ""
|
|
66
135
|
return (
|
|
67
136
|
(
|
|
@@ -142,16 +211,25 @@ static func make_folder_for(file_path: String) -> String:
|
|
|
142
211
|
## Why [param script], loaded from [param script_path], cannot be attached because the editor could not
|
|
143
212
|
## compile it, or "". A script that did not compile has no base type, and the refusal built from that
|
|
144
213
|
## said "start.gd extends , so it cannot be on a Control root" about a script that was fine, whose
|
|
145
|
-
## failure was a class it called being behind in the editor.
|
|
214
|
+
## failure was a class it called being behind in the editor. A headless engine compiles every
|
|
215
|
+
## script from its file, so only an editor can hold a copy that is behind.
|
|
146
216
|
static func uncompiled(script: Script, script_path: String) -> String:
|
|
147
217
|
if not str(script.get_instance_base_type()).is_empty():
|
|
148
218
|
return ""
|
|
219
|
+
if not Engine.is_editor_hint():
|
|
220
|
+
return (
|
|
221
|
+
(
|
|
222
|
+
"%s does not compile, so it cannot be attached. script_diagnostics on it gives the "
|
|
223
|
+
+ "parser's errors."
|
|
224
|
+
)
|
|
225
|
+
% script_path
|
|
226
|
+
)
|
|
149
227
|
return (
|
|
150
228
|
(
|
|
151
|
-
"%s does not compile in the editor, so it cannot be attached. script_diagnostics on it
|
|
152
|
-
+ "the parser's errors. If they deny a member that another class's file declares,
|
|
153
|
-
+ "editor's copy of that class is behind: editor_rescan with reloadScript on that
|
|
154
|
-
+ "ask again."
|
|
229
|
+
"%s does not compile in the editor, so it cannot be attached. script_diagnostics on it "
|
|
230
|
+
+ "gives the parser's errors. If they deny a member that another class's file declares, "
|
|
231
|
+
+ "the editor's copy of that class is behind: editor_rescan with reloadScript on that "
|
|
232
|
+
+ "class, then ask again."
|
|
155
233
|
)
|
|
156
234
|
% script_path
|
|
157
235
|
)
|
|
@@ -221,6 +299,26 @@ func write(check: Callable = Callable()) -> Dictionary:
|
|
|
221
299
|
var packing: Error = packed.pack(root)
|
|
222
300
|
if packing != OK:
|
|
223
301
|
return refuse("Failed to pack %s: %s" % [path, error_string(packing)])
|
|
302
|
+
var headless: bool = not Engine.is_editor_hint()
|
|
303
|
+
if headless:
|
|
304
|
+
var settled: Array = _settled(packed)
|
|
305
|
+
if not str(settled[1]).is_empty():
|
|
306
|
+
return refuse("Nothing was written: " + str(settled[1]))
|
|
307
|
+
packed = settled[0]
|
|
308
|
+
if not path.ends_with(".tscn") and _holds_scripted_embedded():
|
|
309
|
+
return refuse(
|
|
310
|
+
(
|
|
311
|
+
(
|
|
312
|
+
(
|
|
313
|
+
"Nothing was written: %s is a binary scene holding an embedded resource with a script "
|
|
314
|
+
+ "that is not a tool, and what that script changed when a headless engine loaded it "
|
|
315
|
+
+ "cannot be told apart from the file here. "
|
|
316
|
+
)
|
|
317
|
+
% path
|
|
318
|
+
)
|
|
319
|
+
+ HeadlessCheck.REMEDY
|
|
320
|
+
)
|
|
321
|
+
)
|
|
224
322
|
if check.is_valid():
|
|
225
323
|
var saved: Node = packed.instantiate(PackedScene.GEN_EDIT_STATE_MAIN)
|
|
226
324
|
if saved == null:
|
|
@@ -230,7 +328,33 @@ func write(check: Callable = Callable()) -> Dictionary:
|
|
|
230
328
|
if not missing.is_empty():
|
|
231
329
|
return refuse("Nothing was written: " + missing)
|
|
232
330
|
close()
|
|
233
|
-
|
|
331
|
+
var previous: Variant = null
|
|
332
|
+
if FileAccess.file_exists(path):
|
|
333
|
+
previous = FileAccess.get_file_as_bytes(path)
|
|
334
|
+
var written: Dictionary = save_packed(packed, path)
|
|
335
|
+
if not written.is_empty() or not headless or not path.ends_with(".tscn"):
|
|
336
|
+
return written
|
|
337
|
+
var state: SceneState = packed.get_state()
|
|
338
|
+
var values: Array = []
|
|
339
|
+
for index: int in state.get_node_count():
|
|
340
|
+
values.append_array(_stored_at(state, index).values())
|
|
341
|
+
var unkept: String = HeadlessCheck.settle_saved(
|
|
342
|
+
path, _text, values, null, {}, _reached, func() -> Error: return save_keeping_uid(packed, path)
|
|
343
|
+
)
|
|
344
|
+
if unkept.is_empty():
|
|
345
|
+
return {}
|
|
346
|
+
return {"ok": false, "error": "Nothing was written: " + unkept + HeadlessCheck.put_back(path, previous)}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
func _holds_scripted_embedded() -> bool:
|
|
350
|
+
var seen: Dictionary = {}
|
|
351
|
+
var nodes: Array[Node] = [root]
|
|
352
|
+
while not nodes.is_empty():
|
|
353
|
+
var node: Node = nodes.pop_back()
|
|
354
|
+
if HeadlessCheck.holds_scripted_embedded(node, seen):
|
|
355
|
+
return true
|
|
356
|
+
nodes.append_array(node.get_children())
|
|
357
|
+
return false
|
|
234
358
|
|
|
235
359
|
|
|
236
360
|
## Writes [param packed] to [param scene_path], making its directory, and brings the editor up to
|
|
@@ -241,18 +365,51 @@ static func save_packed(packed: PackedScene, scene_path: String) -> Dictionary:
|
|
|
241
365
|
var made: Error = DirAccess.make_dir_recursive_absolute(directory)
|
|
242
366
|
if made != OK:
|
|
243
367
|
return {"ok": false, "error": "Could not create %s: %s" % [directory, error_string(made)]}
|
|
244
|
-
var saving: Error =
|
|
368
|
+
var saving: Error = save_keeping_uid(packed, scene_path)
|
|
245
369
|
if saving != OK:
|
|
246
370
|
return {"ok": false, "error": "Failed to save %s: %s" % [scene_path, error_string(saving)]}
|
|
247
371
|
refresh(scene_path)
|
|
248
372
|
return {}
|
|
249
373
|
|
|
250
374
|
|
|
375
|
+
## Saves [param resource] at [param file_path], keeping the uid the file already has.
|
|
376
|
+
##
|
|
377
|
+
## The editor saves through its uid cache, which knows every file's. A headless engine that has not
|
|
378
|
+
## imported the project has none, and a resave dropped the uid from the header, so every reference
|
|
379
|
+
## made through it stopped resolving. Read from the file itself, since the loader does not answer it
|
|
380
|
+
## for a text scene there, and a new file is given one as the editor gives it.
|
|
381
|
+
static func save_keeping_uid(resource: Resource, file_path: String) -> Error:
|
|
382
|
+
if Engine.is_editor_hint():
|
|
383
|
+
return ResourceSaver.save(resource, file_path)
|
|
384
|
+
var uid: int = _uid_in(file_path)
|
|
385
|
+
var saving: Error = ResourceSaver.save(resource, file_path)
|
|
386
|
+
if saving != OK:
|
|
387
|
+
return saving
|
|
388
|
+
if uid == ResourceUID.INVALID_ID:
|
|
389
|
+
uid = ResourceUID.create_id()
|
|
390
|
+
return ResourceSaver.set_uid(file_path, uid)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
## The uid the file at [param file_path] declares, or [constant ResourceUID.INVALID_ID].
|
|
394
|
+
static func _uid_in(file_path: String) -> int:
|
|
395
|
+
if not FileAccess.file_exists(file_path):
|
|
396
|
+
return ResourceUID.INVALID_ID
|
|
397
|
+
var file: FileAccess = FileAccess.open(file_path, FileAccess.READ)
|
|
398
|
+
if file != null:
|
|
399
|
+
var header: RegExMatch = RegEx.create_from_string('uid="(uid://[a-z0-9]+)"').search(file.get_line())
|
|
400
|
+
file.close()
|
|
401
|
+
if header != null:
|
|
402
|
+
return ResourceUID.text_to_id(header.get_string(1))
|
|
403
|
+
return ResourceLoader.get_resource_uid(file_path)
|
|
404
|
+
|
|
405
|
+
|
|
251
406
|
## Brings the editor up to date with a scene file written under it: the cached copy anything holds,
|
|
252
407
|
## the editor's file list, and the tab showing it, whether current or not.
|
|
253
408
|
static func refresh(scene_path: String) -> void:
|
|
254
409
|
if ResourceLoader.has_cached(scene_path):
|
|
255
410
|
var _replaced: Resource = ResourceLoader.load(scene_path, "", ResourceLoader.CACHE_MODE_REPLACE)
|
|
411
|
+
if not Engine.is_editor_hint():
|
|
412
|
+
return
|
|
256
413
|
scan_later()
|
|
257
414
|
if EditorInterface.get_open_scenes().has(scene_path):
|
|
258
415
|
EditorInterface.reload_scene_from_path(scene_path)
|
|
@@ -294,7 +451,185 @@ static func _start_scan() -> void:
|
|
|
294
451
|
## Whether the editor holds unsaved changes to [param scene_path], for a read to say that the file
|
|
295
452
|
## it answers from is not what the editor shows.
|
|
296
453
|
static func unsaved(scene_path: String) -> bool:
|
|
297
|
-
return EditorInterface.get_unsaved_scenes().has(scene_path)
|
|
454
|
+
return Engine.is_editor_hint() and EditorInterface.get_unsaved_scenes().has(scene_path)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
## [param packed] with what the scene's scripts changed put back, as [pack, ""], or [null, why not].
|
|
458
|
+
##
|
|
459
|
+
## On a node the tool did not ask to change, a difference is put back to what the file holds, or, for
|
|
460
|
+
## a property the file does not set, to what the node would load with without it. On a node it did
|
|
461
|
+
## ask to change, through a script that is not a tool, every property it did not name is put back to
|
|
462
|
+
## what it held before. Packed again afterwards, since a setter can change one property through
|
|
463
|
+
## another, and refused if anything is still different.
|
|
464
|
+
func _settled(packed: PackedScene) -> Array:
|
|
465
|
+
var stray: Array[Array] = _strays(packed.get_state())
|
|
466
|
+
var moved: Array[Array] = _moved_by_scripts()
|
|
467
|
+
if stray.is_empty() and moved.is_empty():
|
|
468
|
+
return [packed, ""]
|
|
469
|
+
for found: Array in stray:
|
|
470
|
+
var node: Node = found[0]
|
|
471
|
+
var name: String = found[1]
|
|
472
|
+
node.set(name, found[3] if found[2] else _unset_value(node, name))
|
|
473
|
+
for found: Array in moved:
|
|
474
|
+
var node: Node = found[0]
|
|
475
|
+
var before: Dictionary = _before[node.get_instance_id()]
|
|
476
|
+
for name: String in found[1]:
|
|
477
|
+
node.set(name, before.get(name, HeadlessCheck.default_of(node, name)))
|
|
478
|
+
var again: PackedScene = PackedScene.new()
|
|
479
|
+
var packing: Error = again.pack(root)
|
|
480
|
+
if packing != OK:
|
|
481
|
+
return [null, "Failed to pack %s: %s" % [path, error_string(packing)]]
|
|
482
|
+
var left: Array[Array] = _strays(again.get_state())
|
|
483
|
+
left.append_array(_moved_by_scripts())
|
|
484
|
+
if left.is_empty():
|
|
485
|
+
return [again, ""]
|
|
486
|
+
var first: Array = left[0]
|
|
487
|
+
var stray_node: Node = first[0]
|
|
488
|
+
var named: String = str(first[1])
|
|
489
|
+
if first[1] is Array:
|
|
490
|
+
var names: Array = first[1]
|
|
491
|
+
named = str(names[0])
|
|
492
|
+
return [null, _stray_text(stray_node, named)]
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
## Each node the tool asked to change through a script that is not a tool, with the properties it did
|
|
496
|
+
## not name that no longer hold what they held before, as [node, names].
|
|
497
|
+
func _moved_by_scripts() -> Array[Array]:
|
|
498
|
+
var moved: Array[Array] = []
|
|
499
|
+
for key: Variant in _asked:
|
|
500
|
+
var id: int = key
|
|
501
|
+
var node: Node = instance_from_id(id) as Node
|
|
502
|
+
if node == null or not root.is_ancestor_of(node) and node != root:
|
|
503
|
+
continue
|
|
504
|
+
var on: Dictionary = _asked[id]
|
|
505
|
+
if not HeadlessCheck.through_script(node, on):
|
|
506
|
+
continue
|
|
507
|
+
var before: Dictionary = _before[id]
|
|
508
|
+
var changed: Array[String] = HeadlessCheck.differing(node, before, on)
|
|
509
|
+
if not changed.is_empty():
|
|
510
|
+
moved.append([node, changed])
|
|
511
|
+
return moved
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
## Every property [param state] stores differently from the file, on a node the tool did not ask to
|
|
515
|
+
## change it on, as [node, name, whether the file stores it, the file's value].
|
|
516
|
+
func _strays(state: SceneState) -> Array[Array]:
|
|
517
|
+
var stray: Array[Array] = []
|
|
518
|
+
for index: int in state.get_node_count():
|
|
519
|
+
var node: Node = _node_at(str(state.get_node_path(index)))
|
|
520
|
+
if node == null:
|
|
521
|
+
continue
|
|
522
|
+
var id: int = node.get_instance_id()
|
|
523
|
+
if _asked.has(id):
|
|
524
|
+
continue
|
|
525
|
+
var filed: Dictionary = _filed.get(id, _filed.get(_copied_from.get(id, 0), {}))
|
|
526
|
+
# A node is instanced before it is added to its parent, which is before its children are, so
|
|
527
|
+
# what a script does as a scene loads stays on the script's own node. Anything else a pack
|
|
528
|
+
# changes is Godot's doing and the editor's pack does it too: a Control saved without its
|
|
529
|
+
# grow directions gets them back from anchors_preset's setter, and loses an anchors_preset
|
|
530
|
+
# its layout mode does not store.
|
|
531
|
+
if not HeadlessCheck.untooled(node.get_script()) and not HeadlessCheck.untooled(filed.get("script")):
|
|
532
|
+
continue
|
|
533
|
+
var stored: Dictionary = _stored_at(state, index)
|
|
534
|
+
var names: Dictionary = filed.duplicate()
|
|
535
|
+
names.merge(stored)
|
|
536
|
+
for key: Variant in names:
|
|
537
|
+
var name: String = str(key)
|
|
538
|
+
var held: bool = filed.has(name)
|
|
539
|
+
if held == stored.has(name) and (not held or PropertyValues.same(stored[name], filed[name])):
|
|
540
|
+
continue
|
|
541
|
+
stray.append([node, name, held, filed.get(name)])
|
|
542
|
+
return stray
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
func _stray_text(node: Node, name: String) -> String:
|
|
546
|
+
var at: String = str(root.name) if node == root else path_of(node)
|
|
547
|
+
if name == "script":
|
|
548
|
+
return (
|
|
549
|
+
(
|
|
550
|
+
(
|
|
551
|
+
"the script of %s did not start when a headless engine instanced %s, so the node would be "
|
|
552
|
+
+ "saved without it; the engine's messages give its errors. "
|
|
553
|
+
)
|
|
554
|
+
% [at, path]
|
|
555
|
+
)
|
|
556
|
+
+ HeadlessCheck.REMEDY
|
|
557
|
+
)
|
|
558
|
+
return (
|
|
559
|
+
(
|
|
560
|
+
(
|
|
561
|
+
"%s.%s would be saved differently from what %s holds, and the call did not ask for it: the "
|
|
562
|
+
+ "scene's scripts ran when a headless engine instanced it, as they do in the game and not in "
|
|
563
|
+
+ "an editor. "
|
|
564
|
+
)
|
|
565
|
+
% [at, name, path]
|
|
566
|
+
)
|
|
567
|
+
+ HeadlessCheck.REMEDY
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
## What [param node] would load with for [param name] if this file did not set it: what the scenes
|
|
572
|
+
## it comes from store, followed down through every scene instanced or inherited, else its script's
|
|
573
|
+
## or class's default. Read from their files, since instancing them would run their scripts.
|
|
574
|
+
func _unset_value(node: Node, name: String) -> Variant:
|
|
575
|
+
var base: SceneState = _state.get_base_scene_state() if _state != null else null
|
|
576
|
+
if node == root or _base_paths.has(path_of(node)):
|
|
577
|
+
return _value_in(base, path_of(node), node, name)
|
|
578
|
+
var top: Node = node
|
|
579
|
+
while top != root and top.owner != root:
|
|
580
|
+
top = top.get_parent()
|
|
581
|
+
if top == root or top.scene_file_path.is_empty():
|
|
582
|
+
return HeadlessCheck.default_of(node, name)
|
|
583
|
+
if _base_paths.has(path_of(top)):
|
|
584
|
+
return _value_in(base, path_of(node), node, name)
|
|
585
|
+
var instanced: PackedScene = ResourceLoader.load(top.scene_file_path) as PackedScene
|
|
586
|
+
return _value_in(
|
|
587
|
+
instanced.get_state() if instanced != null else null, str(top.get_path_to(node)), node, name
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
## What [param state] gives the node at [param at] for [param name], following it into the scene an
|
|
592
|
+
## enclosing node instances, or into the base scene, until a file sets it.
|
|
593
|
+
func _value_in(state: SceneState, at: String, node: Node, name: String) -> Variant:
|
|
594
|
+
while state != null:
|
|
595
|
+
var inside: String = ""
|
|
596
|
+
var instanced: PackedScene = null
|
|
597
|
+
for index: int in state.get_node_count():
|
|
598
|
+
var here: String = _relative(str(state.get_node_path(index)))
|
|
599
|
+
if here == at:
|
|
600
|
+
for property: int in state.get_node_property_count(index):
|
|
601
|
+
if str(state.get_node_property_name(index, property)) == name:
|
|
602
|
+
return state.get_node_property_value(index, property)
|
|
603
|
+
var scene: PackedScene = state.get_node_instance(index)
|
|
604
|
+
if scene == null:
|
|
605
|
+
continue
|
|
606
|
+
var encloses: bool = here == at or here == "." or at.begins_with(here + "/")
|
|
607
|
+
if encloses and (instanced == null or here.length() > inside.length() or inside == "."):
|
|
608
|
+
inside = here
|
|
609
|
+
instanced = scene
|
|
610
|
+
if instanced != null:
|
|
611
|
+
if inside == at:
|
|
612
|
+
at = "."
|
|
613
|
+
elif inside != ".":
|
|
614
|
+
at = at.substr(inside.length() + 1)
|
|
615
|
+
state = instanced.get_state()
|
|
616
|
+
else:
|
|
617
|
+
state = state.get_base_scene_state()
|
|
618
|
+
return HeadlessCheck.default_of(node, name)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
func _node_at(node_path: String) -> Node:
|
|
622
|
+
var at: String = _relative(node_path)
|
|
623
|
+
return root if at == "." else root.get_node_or_null(NodePath(at))
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
static func _stored_at(state: SceneState, index: int) -> Dictionary:
|
|
627
|
+
var stored: Dictionary = {}
|
|
628
|
+
for property: int in state.get_node_property_count(index):
|
|
629
|
+
stored[str(state.get_node_property_name(index, property))] = (state.get_node_property_value(
|
|
630
|
+
index, property
|
|
631
|
+
))
|
|
632
|
+
return stored
|
|
298
633
|
|
|
299
634
|
|
|
300
635
|
func _relative(node_path: String) -> String:
|
|
@@ -16,6 +16,10 @@ const Read = preload("reading.gd")
|
|
|
16
16
|
const NUMBERS: Array[int] = [TYPE_BOOL, TYPE_INT, TYPE_FLOAT]
|
|
17
17
|
const TEXTS: Array[int] = [TYPE_STRING, TYPE_STRING_NAME, TYPE_NODE_PATH]
|
|
18
18
|
|
|
19
|
+
# 2^53. A JSON number is a double, and every whole number below this one in size has a double of
|
|
20
|
+
# its own; from here on two or more share each double, so one arriving tells nobody which was sent.
|
|
21
|
+
const EXACT: int = 9007199254740992
|
|
22
|
+
|
|
19
23
|
# The types that cross as a plain JSON list and have to be built back into themselves. A list is
|
|
20
24
|
# what a caller can write by hand and what the serialiser answers with, and the parameter or
|
|
21
25
|
# property being written is the only thing that knows which of these it wanted.
|
|
@@ -53,6 +57,8 @@ const LIST_TYPES: Array[int] = [
|
|
|
53
57
|
# type it wanted.
|
|
54
58
|
const SERIALISERS: Dictionary = {
|
|
55
59
|
TYPE_NIL: "_serialize_nil",
|
|
60
|
+
TYPE_INT: "_serialize_int",
|
|
61
|
+
TYPE_PACKED_INT64_ARRAY: "_serialize_ints",
|
|
56
62
|
TYPE_FLOAT: "_serialize_float",
|
|
57
63
|
TYPE_PACKED_FLOAT32_ARRAY: "_serialize_floats",
|
|
58
64
|
TYPE_PACKED_FLOAT64_ARRAY: "_serialize_floats",
|
|
@@ -109,6 +115,8 @@ const BUILDERS: Dictionary = {
|
|
|
109
115
|
"NodePath": "_build_node_path",
|
|
110
116
|
"PackedByteArray": "_build_bytes",
|
|
111
117
|
"float": "_build_float",
|
|
118
|
+
"int": "_build_int",
|
|
119
|
+
"RID": "_build_rid",
|
|
112
120
|
"Dictionary": "_build_dictionary",
|
|
113
121
|
}
|
|
114
122
|
|
|
@@ -136,6 +144,8 @@ const REQUIRED: Dictionary = {
|
|
|
136
144
|
"NodePath": [["path"]],
|
|
137
145
|
"PackedByteArray": [["base64"]],
|
|
138
146
|
"float": [["value"]],
|
|
147
|
+
"int": [["value"]],
|
|
148
|
+
"RID": [["id"]],
|
|
139
149
|
"Dictionary": [["entries"]],
|
|
140
150
|
}
|
|
141
151
|
|
|
@@ -243,9 +253,40 @@ func deserialize_value(value: Variant) -> Variant:
|
|
|
243
253
|
return fields
|
|
244
254
|
if tag == "Dictionary" and not pairs(fields["entries"]):
|
|
245
255
|
return fields
|
|
256
|
+
# Digits only: the tag is there to carry what a number cannot, and {"value": 5} meant for some
|
|
257
|
+
# other shape is not to be read as 5.
|
|
258
|
+
if tag == "int" and not (fields["value"] is String and _exact_int(fields["value"]) != null):
|
|
259
|
+
return fields
|
|
260
|
+
if tag == "RID" and _exact_int(fields["id"]) == null:
|
|
261
|
+
return fields
|
|
246
262
|
return call(builder, fields)
|
|
247
263
|
|
|
248
264
|
|
|
265
|
+
## [param held] as the whole number it spells exactly, or null: an int, a JSON number that is whole
|
|
266
|
+
## and no larger than a double holds exactly, the digits of one, or the int tag carrying them. A
|
|
267
|
+
## fraction, or a number past 2^53 that arrived as a number and so may already be a neighbour of the
|
|
268
|
+
## one meant, is no id.
|
|
269
|
+
func _exact_int(held: Variant) -> Variant:
|
|
270
|
+
if held is int:
|
|
271
|
+
return held
|
|
272
|
+
if held is float:
|
|
273
|
+
var number: float = held
|
|
274
|
+
if is_finite(number) and number == floorf(number) and absf(number) < EXACT:
|
|
275
|
+
return int(number)
|
|
276
|
+
return null
|
|
277
|
+
if held is String:
|
|
278
|
+
var digits: String = held
|
|
279
|
+
var parsed: int = digits.to_int()
|
|
280
|
+
if digits.is_valid_int() and str(parsed) == digits:
|
|
281
|
+
return parsed
|
|
282
|
+
return null
|
|
283
|
+
if held is Dictionary:
|
|
284
|
+
var tagged: Dictionary = held
|
|
285
|
+
if str(tagged.get("_type", "")) == "int" and tagged.has("value"):
|
|
286
|
+
return _exact_int(tagged["value"])
|
|
287
|
+
return null
|
|
288
|
+
|
|
289
|
+
|
|
249
290
|
## The components a path reads off [param value], by the names GDScript gives them, or none for a
|
|
250
291
|
## value that has none: a vector's axes, a colour's channels, a rectangle's corners and size, a
|
|
251
292
|
## transform's basis and origin. A value of one of these is a copy wherever it is read, so a write
|
|
@@ -393,6 +434,11 @@ func fitted(value: Variant, type: int) -> Variant:
|
|
|
393
434
|
if not simple.has(type) or not simple.has(typeof(rebuilt)):
|
|
394
435
|
return rebuilt
|
|
395
436
|
|
|
437
|
+
# Digits are read as digits, since read as JSON they become a double and one past 2^53 a
|
|
438
|
+
# neighbour of the number written.
|
|
439
|
+
if rebuilt is String and type == TYPE_INT and _exact_int(rebuilt) != null:
|
|
440
|
+
return _exact_int(rebuilt)
|
|
441
|
+
|
|
396
442
|
if rebuilt is String and type != TYPE_STRING:
|
|
397
443
|
var text: String = rebuilt
|
|
398
444
|
var parsed: Variant = Read.json_or_null(text)
|
|
@@ -408,13 +454,33 @@ func fitted(value: Variant, type: int) -> Variant:
|
|
|
408
454
|
return type_convert(rebuilt, type)
|
|
409
455
|
|
|
410
456
|
|
|
457
|
+
## Why [param given], meant for an int, may not be the number that was sent, or "" when it is. A
|
|
458
|
+
## JSON number is a double wherever it is read, so a whole number from 2^53 on arrives as the nearest
|
|
459
|
+
## double to the one written: 9007199254740993 sent as a number arrives as 9007199254740992, and a
|
|
460
|
+
## seed written that way would be stored as its neighbour with the answer saying it was written.
|
|
461
|
+
static func inexact(given: Variant, type: int) -> String:
|
|
462
|
+
if type != TYPE_INT or (typeof(given) != TYPE_FLOAT and typeof(given) != TYPE_INT):
|
|
463
|
+
return ""
|
|
464
|
+
var number: float = Read.as_float(given)
|
|
465
|
+
if not is_finite(number) or absf(number) < EXACT:
|
|
466
|
+
return ""
|
|
467
|
+
return (
|
|
468
|
+
(
|
|
469
|
+
"%s is 2^53 or more, where a JSON number holds a whole number only to the nearest double,"
|
|
470
|
+
+ ' so it may not be the number sent: send the digits as {"_type": "int", "value": "..."}'
|
|
471
|
+
+ ", the form a read answers with"
|
|
472
|
+
)
|
|
473
|
+
% String.num(number, 0)
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
411
477
|
## How a value of [param type] is written to be read as one, for a refusal to name, or empty for a
|
|
412
478
|
## type that is not built from fields. A refusal naming only the two types left a caller to find
|
|
413
479
|
## the shape in the description of another op.
|
|
414
480
|
static func written_as(type: int) -> String:
|
|
415
481
|
var tag: String = type_string(type)
|
|
416
482
|
var alternatives: Array = REQUIRED.get(tag, [])
|
|
417
|
-
if alternatives.is_empty() or tag
|
|
483
|
+
if alternatives.is_empty() or tag in ["float", "int", "Dictionary"]:
|
|
418
484
|
return ""
|
|
419
485
|
var keys: Array = alternatives[0]
|
|
420
486
|
var parts: Array[String] = []
|
|
@@ -530,10 +596,32 @@ func _serialize_node_path(value: NodePath) -> Dictionary:
|
|
|
530
596
|
return {"path": str(value), "_type": "NodePath"}
|
|
531
597
|
|
|
532
598
|
|
|
533
|
-
#
|
|
534
|
-
#
|
|
599
|
+
# By its id, which is all an RID is from outside the server holding it, and which builds it back:
|
|
600
|
+
# a viewport's RID read off one call is what RenderingServer's render timing takes in the next.
|
|
535
601
|
func _serialize_rid(value: RID) -> Dictionary:
|
|
536
|
-
return {"id": value.get_id(), "_type": "RID"}
|
|
602
|
+
return {"id": _serialize_int(value.get_id()), "_type": "RID"}
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
# A whole number from 2^53 on as its digits, because JSON numbers are doubles on the server's side
|
|
606
|
+
# and on the engine's, and from there on neighbouring whole numbers share a double. A RefCounted's
|
|
607
|
+
# instance id is one, with its top bit set, and a resource uid another; read as numbers they came
|
|
608
|
+
# back as a neighbouring id, which named nothing or something else.
|
|
609
|
+
func _serialize_int(value: int) -> Variant:
|
|
610
|
+
if value > -EXACT and value < EXACT:
|
|
611
|
+
return value
|
|
612
|
+
return {"_type": "int", "value": str(value)}
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
# A packed list of 64-bit numbers stays the JSON list it is unless something in it is too large for
|
|
616
|
+
# a double to hold.
|
|
617
|
+
func _serialize_ints(value: PackedInt64Array) -> Variant:
|
|
618
|
+
for number: int in value:
|
|
619
|
+
if number <= -EXACT or number >= EXACT:
|
|
620
|
+
var items: Array = []
|
|
621
|
+
for each: int in value:
|
|
622
|
+
items.append(_serialize_int(each))
|
|
623
|
+
return items
|
|
624
|
+
return value
|
|
537
625
|
|
|
538
626
|
|
|
539
627
|
# A bound method, said in the terms a caller could act on: what it is on, and what it is called.
|
|
@@ -672,7 +760,7 @@ func _describe_object(value: Object) -> Dictionary:
|
|
|
672
760
|
# in a costume and reads as if the object had been asked and had nothing to say.
|
|
673
761
|
if value.has_method("_to_string"):
|
|
674
762
|
described["says"] = value.to_string()
|
|
675
|
-
described["id"] = value.get_instance_id()
|
|
763
|
+
described["id"] = _serialize_int(value.get_instance_id())
|
|
676
764
|
return described
|
|
677
765
|
|
|
678
766
|
|
|
@@ -808,6 +896,16 @@ func _build_float(fields: Dictionary) -> float:
|
|
|
808
896
|
return Read.as_float(fields)
|
|
809
897
|
|
|
810
898
|
|
|
899
|
+
func _build_int(fields: Dictionary) -> int:
|
|
900
|
+
var exact: int = _exact_int(fields["value"])
|
|
901
|
+
return exact
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
func _build_rid(fields: Dictionary) -> RID:
|
|
905
|
+
var id: int = _exact_int(fields["id"])
|
|
906
|
+
return rid_from_int64(id)
|
|
907
|
+
|
|
908
|
+
|
|
811
909
|
## The entries of a dictionary whose keys are not all text. JSON carries every number as a float,
|
|
812
910
|
## and a dictionary keys 1.0 apart from 1, so a whole-number key is built as an int: the key a
|
|
813
911
|
## game uses far more often, and one JSON cannot tell from the float.
|