gdharness 1.3.40 → 1.3.42
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 +343 -142
- package/build/godot/addons/gdharness_editor/animation_reads.gd +31 -4
- package/build/godot/addons/gdharness_editor/file_reads.gd +36 -1
- package/build/godot/addons/gdharness_editor/file_walk.gd +33 -1
- package/build/godot/addons/gdharness_editor/headless_check.gd +29 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +6 -9
- package/build/godot/addons/gdharness_editor/scene_file.gd +27 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +5 -2
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +6 -2
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +15 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +7 -2
- package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +59 -0
- package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +75 -11
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +77 -22
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +4 -1
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +4 -3
- package/build/godot/addons/gdharness_runtime/runtime_touch.gd +211 -0
- package/build/godot/addons/gdharness_runtime/runtime_touch.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +15 -8
- package/build/godot/operations/animation_reads.gd +31 -4
- package/build/godot/operations/dependencies.gd +5 -2
- package/build/godot/operations/file_reads.gd +36 -1
- package/build/godot/operations/file_walk.gd +33 -1
- package/build/godot/operations/gdscript_analysis.gd +19 -1
- package/build/godot/operations/gdscript_authoring.gd +65 -4
- package/build/godot/operations/import_pipeline.gd +1 -1
- package/build/godot/operations/input_actions.gd +1 -0
- package/build/godot/operations/plugins.gd +39 -1
- package/build/godot/operations/project_config.gd +104 -1
- package/build/godot/operations/property_values.gd +6 -9
- package/package.json +1 -1
|
@@ -64,7 +64,13 @@ static func scene_states(scene_path: String, tree_path: String, state_machine_pa
|
|
|
64
64
|
player_path = "."
|
|
65
65
|
var found: Array = _bare(scene_path, player_path, "AnimationPlayer")
|
|
66
66
|
player = found[0]
|
|
67
|
-
var
|
|
67
|
+
var reached: Node = null
|
|
68
|
+
if player == null and not player_path.is_empty():
|
|
69
|
+
reached = _bare(scene_path, player_path, "Node")[0]
|
|
70
|
+
var unplayed: String = no_player(str(tree.anim_player), reached)
|
|
71
|
+
if reached != null:
|
|
72
|
+
reached.free()
|
|
73
|
+
var answer: Dictionary = states(tree, state_machine_path, player, player_path, unplayed)
|
|
68
74
|
tree.free()
|
|
69
75
|
if player != null:
|
|
70
76
|
player.free()
|
|
@@ -236,6 +242,20 @@ static func no_machine(tree: AnimationTree, state_machine_path: String) -> Strin
|
|
|
236
242
|
return "No state machine at " + state_machine_path
|
|
237
243
|
|
|
238
244
|
|
|
245
|
+
## Why no state finds its animation when [param anim_player], an AnimationTree's path to its
|
|
246
|
+
## player, reaches no AnimationPlayer: [param reached] is the node it reaches instead, null for none.
|
|
247
|
+
static func no_player(anim_player: String, reached: Node) -> String:
|
|
248
|
+
var until: String = "so no state plays anything until scene_node set points anim_player at one."
|
|
249
|
+
if anim_player.is_empty():
|
|
250
|
+
return "The AnimationTree has no anim_player, " + until
|
|
251
|
+
if reached == null:
|
|
252
|
+
return "The AnimationTree's anim_player %s reaches no node, %s" % [anim_player, until]
|
|
253
|
+
return (
|
|
254
|
+
"The AnimationTree's anim_player %s reaches %s, a %s rather than an AnimationPlayer, %s"
|
|
255
|
+
% [anim_player, reached.name, reached.get_class(), until]
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
239
259
|
## Why a state playing [param animation_name] from [param player], at [param player_path] from the
|
|
240
260
|
## scene root as playerNodePath takes it, plays nothing yet. anim_player is relative to the tree
|
|
241
261
|
## ("../Anim"), which no argument takes.
|
|
@@ -254,9 +274,14 @@ static func missing_animation(player: AnimationPlayer, player_path: String, anim
|
|
|
254
274
|
|
|
255
275
|
## The states and transitions of the state machine at [param state_machine_path] in [param tree].
|
|
256
276
|
## A state playing an animation says whether [param player], at [param player_path], has it, as
|
|
257
|
-
## add_state answers: a state naming one it lacks plays nothing, and says so only in a log.
|
|
277
|
+
## add_state answers: a state naming one it lacks plays nothing, and says so only in a log. With no
|
|
278
|
+
## player, each says [param unplayed], what [method no_player] made of the tree's path.
|
|
258
279
|
static func states(
|
|
259
|
-
tree: AnimationTree,
|
|
280
|
+
tree: AnimationTree,
|
|
281
|
+
state_machine_path: String,
|
|
282
|
+
player: AnimationPlayer,
|
|
283
|
+
player_path: String,
|
|
284
|
+
unplayed: String
|
|
260
285
|
) -> Dictionary:
|
|
261
286
|
var sm: AnimationNodeStateMachine = state_machine(tree, state_machine_path)
|
|
262
287
|
if sm == null:
|
|
@@ -270,7 +295,9 @@ static func states(
|
|
|
270
295
|
entry["animationName"] = str(plays.animation)
|
|
271
296
|
var found: bool = player != null and player.has_animation(plays.animation)
|
|
272
297
|
entry["animationFound"] = found
|
|
273
|
-
if player
|
|
298
|
+
if player == null:
|
|
299
|
+
entry["animationMissing"] = unplayed
|
|
300
|
+
elif not found:
|
|
274
301
|
entry["animationMissing"] = missing_animation(player, player_path, str(plays.animation))
|
|
275
302
|
listed.append(entry)
|
|
276
303
|
var transitions: Array[Dictionary] = []
|
|
@@ -11,6 +11,7 @@ extends RefCounted
|
|
|
11
11
|
## initialisers run, and a resource with a script is made with it.
|
|
12
12
|
|
|
13
13
|
const GdhRead = preload("reading.gd")
|
|
14
|
+
const GdhFileWalk = preload("file_walk.gd")
|
|
14
15
|
const GdhSame = preload("same_values.gd")
|
|
15
16
|
const GdhSerialisation = preload("serialisation.gd")
|
|
16
17
|
const GdhShapes = preload("value_shapes.gd")
|
|
@@ -487,7 +488,7 @@ static func _stored(type: String, given: Dictionary, include_defaults: bool) ->
|
|
|
487
488
|
static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
488
489
|
var file: String = res_path.get_slice("::", 0)
|
|
489
490
|
if not FileAccess.file_exists(file):
|
|
490
|
-
return {"ok": false, "error": "No resource at " + file}
|
|
491
|
+
return {"ok": false, "error": GdhFileWalk.new().with_nearest_files("No resource at " + file, file)}
|
|
491
492
|
var whole: Resource = ResourceLoader.load(file, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
492
493
|
if whole == null:
|
|
493
494
|
return {"ok": false, "error": "%s did not load as a resource." % file}
|
|
@@ -516,6 +517,18 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
516
517
|
scripted[str(declared.get("name", ""))] = true
|
|
517
518
|
var serialiser: GdhSerialisation = GdhSerialisation.new()
|
|
518
519
|
var properties: Dictionary = {}
|
|
520
|
+
# A headless engine runs a script that is not a tool as the file loads, so what its _init and
|
|
521
|
+
# setters gave other properties read as values the file sets; the file's own lines say which it
|
|
522
|
+
# does. The editor runs no such script.
|
|
523
|
+
var untooled: bool = script != null and not script.is_tool() and not Engine.is_editor_hint()
|
|
524
|
+
var from_file: bool = (
|
|
525
|
+
untooled and file.ends_with(".tres") and not GdhShapes.STORED_APART.has(loaded.get_class())
|
|
526
|
+
)
|
|
527
|
+
var set_in_file: Dictionary = (
|
|
528
|
+
_names_set_in(FileAccess.get_file_as_string(file), res_path.trim_prefix(file).trim_prefix("::"))
|
|
529
|
+
if from_file
|
|
530
|
+
else {}
|
|
531
|
+
)
|
|
519
532
|
# A Curve's points are read off one with none, since the engine has no default for point_0.
|
|
520
533
|
var fresh: Object = (
|
|
521
534
|
ClassDB.instantiate(loaded.get_class()) if GdhShapes.STORED_APART.has(loaded.get_class()) else null
|
|
@@ -537,6 +550,8 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
537
550
|
)
|
|
538
551
|
if GdhSame.same(value, default):
|
|
539
552
|
continue
|
|
553
|
+
if from_file and not set_in_file.has(property):
|
|
554
|
+
continue
|
|
540
555
|
properties[property] = serialiser.serialize_value(value)
|
|
541
556
|
var answer: Dictionary = {
|
|
542
557
|
"ok": true, "resourcePath": res_path, "resourceType": loaded.get_class(), "properties": properties
|
|
@@ -551,6 +566,26 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
551
566
|
return answer
|
|
552
567
|
|
|
553
568
|
|
|
569
|
+
## The names the [code][resource][/code] section of [param text], a .tres file, sets, or those of the
|
|
570
|
+
## embedded resource with the id [param id] when one is given, as a set.
|
|
571
|
+
static func _names_set_in(text: String, id: String) -> Dictionary:
|
|
572
|
+
var names: Dictionary = {}
|
|
573
|
+
var inside: bool = false
|
|
574
|
+
var key: RegEx = RegEx.create_from_string("^([^\\s=\\[][^=]*?) = ")
|
|
575
|
+
for line: String in text.split("\n"):
|
|
576
|
+
if line.begins_with("["):
|
|
577
|
+
inside = (
|
|
578
|
+
line.begins_with("[resource]")
|
|
579
|
+
if id.is_empty()
|
|
580
|
+
else line.begins_with("[sub_resource") and line.contains('id="%s"' % id)
|
|
581
|
+
)
|
|
582
|
+
continue
|
|
583
|
+
var found: RegExMatch = key.search(line) if inside else null
|
|
584
|
+
if found != null:
|
|
585
|
+
names[found.get_string(1)] = true
|
|
586
|
+
return names
|
|
587
|
+
|
|
588
|
+
|
|
554
589
|
## The resource [param file] keeps under [param id], found in [param value] or anything it holds.
|
|
555
590
|
## The engine loads such a path only while the file is cached, and a read loads the file uncached,
|
|
556
591
|
## so it is looked for among what the file holds. A resource of another file is neither taken nor
|
|
@@ -35,8 +35,24 @@ static func missing_reason(path: String) -> String:
|
|
|
35
35
|
|
|
36
36
|
## [param said], the refusal for [param path], a res:// path nothing is at, with the files of its
|
|
37
37
|
## extension whose names are nearest its own, or every one when there are few and none is near, so
|
|
38
|
-
## the caller has a path to send instead.
|
|
38
|
+
## the caller has a path to send instead. Files of the same name beside it come first, since a
|
|
39
|
+
## misspelt extension has no file of its own extension to be near.
|
|
39
40
|
func with_nearest_files(said: String, path: String) -> String:
|
|
41
|
+
# One the refusal names is the file it refuses, as a script given for a scene is. Word by word,
|
|
42
|
+
# since res://brain.gd is inside res://brain.gdd.
|
|
43
|
+
var named: Array[String] = []
|
|
44
|
+
for word: String in said.replace("\n", " ").replace(",", " ").replace(";", " ").split(" ", false):
|
|
45
|
+
named.append(word.rstrip(".:"))
|
|
46
|
+
var namesakes: Array[String] = []
|
|
47
|
+
for file: String in _namesakes(path):
|
|
48
|
+
if not file in named:
|
|
49
|
+
namesakes.append(file)
|
|
50
|
+
if not namesakes.is_empty():
|
|
51
|
+
var alone: bool = namesakes.size() == 1
|
|
52
|
+
return (
|
|
53
|
+
"%s. The %s of that name beside it %s %s."
|
|
54
|
+
% [said, "file" if alone else "files", "is" if alone else "are", ", ".join(namesakes)]
|
|
55
|
+
)
|
|
40
56
|
var extension: String = path.get_extension()
|
|
41
57
|
if extension.is_empty():
|
|
42
58
|
return said
|
|
@@ -65,6 +81,22 @@ func with_nearest_files(said: String, path: String) -> String:
|
|
|
65
81
|
return "%s. None of the project's %d .%s files is named like it." % [said, files.size(), extension]
|
|
66
82
|
|
|
67
83
|
|
|
84
|
+
# The files in [param path]'s folder named as it is with another extension, leaving out the import
|
|
85
|
+
# and uid files the engine keeps beside a file, which are named after the whole file name.
|
|
86
|
+
static func _namesakes(path: String) -> Array[String]:
|
|
87
|
+
var found: Array[String] = []
|
|
88
|
+
var folder: String = path.get_base_dir()
|
|
89
|
+
if not DirAccess.dir_exists_absolute(folder):
|
|
90
|
+
return found
|
|
91
|
+
var stem: String = path.get_file().get_basename()
|
|
92
|
+
var files: PackedStringArray = DirAccess.get_files_at(folder)
|
|
93
|
+
files.sort()
|
|
94
|
+
for file: String in files:
|
|
95
|
+
if file.get_basename() == stem and file != path.get_file():
|
|
96
|
+
found.append(folder.path_join(file))
|
|
97
|
+
return found
|
|
98
|
+
|
|
99
|
+
|
|
68
100
|
# A directory the engine steps over, marker file and all.
|
|
69
101
|
func is_stepped_over(path: String) -> bool:
|
|
70
102
|
return FileAccess.file_exists(path + ".gdignore")
|
|
@@ -505,6 +505,35 @@ static func setter_ran(object: Object, property: String, refused: String) -> Str
|
|
|
505
505
|
)
|
|
506
506
|
|
|
507
507
|
|
|
508
|
+
## Moves out of alsoChanged in [param answer] each of [param names], what a headless write put back
|
|
509
|
+
## as the doing of the script of [param owner], which is not a tool, to setByScript, with a note
|
|
510
|
+
## saying why the file does not hold it.
|
|
511
|
+
static func move_script_set(answer: Dictionary, names: Array, owner: String) -> void:
|
|
512
|
+
if not answer.has("alsoChanged"):
|
|
513
|
+
return
|
|
514
|
+
var changed: Dictionary = answer["alsoChanged"]
|
|
515
|
+
var by_script: Dictionary = {}
|
|
516
|
+
for key: Variant in names:
|
|
517
|
+
if changed.has(key):
|
|
518
|
+
by_script[key] = changed[key]
|
|
519
|
+
var _erased: bool = changed.erase(key)
|
|
520
|
+
if by_script.is_empty():
|
|
521
|
+
return
|
|
522
|
+
if changed.is_empty():
|
|
523
|
+
var _dropped: bool = answer.erase("alsoChanged")
|
|
524
|
+
answer["setByScript"] = by_script
|
|
525
|
+
var listed: Array = by_script.keys()
|
|
526
|
+
var pronoun: String = "it" if listed.size() == 1 else "them"
|
|
527
|
+
answer["setByScriptNote"] = (
|
|
528
|
+
(
|
|
529
|
+
"%s's script is not a tool, so it ran in the headless engine that wrote the file, and set %s "
|
|
530
|
+
+ "as it did. The file does not hold %s, as a save from the editor, where the script does not "
|
|
531
|
+
+ "run, would not; the script sets %s again when it runs in the game."
|
|
532
|
+
)
|
|
533
|
+
% [owner, ", ".join(listed), pronoun, pronoun]
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
|
|
508
537
|
## Why [param name] on [param what] cannot be saved as the file held it.
|
|
509
538
|
static func unasked(what: String, name: String, why: String = "") -> String:
|
|
510
539
|
return (
|
|
@@ -383,19 +383,16 @@ func _readable(value: Variant, relative_to: Node) -> Variant:
|
|
|
383
383
|
return _values.serialize_value(value)
|
|
384
384
|
|
|
385
385
|
|
|
386
|
-
## What [param target] stores, by name, but its script and the properties in [param leave]
|
|
387
|
-
##
|
|
386
|
+
## What [param target] stores, by name, but its script and the properties in [param leave], in the
|
|
387
|
+
## names a read answers with: a Curve's points as point_0/position and the rest rather than the _data
|
|
388
|
+
## list every point write changes. A list or a map is copied, since a setter can change the one it
|
|
389
|
+
## holds in place.
|
|
388
390
|
func _stored(target: Object, leave: Dictionary) -> Dictionary:
|
|
389
391
|
var held: Dictionary = {}
|
|
390
|
-
for entry: Dictionary in
|
|
392
|
+
for entry: Dictionary in GdhShapes.read_properties(target):
|
|
391
393
|
var name: String = str(entry["name"])
|
|
392
394
|
var usage: int = GdhRead.as_int(entry["usage"])
|
|
393
|
-
if (
|
|
394
|
-
usage & PROPERTY_USAGE_STORAGE == 0
|
|
395
|
-
or usage & HEADINGS != 0
|
|
396
|
-
or name == "script"
|
|
397
|
-
or leave.has(name)
|
|
398
|
-
):
|
|
395
|
+
if usage & HEADINGS != 0 or name == "script" or leave.has(name):
|
|
399
396
|
continue
|
|
400
397
|
var value: Variant = target.get(name)
|
|
401
398
|
if value is Array:
|
|
@@ -45,6 +45,17 @@ var root: Node = null
|
|
|
45
45
|
var path: String = ""
|
|
46
46
|
## What the last write saved, for its answer to read what the file now stores.
|
|
47
47
|
var written_pack: PackedScene = null
|
|
48
|
+
## The properties a headless write put back on each node the tool changed, by instance id, as a
|
|
49
|
+
## set: what the node's script, which is not a tool, changed as the tool wrote, and the file was
|
|
50
|
+
## not given.
|
|
51
|
+
var restored_by_write: Dictionary = {}
|
|
52
|
+
## What the scene loads with where it stores what the tool gave, by node path and then property:
|
|
53
|
+
## the node's script changed the value as the copy the write checks loaded.
|
|
54
|
+
var loads_as: Dictionary = {}
|
|
55
|
+
## The node paths in [member loads_as] whose script is not a tool, so an editor does not run it.
|
|
56
|
+
var loads_untooled: Dictionary = {}
|
|
57
|
+
## The pack a write's check reads, for [method stores] to answer from.
|
|
58
|
+
var _checked_pack: PackedScene = null
|
|
48
59
|
## The path of each node a scene this one inherits gives, with the file that makes it.
|
|
49
60
|
var _base_paths: Dictionary = {}
|
|
50
61
|
var _state: SceneState = null
|
|
@@ -564,6 +575,19 @@ func kept(saved: Node, loaded: Variant, given: Variant) -> bool:
|
|
|
564
575
|
return GdhSame.same(loaded, given)
|
|
565
576
|
|
|
566
577
|
|
|
578
|
+
## Whether the pack a write is checking stores [param given] for [param property] on the node at
|
|
579
|
+
## [param at], a path from the root.
|
|
580
|
+
func stores(at: String, property: String, given: Variant) -> bool:
|
|
581
|
+
if _checked_pack == null:
|
|
582
|
+
return false
|
|
583
|
+
var state: SceneState = _checked_pack.get_state()
|
|
584
|
+
for index: int in state.get_node_count():
|
|
585
|
+
if _relative(str(state.get_node_path(index))) == at:
|
|
586
|
+
var stored: Dictionary = _stored_at(state, index)
|
|
587
|
+
return stored.has(property) and GdhSame.same(stored[property], given)
|
|
588
|
+
return false
|
|
589
|
+
|
|
590
|
+
|
|
567
591
|
## Packs [member root], has [param check] read the result as the file will hold it, and writes the
|
|
568
592
|
## file only if the check answers "". Frees [member root] either way.
|
|
569
593
|
##
|
|
@@ -592,6 +616,7 @@ func write(check: Callable = Callable()) -> Dictionary:
|
|
|
592
616
|
var saved: Node = packed.instantiate(PackedScene.GEN_EDIT_STATE_MAIN)
|
|
593
617
|
if saved == null:
|
|
594
618
|
return refuse("The packed copy of %s did not instantiate, so nothing was written." % path)
|
|
619
|
+
_checked_pack = packed
|
|
595
620
|
var missing: String = str(check.call(saved))
|
|
596
621
|
saved.free()
|
|
597
622
|
if not missing.is_empty():
|
|
@@ -777,8 +802,10 @@ func _settled(packed: PackedScene) -> Array:
|
|
|
777
802
|
for found: Array in moved:
|
|
778
803
|
var node: Node = found[0]
|
|
779
804
|
var before: Dictionary = _before[node.get_instance_id()]
|
|
805
|
+
var restored: Dictionary = restored_by_write.get_or_add(node.get_instance_id(), {})
|
|
780
806
|
for name: String in found[1]:
|
|
781
807
|
node.set(name, before.get(name, GdhHeadlessCheck.default_of(node, name)))
|
|
808
|
+
restored[name] = true
|
|
782
809
|
current = PackedScene.new()
|
|
783
810
|
var packing: Error = current.pack(root)
|
|
784
811
|
if packing != OK:
|
|
@@ -29,7 +29,8 @@ func add_animation_state(args: Dictionary) -> Dictionary:
|
|
|
29
29
|
# are in use in every state machine.
|
|
30
30
|
if sm.has_node(StringName(state_name)):
|
|
31
31
|
return scene.refuse("The state machine already has a state named %s." % state_name)
|
|
32
|
-
var
|
|
32
|
+
var reached: Node = anim_tree.get_node_or_null(anim_tree.anim_player)
|
|
33
|
+
var player: AnimationPlayer = reached as AnimationPlayer
|
|
33
34
|
var found: bool = player != null and player.has_animation(StringName(animation_name))
|
|
34
35
|
|
|
35
36
|
var anim_node: AnimationNodeAnimation = AnimationNodeAnimation.new()
|
|
@@ -57,7 +58,9 @@ func add_animation_state(args: Dictionary) -> Dictionary:
|
|
|
57
58
|
}
|
|
58
59
|
# Written all the same, as the editor writes one: a state machine is often laid out before its
|
|
59
60
|
# animations are made, and refusing it stopped a tree being built in that order.
|
|
60
|
-
if player
|
|
61
|
+
if player == null:
|
|
62
|
+
answer["animationMissing"] = GdhAnimationReads.no_player(str(anim_tree.anim_player), reached)
|
|
63
|
+
elif not found:
|
|
61
64
|
answer["animationMissing"] = GdhAnimationReads.missing_animation(
|
|
62
65
|
player, scene.path_of(player), animation_name
|
|
63
66
|
)
|
|
@@ -259,9 +259,13 @@ func read_animation_states(args: Dictionary) -> Dictionary:
|
|
|
259
259
|
if opened[1] == null:
|
|
260
260
|
return opened[2]
|
|
261
261
|
var anim_tree: AnimationTree = opened[1]
|
|
262
|
-
var
|
|
262
|
+
var reached: Node = anim_tree.get_node_or_null(anim_tree.anim_player)
|
|
263
|
+
var player: AnimationPlayer = reached as AnimationPlayer
|
|
263
264
|
var player_path: String = "" if player == null else scene.path_of(player)
|
|
264
|
-
|
|
265
|
+
var unplayed: String = GdhAnimationReads.no_player(str(anim_tree.anim_player), reached)
|
|
266
|
+
return _read_in(
|
|
267
|
+
scene, GdhAnimationReads.states(anim_tree, state_machine_path, player, player_path, unplayed)
|
|
268
|
+
)
|
|
265
269
|
|
|
266
270
|
|
|
267
271
|
# =============================================================================
|
|
@@ -246,6 +246,7 @@ func create_resource(args: Dictionary) -> Dictionary:
|
|
|
246
246
|
var failed: String = _save_checked(resource, res_path, properties.keys(), null)
|
|
247
247
|
if not failed.is_empty():
|
|
248
248
|
return {"ok": false, "error": failed}
|
|
249
|
+
_note_put_back(answer, resource, res_path)
|
|
249
250
|
if not properties.is_empty():
|
|
250
251
|
answer["properties"] = _held(resource, properties.keys())
|
|
251
252
|
return answer
|
|
@@ -302,10 +303,24 @@ func modify_resource(args: Dictionary) -> Dictionary:
|
|
|
302
303
|
var failed: String = _save_checked(resource, res_path, properties.keys(), previous)
|
|
303
304
|
if not failed.is_empty():
|
|
304
305
|
return {"ok": false, "error": failed}
|
|
306
|
+
_note_put_back(answer, resource, res_path)
|
|
305
307
|
answer["properties"] = _held(resource, properties.keys())
|
|
306
308
|
return answer
|
|
307
309
|
|
|
308
310
|
|
|
311
|
+
## Moves to setByScript each property alsoChanged in [param answer] gave a value [param resource] no
|
|
312
|
+
## longer holds once saved at [param path]: the save put it back as its script's doing.
|
|
313
|
+
func _note_put_back(answer: Dictionary, resource: Resource, path: String) -> void:
|
|
314
|
+
var noted: Dictionary = answer.get("alsoChanged", {})
|
|
315
|
+
var restored: Array = []
|
|
316
|
+
for key: Variant in noted:
|
|
317
|
+
var now: Variant = _values.serialize_value(_properties.read_at(resource, str(key)))
|
|
318
|
+
var entry: Dictionary = noted[key]
|
|
319
|
+
if JSON.stringify(now) != JSON.stringify(entry.get("now")):
|
|
320
|
+
restored.append(key)
|
|
321
|
+
GdhHeadlessCheck.move_script_set(answer, restored, path)
|
|
322
|
+
|
|
323
|
+
|
|
309
324
|
## Why [param resource], saved at [param path], cannot hold what [param names] were given, or "": a
|
|
310
325
|
## resource cannot hold the file it is saved as. Godot saves the reference as null with only a
|
|
311
326
|
## warning in its log, and the read back after the save reported the property as one the file does
|
|
@@ -126,8 +126,13 @@ static func inherit(scene_path: String, base_path: String, root_name: String, sc
|
|
|
126
126
|
"rootNodeType": root_type,
|
|
127
127
|
"rootNodeName": name
|
|
128
128
|
}
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
var properties: Dictionary = base["properties"]
|
|
130
|
+
var carried: Variant = script if script != null else properties.get("script")
|
|
131
|
+
if carried is Script:
|
|
132
|
+
var carried_script: Script = carried
|
|
133
|
+
answer["script"] = carried_script.resource_path
|
|
134
|
+
if not str(carried_script.get_global_name()).is_empty():
|
|
135
|
+
answer["scriptClass"] = str(carried_script.get_global_name())
|
|
131
136
|
return answer
|
|
132
137
|
|
|
133
138
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends RefCounted
|
|
3
|
+
|
|
4
|
+
## Where else in a scene a resource built into it is held, since such a resource is one object
|
|
5
|
+
## wherever the scene uses it.
|
|
6
|
+
|
|
7
|
+
const GdhPropertyPaths = preload("../property_paths.gd")
|
|
8
|
+
const GdhRead = preload("../reading.gd")
|
|
9
|
+
const GdhSceneFile = preload("../scene_file.gd")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## Every other place in [param scene] holding a resource that [param key] was written through on
|
|
13
|
+
## [param node], as {node, property}. A copy made by duplicate shares its original's resources, so
|
|
14
|
+
## the write changed each of them too.
|
|
15
|
+
static func holders(scene: GdhSceneFile, node: Node, key: String) -> Array[Dictionary]:
|
|
16
|
+
var found_at: Array[Dictionary] = []
|
|
17
|
+
var through: Array = GdhPropertyPaths.holder_of(node, key, GdhPropertyPaths.home_of(node))[2]
|
|
18
|
+
if through.is_empty():
|
|
19
|
+
return found_at
|
|
20
|
+
var wanted: Dictionary = {}
|
|
21
|
+
for reached: Resource in through:
|
|
22
|
+
wanted[reached.get_instance_id()] = true
|
|
23
|
+
var nodes: Array[Node] = [scene.root]
|
|
24
|
+
nodes.append_array(scene.root.find_children("*", "", true, false))
|
|
25
|
+
for at: Node in nodes:
|
|
26
|
+
for declared: Dictionary in at.get_property_list():
|
|
27
|
+
var property: String = str(declared.get("name", ""))
|
|
28
|
+
if property.is_empty() or not (GdhRead.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
29
|
+
continue
|
|
30
|
+
for found: String in _paths_to(at.get(property), property, wanted, {}):
|
|
31
|
+
if at != node or not key.begins_with(found + "/"):
|
|
32
|
+
found_at.append({"node": scene.path_of(at), "property": found})
|
|
33
|
+
return found_at
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## The paths below [param path] at which [param value] holds a resource of [param wanted], by
|
|
37
|
+
## instance id, stepping into lists and into resources built into a scene.
|
|
38
|
+
static func _paths_to(value: Variant, path: String, wanted: Dictionary, seen: Dictionary) -> Array[String]:
|
|
39
|
+
var paths: Array[String] = []
|
|
40
|
+
if value is Array:
|
|
41
|
+
var items: Array = value
|
|
42
|
+
for index: int in items.size():
|
|
43
|
+
paths.append_array(_paths_to(items[index], "%s/%d" % [path, index], wanted, seen))
|
|
44
|
+
return paths
|
|
45
|
+
if not value is Resource:
|
|
46
|
+
return paths
|
|
47
|
+
var held: Resource = value
|
|
48
|
+
var id: int = held.get_instance_id()
|
|
49
|
+
if wanted.has(id):
|
|
50
|
+
paths.append(path)
|
|
51
|
+
var built_in: bool = held.resource_path.is_empty() or held.resource_path.contains("::")
|
|
52
|
+
if seen.has(id) or not built_in:
|
|
53
|
+
return paths
|
|
54
|
+
seen[id] = true
|
|
55
|
+
for declared: Dictionary in held.get_property_list():
|
|
56
|
+
var property: String = str(declared.get("name", ""))
|
|
57
|
+
if not property.is_empty() and GdhRead.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
58
|
+
paths.append_array(_paths_to(held.get(property), path + "/" + property, wanted, seen))
|
|
59
|
+
return paths
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://blndo1urj5l2w
|
|
@@ -15,6 +15,7 @@ const GdhPlacing = preload("scene_placing.gd")
|
|
|
15
15
|
const GdhPropertyValues = preload("../property_values.gd")
|
|
16
16
|
const GdhSame = preload("../same_values.gd")
|
|
17
17
|
const GdhSceneFile = preload("../scene_file.gd")
|
|
18
|
+
const GdhSceneSharing = preload("scene_sharing.gd")
|
|
18
19
|
const GdhSerialisation = preload("../serialisation.gd")
|
|
19
20
|
const GdhTilesetTools = preload("tileset_tools.gd")
|
|
20
21
|
|
|
@@ -43,7 +44,16 @@ func _kept(scene: GdhSceneFile, saved: Node, node: Node, names: Array) -> String
|
|
|
43
44
|
for key: Variant in names:
|
|
44
45
|
var property: String = str(key)
|
|
45
46
|
var loaded: Variant = _properties.read_at(copy, property)
|
|
46
|
-
|
|
47
|
+
var given: Variant = _properties.read_at(node, property)
|
|
48
|
+
if not scene.kept(saved, loaded, given):
|
|
49
|
+
# The file holds it, and the copy's own script set it again as the copy loaded, as it will
|
|
50
|
+
# wherever the scene loads; an editor saves the same.
|
|
51
|
+
if scene.stores(at, property, given):
|
|
52
|
+
var on: Dictionary = scene.loads_as.get_or_add(at, {})
|
|
53
|
+
if GdhHeadlessCheck.untooled(copy.get_script()):
|
|
54
|
+
scene.loads_untooled[at] = true
|
|
55
|
+
on[property] = _values.serialize_value(loaded)
|
|
56
|
+
continue
|
|
47
57
|
return (
|
|
48
58
|
(
|
|
49
59
|
"%s.%s would load from the file as %s: the scene does not keep it, because the property "
|
|
@@ -142,7 +152,6 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
|
|
|
142
152
|
|
|
143
153
|
if scene_path.is_empty():
|
|
144
154
|
return {"ok": false, "error": "Missing scenePath"}
|
|
145
|
-
var asked_type: String = root_node_type
|
|
146
155
|
var made: Dictionary = GdhSceneFile.made_as(
|
|
147
156
|
root_node_type,
|
|
148
157
|
"" if script_path.is_empty() else GdhSceneFile.res_path(project_path, script_path),
|
|
@@ -200,13 +209,20 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
|
|
|
200
209
|
if not folder.is_empty():
|
|
201
210
|
root.free()
|
|
202
211
|
return {"ok": false, "error": folder}
|
|
212
|
+
# Read before the write, which frees the root.
|
|
213
|
+
var root_class: Dictionary = GdhSceneFile.class_of(root)
|
|
203
214
|
var written: Dictionary = scene.write()
|
|
204
215
|
if not written.is_empty():
|
|
205
216
|
return written
|
|
206
217
|
|
|
207
218
|
var answer: Dictionary = {
|
|
208
|
-
"ok": true,
|
|
219
|
+
"ok": true,
|
|
220
|
+
"scenePath": scene_path,
|
|
221
|
+
"rootNodeType": root_class["nodeType"],
|
|
222
|
+
"rootNodeName": root_node_name
|
|
209
223
|
}
|
|
224
|
+
if root_class.has("scriptClass"):
|
|
225
|
+
answer["scriptClass"] = root_class["scriptClass"]
|
|
210
226
|
if not script_path.is_empty():
|
|
211
227
|
answer["script"] = script_path
|
|
212
228
|
if not had_folder:
|
|
@@ -241,7 +257,6 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
241
257
|
|
|
242
258
|
if node_name.is_empty():
|
|
243
259
|
return {"ok": false, "error": "Missing nodeName"}
|
|
244
|
-
var asked_type: String = node_type
|
|
245
260
|
var class_script: Script = null
|
|
246
261
|
if not node_type.is_empty() and not ClassDB.class_exists(node_type):
|
|
247
262
|
var declared: Dictionary = GdhSceneFile.script_class(node_type, "Node")
|
|
@@ -301,13 +316,9 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
301
316
|
|
|
302
317
|
var new_path: String = scene.path_of(new_node)
|
|
303
318
|
# Read before the write, which frees the scene it was added to.
|
|
304
|
-
var answer: Dictionary = {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
"nodeType": asked_type if class_script != null else new_node.get_class(),
|
|
308
|
-
"nodePath": new_path,
|
|
309
|
-
"index": new_node.get_index(),
|
|
310
|
-
}
|
|
319
|
+
var answer: Dictionary = {"ok": true, "nodeName": node_name}
|
|
320
|
+
answer.merge(GdhSceneFile.class_of(new_node))
|
|
321
|
+
answer.merge({"nodePath": new_path, "index": new_node.get_index()})
|
|
311
322
|
if not properties.is_empty():
|
|
312
323
|
var held: Dictionary = {}
|
|
313
324
|
for key: Variant in properties:
|
|
@@ -316,6 +327,7 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
316
327
|
_properties.note_write(answer, new_node)
|
|
317
328
|
if not adding.is_empty():
|
|
318
329
|
answer.merge(GdhGroups.read_added(new_node, adding))
|
|
330
|
+
var id: int = new_node.get_instance_id()
|
|
319
331
|
var written: Dictionary = scene.write(
|
|
320
332
|
func(saved: Node) -> String:
|
|
321
333
|
var unkept: String = _kept(
|
|
@@ -328,6 +340,8 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
328
340
|
if not written.is_empty():
|
|
329
341
|
return written
|
|
330
342
|
|
|
343
|
+
_note_put_back(answer, scene, id, new_path)
|
|
344
|
+
_note_loads_as(answer, scene, new_path)
|
|
331
345
|
if packed != null:
|
|
332
346
|
answer["instanceOf"] = instance_path
|
|
333
347
|
return answer
|
|
@@ -518,6 +532,7 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
518
532
|
for property: String in GdhPlacing.placing(node):
|
|
519
533
|
drawn_at[property] = _properties.read_at(node, property)
|
|
520
534
|
var held_at: Transform3D = (node as Node3D).transform if node is Node3D else Transform3D.IDENTITY
|
|
535
|
+
var stored_before: Dictionary = GdhHeadlessCheck.stored(node)
|
|
521
536
|
# Unowned while it moves: a node taken out of the tree keeps its owner, and adding it under the new
|
|
522
537
|
# parent warned that the owner was not above it. Node.reparent warns the same way.
|
|
523
538
|
node.owner = null
|
|
@@ -527,6 +542,11 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
527
542
|
GdhPlacing.own(node, root)
|
|
528
543
|
if GdhRead.as_bool(args.get("keepGlobalTransform", true)):
|
|
529
544
|
GdhPlacing.keep_in_place(node, old_parent)
|
|
545
|
+
# Unasked, a headless write takes the move on a node whose script is not a tool for the
|
|
546
|
+
# script's doing and puts the node back as the file held it.
|
|
547
|
+
var placed: Array[String] = GdhHeadlessCheck.differing(node, stored_before, {})
|
|
548
|
+
if not placed.is_empty():
|
|
549
|
+
scene.asked(node, placed)
|
|
530
550
|
var also_changed: Dictionary = {}
|
|
531
551
|
for property: String in drawn_at:
|
|
532
552
|
var now: Variant = _properties.read_at(node, property)
|
|
@@ -643,6 +663,13 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
643
663
|
var new_path: String = scene.path_of(node)
|
|
644
664
|
var answer: Dictionary = {"ok": true, "nodePath": new_path, "properties": held}
|
|
645
665
|
_properties.note_write(answer, node)
|
|
666
|
+
var shared: Dictionary = {}
|
|
667
|
+
for key: Variant in properties:
|
|
668
|
+
var holders: Array[Dictionary] = GdhSceneSharing.holders(scene, node, str(key))
|
|
669
|
+
if not holders.is_empty():
|
|
670
|
+
shared[str(key)] = holders
|
|
671
|
+
if not shared.is_empty():
|
|
672
|
+
answer["sharedWith"] = shared
|
|
646
673
|
if not adding.is_empty() or not removing.is_empty():
|
|
647
674
|
answer["groups"] = GdhGroups.read(scene, node).get("groups", [])
|
|
648
675
|
var named: Array[String] = []
|
|
@@ -653,6 +680,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
653
680
|
var source: String = scene.values_from(node, old_path)
|
|
654
681
|
var carried: GdhPaths = GdhPaths.new()
|
|
655
682
|
var _rewritten: int = carried.carry(scene, {old_path: new_path} if new_path != old_path else {})
|
|
683
|
+
var id: int = node.get_instance_id()
|
|
656
684
|
var written: Dictionary = scene.write(
|
|
657
685
|
func(saved: Node) -> String:
|
|
658
686
|
var unkept: String = _kept(scene, saved, node, properties.keys())
|
|
@@ -674,12 +702,48 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
674
702
|
if not dropped.is_empty():
|
|
675
703
|
answer["droppedValues"] = dropped["values"]
|
|
676
704
|
answer["droppedNodes"] = dropped["nodes"]
|
|
705
|
+
_note_put_back(answer, scene, id, new_path)
|
|
706
|
+
_note_loads_as(answer, scene, new_path)
|
|
677
707
|
_note_left_out(answer, scene.written_pack, new_path, named, source)
|
|
678
708
|
if new_path != old_path:
|
|
679
709
|
answer.merge(GdhKin.follow(scene_path, old_path, new_path, carried))
|
|
680
710
|
return answer
|
|
681
711
|
|
|
682
712
|
|
|
713
|
+
## Puts under loadsAs in [param answer] each value given that the file holds and the script of the
|
|
714
|
+
## node at [param at] changes as the scene loads, as it loads.
|
|
715
|
+
static func _note_loads_as(answer: Dictionary, scene: GdhSceneFile, at: String) -> void:
|
|
716
|
+
var on: Dictionary = scene.loads_as.get(at, {})
|
|
717
|
+
if on.is_empty():
|
|
718
|
+
return
|
|
719
|
+
answer["loadsAs"] = on
|
|
720
|
+
var names: Array = on.keys()
|
|
721
|
+
var pronoun: String = "it" if names.size() == 1 else "them"
|
|
722
|
+
if scene.loads_untooled.has(at):
|
|
723
|
+
answer["loadsAsNote"] = (
|
|
724
|
+
(
|
|
725
|
+
"The file holds %s as given, which the editor shows. %s's script is not a tool, so in the "
|
|
726
|
+
+ "game it runs as the scene loads and changes %s to what loadsAs gives."
|
|
727
|
+
)
|
|
728
|
+
% [", ".join(names), at, pronoun]
|
|
729
|
+
)
|
|
730
|
+
else:
|
|
731
|
+
answer["loadsAsNote"] = (
|
|
732
|
+
(
|
|
733
|
+
"The file holds %s as given. %s's script runs as the scene loads, in the editor and in "
|
|
734
|
+
+ "the game, and changes %s to what loadsAs gives."
|
|
735
|
+
)
|
|
736
|
+
% [", ".join(names), at, pronoun]
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
## Moves out of alsoChanged in [param answer] what the last write of [param scene] put back on the
|
|
741
|
+
## node with instance id [param id], at [param at], to setByScript.
|
|
742
|
+
static func _note_put_back(answer: Dictionary, scene: GdhSceneFile, id: int, at: String) -> void:
|
|
743
|
+
var restored: Dictionary = scene.restored_by_write.get(id, {})
|
|
744
|
+
GdhHeadlessCheck.move_script_set(answer, restored.keys(), at)
|
|
745
|
+
|
|
746
|
+
|
|
683
747
|
## Notes under notStored each of [param named] the file saved from [param pack] does not set on the
|
|
684
748
|
## node at [param at]: a set gave it the value the node has without it, so the packer leaves it out,
|
|
685
749
|
## as the editor does, and it follows [param source], or its default, from then on.
|