gdharness 1.3.7 → 1.3.9
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 +999 -864
- package/build/godot/addons/gdharness_editor/file_reads.gd +13 -0
- package/build/godot/addons/gdharness_editor/reading.gd +61 -5
- package/build/godot/addons/gdharness_editor/scene_file.gd +41 -35
- package/build/godot/addons/gdharness_editor/serialisation.gd +8 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -2
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +244 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +105 -157
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +25 -1
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -1
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +72 -3
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +7 -1
- package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
- package/build/godot/addons/gdharness_runtime/reading.gd +61 -5
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +6 -1
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +74 -54
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +38 -6
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +2 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +26 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +105 -6
- package/build/godot/addons/gdharness_runtime/serialisation.gd +8 -7
- package/build/godot/operations/dependencies.gd +61 -10
- package/build/godot/operations/editor_tools.gd +8 -0
- package/build/godot/operations/file_reads.gd +13 -0
- package/build/godot/operations/gdscript_analysis.gd +76 -6
- package/build/godot/operations/gdscript_authoring.gd +5 -2
- package/build/godot/operations/godot_operations.gd +4 -0
- package/build/godot/operations/import_pipeline.gd +45 -1
- package/build/godot/operations/input_actions.gd +22 -7
- package/build/godot/operations/project_config.gd +31 -0
- package/build/godot/operations/project_diagnostics.gd +6 -5
- package/build/godot/operations/reading.gd +61 -5
- package/build/godot/operations/serialisation.gd +8 -7
- package/build/keeper.js +44 -13
- package/package.json +1 -1
|
@@ -11,9 +11,6 @@ const GdhRead = preload("../reading.gd")
|
|
|
11
11
|
const GdhSceneFile = preload("../scene_file.gd")
|
|
12
12
|
const GdhSerialisation = preload("../serialisation.gd")
|
|
13
13
|
|
|
14
|
-
## How far right of the rightmost state a new one goes in the editor's graph, wider than a state's
|
|
15
|
-
## box there.
|
|
16
|
-
const STATE_SPACING: float = 200.0
|
|
17
14
|
const LOOP_MODES: Dictionary = {
|
|
18
15
|
"none": Animation.LOOP_NONE, "linear": Animation.LOOP_LINEAR, "pingpong": Animation.LOOP_PINGPONG
|
|
19
16
|
}
|
|
@@ -335,6 +332,10 @@ func add_animation_track(args: Dictionary) -> Dictionary:
|
|
|
335
332
|
var track: Dictionary = given_track if given_track is Dictionary else {}
|
|
336
333
|
if track.is_empty():
|
|
337
334
|
return {"ok": false, "error": "Missing track"}
|
|
335
|
+
# The schema leaves keyframes out of what track requires, since remove_track finds a track without
|
|
336
|
+
# them.
|
|
337
|
+
if not track.has("keyframes"):
|
|
338
|
+
return {"ok": false, "error": "Missing track.keyframes"}
|
|
338
339
|
var keyed: Array = _keyframes(track)
|
|
339
340
|
if not keyed[0]:
|
|
340
341
|
return {"ok": false, "error": keyed[1]}
|
|
@@ -439,7 +440,15 @@ func _add_track(
|
|
|
439
440
|
method_found = target.has_method(method_name)
|
|
440
441
|
kind = Animation.TYPE_METHOD
|
|
441
442
|
else:
|
|
442
|
-
return [
|
|
443
|
+
return [
|
|
444
|
+
-1,
|
|
445
|
+
(
|
|
446
|
+
"add_track makes property and method tracks, and track.type is %s; remove_track takes any."
|
|
447
|
+
% track_type
|
|
448
|
+
),
|
|
449
|
+
null,
|
|
450
|
+
false
|
|
451
|
+
]
|
|
443
452
|
|
|
444
453
|
var track_idx: int = anim.find_track(path, kind)
|
|
445
454
|
var existing: bool = track_idx >= 0
|
|
@@ -472,6 +481,98 @@ func _add_track(
|
|
|
472
481
|
return [track_idx, "", method_found, existing]
|
|
473
482
|
|
|
474
483
|
|
|
484
|
+
## Takes out the track [code]track[/code] names by its type, nodePath and property as read answers
|
|
485
|
+
## them, so a track read back can be passed as it stands; its keyframes are not looked at. Every
|
|
486
|
+
## track of that shape goes, since the editor can make two for one path and both play.
|
|
487
|
+
func remove_animation_track(args: Dictionary) -> Dictionary:
|
|
488
|
+
var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
|
|
489
|
+
var player_node_path: String = str(args.get("playerNodePath", "."))
|
|
490
|
+
var animation_name: String = str(args.get("animationName", ""))
|
|
491
|
+
var given_track: Variant = args.get("track", {})
|
|
492
|
+
if scene_path.strip_edges() == "res://":
|
|
493
|
+
return {"ok": false, "error": "Missing scenePath"}
|
|
494
|
+
if animation_name.strip_edges().is_empty():
|
|
495
|
+
return {"ok": false, "error": "Missing animationName"}
|
|
496
|
+
var track: Dictionary = given_track if given_track is Dictionary else {}
|
|
497
|
+
if track.is_empty():
|
|
498
|
+
return {"ok": false, "error": "Missing track"}
|
|
499
|
+
var track_type: String = str(track.get("type", ""))
|
|
500
|
+
if not track_type in GdhAnimationReads.TRACK_TYPE_NAMES.values():
|
|
501
|
+
return {"ok": false, "error": "Unsupported track.type: " + track_type}
|
|
502
|
+
var node_path: String = str(track.get("nodePath", ""))
|
|
503
|
+
var property: String = str(track.get("property", ""))
|
|
504
|
+
var shape: Array = [track_type, node_path, property]
|
|
505
|
+
|
|
506
|
+
var found: Array = _animation_at(scene_path, player_node_path, animation_name)
|
|
507
|
+
if found[1] == null:
|
|
508
|
+
return found[2]
|
|
509
|
+
var scene: GdhSceneFile = found[0]
|
|
510
|
+
var player: AnimationPlayer = found[1]
|
|
511
|
+
var anim: Animation = player.get_animation(StringName(animation_name))
|
|
512
|
+
var homes: Array = _homes(
|
|
513
|
+
scene, [[_library_of(player, animation_name), "animation library"], [anim, "animation"]]
|
|
514
|
+
)
|
|
515
|
+
if not homes[0]:
|
|
516
|
+
return scene.refuse(str(homes[1]))
|
|
517
|
+
var matching: Array[int] = []
|
|
518
|
+
var held: PackedStringArray = []
|
|
519
|
+
for index: int in anim.get_track_count():
|
|
520
|
+
var of: Array = _track_shape(anim, index)
|
|
521
|
+
if of == shape:
|
|
522
|
+
matching.append(index)
|
|
523
|
+
var _listed: bool = held.append("%s (%s)" % [str(anim.track_get_path(index)), of[0]])
|
|
524
|
+
if matching.is_empty():
|
|
525
|
+
var said: String = (
|
|
526
|
+
"%s has no %s track on %s"
|
|
527
|
+
% [
|
|
528
|
+
animation_name,
|
|
529
|
+
track_type,
|
|
530
|
+
node_path if property.is_empty() else "%s:%s" % [node_path, property]
|
|
531
|
+
]
|
|
532
|
+
)
|
|
533
|
+
if held.is_empty():
|
|
534
|
+
return scene.refuse("%s; it has no tracks." % said)
|
|
535
|
+
return scene.refuse("%s; its tracks are %s." % [said, ", ".join(held)])
|
|
536
|
+
var keys: int = 0
|
|
537
|
+
matching.reverse()
|
|
538
|
+
for index: int in matching:
|
|
539
|
+
keys += anim.track_get_key_count(index)
|
|
540
|
+
anim.remove_track(index)
|
|
541
|
+
|
|
542
|
+
var at: String = scene.path_of(player)
|
|
543
|
+
var left: int = anim.get_track_count()
|
|
544
|
+
var check: Callable = func(saved: Node) -> String:
|
|
545
|
+
var copy: AnimationPlayer = saved.get_node_or_null(at) as AnimationPlayer
|
|
546
|
+
if copy == null or not copy.has_animation(StringName(animation_name)):
|
|
547
|
+
return "%s would not be in the scene when it loads." % animation_name
|
|
548
|
+
if copy.get_animation(StringName(animation_name)).get_track_count() != left:
|
|
549
|
+
return "the track would still be in %s when the scene loads." % animation_name
|
|
550
|
+
return ""
|
|
551
|
+
var removed: Dictionary = {"type": track_type, "nodePath": node_path}
|
|
552
|
+
if not property.is_empty():
|
|
553
|
+
removed["property"] = property
|
|
554
|
+
var answer: Dictionary = {
|
|
555
|
+
"ok": true,
|
|
556
|
+
"animationName": animation_name,
|
|
557
|
+
"removed": removed,
|
|
558
|
+
"tracksRemoved": matching.size(),
|
|
559
|
+
"keysRemoved": keys,
|
|
560
|
+
"tracks": left,
|
|
561
|
+
}
|
|
562
|
+
var files: Dictionary = homes[1]
|
|
563
|
+
return _finish(scene, check, files, answer)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
## Track [param index] of [param anim] as [type, nodePath, property], named as read names them.
|
|
567
|
+
static func _track_shape(anim: Animation, index: int) -> Array:
|
|
568
|
+
var path: NodePath = anim.track_get_path(index)
|
|
569
|
+
return [
|
|
570
|
+
GdhAnimationReads.TRACK_TYPE_NAMES.get(anim.track_get_type(index), str(anim.track_get_type(index))),
|
|
571
|
+
str(path.get_concatenated_names()),
|
|
572
|
+
str(path.get_concatenated_subnames()),
|
|
573
|
+
]
|
|
574
|
+
|
|
575
|
+
|
|
475
576
|
## The keys of [param anim], called [param animation_name], that lie past its end, where they never
|
|
476
577
|
## play, as a sentence naming each by its track's path and time, or "" when none does. The engine
|
|
477
578
|
## keeps such a key, as the editor does, so a longer length plays it again.
|
|
@@ -826,156 +927,3 @@ static func _collect_playing(
|
|
|
826
927
|
for step: String in children:
|
|
827
928
|
var child: AnimationNode = children[step]
|
|
828
929
|
_collect_playing(child, step if at.is_empty() else at + "/" + step, tree_path, animation_name, found)
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
# =============================================================================
|
|
832
|
-
# add_animation_state
|
|
833
|
-
# =============================================================================
|
|
834
|
-
func add_animation_state(args: Dictionary) -> Dictionary:
|
|
835
|
-
var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
|
|
836
|
-
var anim_tree_path: String = str(args.get("animTreePath", ""))
|
|
837
|
-
var state_name: String = str(args.get("stateName", ""))
|
|
838
|
-
var animation_name: String = str(args.get("animationName", ""))
|
|
839
|
-
var state_machine_path: String = str(args.get("stateMachinePath", ""))
|
|
840
|
-
|
|
841
|
-
if scene_path.strip_edges() == "res://":
|
|
842
|
-
return {"ok": false, "error": "Missing scenePath"}
|
|
843
|
-
for required: Array in [
|
|
844
|
-
[anim_tree_path, "animTreePath"], [state_name, "stateName"], [animation_name, "animationName"]
|
|
845
|
-
]:
|
|
846
|
-
if str(required[0]).is_empty():
|
|
847
|
-
return {"ok": false, "error": "Missing " + str(required[1])}
|
|
848
|
-
|
|
849
|
-
var opened: Array = _open_at(scene_path, anim_tree_path, "AnimationTree")
|
|
850
|
-
var scene: GdhSceneFile = opened[0]
|
|
851
|
-
if opened[1] == null:
|
|
852
|
-
return opened[2]
|
|
853
|
-
var anim_tree: AnimationTree = opened[1]
|
|
854
|
-
var machine: Array = _machine(scene, anim_tree, state_machine_path)
|
|
855
|
-
if machine[0] == null:
|
|
856
|
-
return scene.refuse(str(machine[2]))
|
|
857
|
-
var sm: AnimationNodeStateMachine = machine[0]
|
|
858
|
-
# add_node fails with nothing but a line in the editor's log on a name in use, and Start and End
|
|
859
|
-
# are in use in every state machine.
|
|
860
|
-
if sm.has_node(StringName(state_name)):
|
|
861
|
-
return scene.refuse("The state machine already has a state named %s." % state_name)
|
|
862
|
-
var player: AnimationPlayer = anim_tree.get_node_or_null(anim_tree.anim_player) as AnimationPlayer
|
|
863
|
-
var found: bool = player != null and player.has_animation(StringName(animation_name))
|
|
864
|
-
|
|
865
|
-
var anim_node: AnimationNodeAnimation = AnimationNodeAnimation.new()
|
|
866
|
-
anim_node.animation = StringName(animation_name)
|
|
867
|
-
# Beside the rightmost state: add_node puts a state at the origin, where the editor's graph
|
|
868
|
-
# draws it over whatever is already there, Start included.
|
|
869
|
-
var rightmost: Vector2 = Vector2.ZERO
|
|
870
|
-
for state: StringName in sm.get_node_list():
|
|
871
|
-
var held: Vector2 = sm.get_node_position(state)
|
|
872
|
-
if held.x > rightmost.x:
|
|
873
|
-
rightmost = held
|
|
874
|
-
var position: Vector2 = rightmost + Vector2(STATE_SPACING, 0)
|
|
875
|
-
sm.add_node(StringName(state_name), anim_node, position)
|
|
876
|
-
if not sm.has_node(StringName(state_name)):
|
|
877
|
-
return scene.refuse("The state machine did not take the state %s." % state_name)
|
|
878
|
-
|
|
879
|
-
var answer: Dictionary = {
|
|
880
|
-
"ok": true,
|
|
881
|
-
"stateName": state_name,
|
|
882
|
-
"animationName": animation_name,
|
|
883
|
-
# False when the tree has no player to look the animation up in, or its player has none of
|
|
884
|
-
# that name.
|
|
885
|
-
"animationFound": found,
|
|
886
|
-
"position": GdhSerialisation.new().serialize_value(position),
|
|
887
|
-
}
|
|
888
|
-
# Written all the same, as the editor writes one: a state machine is often laid out before its
|
|
889
|
-
# animations are made, and refusing it stopped a tree being built in that order.
|
|
890
|
-
if player != null and not found:
|
|
891
|
-
answer["animationMissing"] = _missing_animation(scene, player, animation_name)
|
|
892
|
-
var files: Dictionary = machine[1]
|
|
893
|
-
return _finish(scene, Callable(), files, answer)
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
## Why the state playing [param animation_name] plays nothing yet. The player is named by its path
|
|
897
|
-
## from the scene root, as playerNodePath takes it; anim_player is relative to the tree ("../Anim"),
|
|
898
|
-
## which no argument takes.
|
|
899
|
-
static func _missing_animation(
|
|
900
|
-
scene: GdhSceneFile, player: AnimationPlayer, animation_name: String
|
|
901
|
-
) -> String:
|
|
902
|
-
var said: String = (
|
|
903
|
-
"The AnimationPlayer at %s has no animation %s yet, so the state plays nothing until it has one."
|
|
904
|
-
% [str(scene.root.get_path_to(player)), animation_name]
|
|
905
|
-
)
|
|
906
|
-
var close: PackedStringArray = GdhRead.nearest(animation_name, player.get_animation_list())
|
|
907
|
-
if close.is_empty():
|
|
908
|
-
return said
|
|
909
|
-
return (
|
|
910
|
-
said + " The nearest %s %s." % ["it has is" if close.size() == 1 else "it has are", ", ".join(close)]
|
|
911
|
-
)
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
# =============================================================================
|
|
915
|
-
# connect_animation_states
|
|
916
|
-
# =============================================================================
|
|
917
|
-
func connect_animation_states(args: Dictionary) -> Dictionary:
|
|
918
|
-
var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
|
|
919
|
-
var anim_tree_path: String = str(args.get("animTreePath", ""))
|
|
920
|
-
var from_state: String = str(args.get("fromState", ""))
|
|
921
|
-
var to_state: String = str(args.get("toState", ""))
|
|
922
|
-
var transition_type: String = str(args.get("transitionType", "immediate"))
|
|
923
|
-
var state_machine_path: String = str(args.get("stateMachinePath", ""))
|
|
924
|
-
var advance_condition: String = str(args.get("advanceCondition", ""))
|
|
925
|
-
|
|
926
|
-
if scene_path.strip_edges() == "res://":
|
|
927
|
-
return {"ok": false, "error": "Missing scenePath"}
|
|
928
|
-
if anim_tree_path.is_empty():
|
|
929
|
-
return {"ok": false, "error": "Missing animTreePath"}
|
|
930
|
-
if from_state.is_empty() or to_state.is_empty():
|
|
931
|
-
return {"ok": false, "error": "Missing fromState or toState"}
|
|
932
|
-
var modes: Dictionary = {
|
|
933
|
-
"sync": AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC,
|
|
934
|
-
"at_end": AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END,
|
|
935
|
-
"immediate": AnimationNodeStateMachineTransition.SWITCH_MODE_IMMEDIATE,
|
|
936
|
-
}
|
|
937
|
-
if not modes.has(transition_type):
|
|
938
|
-
return {"ok": false, "error": "Unsupported transitionType: " + transition_type}
|
|
939
|
-
var transition: AnimationNodeStateMachineTransition = AnimationNodeStateMachineTransition.new()
|
|
940
|
-
transition.switch_mode = (
|
|
941
|
-
GdhRead.as_int(modes[transition_type]) as AnimationNodeStateMachineTransition.SwitchMode
|
|
942
|
-
)
|
|
943
|
-
if not advance_condition.is_empty():
|
|
944
|
-
transition.advance_condition = StringName(advance_condition)
|
|
945
|
-
|
|
946
|
-
var opened: Array = _open_at(scene_path, anim_tree_path, "AnimationTree")
|
|
947
|
-
var scene: GdhSceneFile = opened[0]
|
|
948
|
-
if opened[1] == null:
|
|
949
|
-
return opened[2]
|
|
950
|
-
var anim_tree: AnimationTree = opened[1]
|
|
951
|
-
var machine: Array = _machine(scene, anim_tree, state_machine_path)
|
|
952
|
-
if machine[0] == null:
|
|
953
|
-
return scene.refuse(str(machine[2]))
|
|
954
|
-
var sm: AnimationNodeStateMachine = machine[0]
|
|
955
|
-
var problem: String = _transition_problem(sm, from_state, to_state)
|
|
956
|
-
if not problem.is_empty():
|
|
957
|
-
return scene.refuse(problem)
|
|
958
|
-
|
|
959
|
-
sm.add_transition(StringName(from_state), StringName(to_state), transition)
|
|
960
|
-
if not sm.has_transition(StringName(from_state), StringName(to_state)):
|
|
961
|
-
return scene.refuse("The state machine did not take the transition.")
|
|
962
|
-
var files: Dictionary = machine[1]
|
|
963
|
-
return _finish(scene, Callable(), files, {"ok": true, "from": from_state, "to": to_state})
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
## Why [param sm] cannot take a transition from [param from_state] to [param to_state], or "".
|
|
967
|
-
## Each is a condition add_transition fails on with only a line in the editor's log, and the
|
|
968
|
-
## transition was answered as made.
|
|
969
|
-
func _transition_problem(sm: AnimationNodeStateMachine, from_state: String, to_state: String) -> String:
|
|
970
|
-
for state: String in [from_state, to_state]:
|
|
971
|
-
if not sm.has_node(StringName(state)):
|
|
972
|
-
return "The state machine has no state named %s." % state
|
|
973
|
-
if from_state == to_state:
|
|
974
|
-
return "A transition needs two different states."
|
|
975
|
-
if from_state == "End":
|
|
976
|
-
return "No transition can leave End."
|
|
977
|
-
if to_state == "Start":
|
|
978
|
-
return "No transition can arrive at Start."
|
|
979
|
-
if sm.has_transition(StringName(from_state), StringName(to_state)):
|
|
980
|
-
return "There is already a transition from %s to %s." % [from_state, to_state]
|
|
981
|
-
return ""
|
|
@@ -9,6 +9,7 @@ const GdhPropertyValues = preload("../property_values.gd")
|
|
|
9
9
|
const GdhRead = preload("../reading.gd")
|
|
10
10
|
const GdhSame = preload("../same_values.gd")
|
|
11
11
|
const GdhSceneFile = preload("../scene_file.gd")
|
|
12
|
+
const GdhSerialisation = preload("../serialisation.gd")
|
|
12
13
|
|
|
13
14
|
# The minimal shader of a type: its declaration and the one function the engine calls for it.
|
|
14
15
|
const SHADER_EMPTY: String = """shader_type %s;
|
|
@@ -25,6 +26,7 @@ const CELLS_SHOWN: int = 1000
|
|
|
25
26
|
|
|
26
27
|
var _editor_plugin: EditorPlugin = null
|
|
27
28
|
var _properties: GdhPropertyValues = GdhPropertyValues.new()
|
|
29
|
+
var _values: GdhSerialisation = GdhSerialisation.new()
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
@@ -257,6 +259,8 @@ func create_resource(args: Dictionary) -> Dictionary:
|
|
|
257
259
|
var failed: String = _save_checked(resource, res_path, properties.keys(), null)
|
|
258
260
|
if not failed.is_empty():
|
|
259
261
|
return {"ok": false, "error": failed}
|
|
262
|
+
if not properties.is_empty():
|
|
263
|
+
answer["properties"] = _held(resource, properties.keys())
|
|
260
264
|
return answer
|
|
261
265
|
|
|
262
266
|
|
|
@@ -307,9 +311,20 @@ func modify_resource(args: Dictionary) -> Dictionary:
|
|
|
307
311
|
var failed: String = _save_checked(resource, res_path, properties.keys(), previous)
|
|
308
312
|
if not failed.is_empty():
|
|
309
313
|
return {"ok": false, "error": failed}
|
|
314
|
+
answer["properties"] = _held(resource, properties.keys())
|
|
310
315
|
return answer
|
|
311
316
|
|
|
312
317
|
|
|
318
|
+
## What [param resource] holds at each property or path in [param asked], once the save has been
|
|
319
|
+
## checked against it: a write saying only that it was written left the caller a second call to
|
|
320
|
+
## learn what the engine kept.
|
|
321
|
+
func _held(resource: Resource, asked: Array) -> Dictionary:
|
|
322
|
+
var held: Dictionary = {}
|
|
323
|
+
for key: Variant in asked:
|
|
324
|
+
held[str(key)] = _values.serialize_value(_properties.read_at(resource, str(key)))
|
|
325
|
+
return held
|
|
326
|
+
|
|
327
|
+
|
|
313
328
|
## What a resource file holds, read as a headless engine reads it with no editor connected.
|
|
314
329
|
func read_resource(args: Dictionary) -> Dictionary:
|
|
315
330
|
var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
|
|
@@ -649,7 +664,16 @@ func _tile_node(scene: GdhSceneFile, node_path: String, layer: int) -> Array:
|
|
|
649
664
|
return [null, "%s has %d layers, and no layer %d" % [node_path, layered.get_layers_count(), layer]]
|
|
650
665
|
var tile_set: TileSet = layered.tile_set if layered != null else single.tile_set
|
|
651
666
|
if tile_set == null:
|
|
652
|
-
return [
|
|
667
|
+
return [
|
|
668
|
+
null,
|
|
669
|
+
(
|
|
670
|
+
(
|
|
671
|
+
'%s has no TileSet, so no cell can name a source. scene_node set with properties {"tile_set": '
|
|
672
|
+
+ '"res://..."} on %s gives it one, a TileSet file such as resource_edit create_tileset makes.'
|
|
673
|
+
)
|
|
674
|
+
% [node_path, node_path]
|
|
675
|
+
)
|
|
676
|
+
]
|
|
653
677
|
return [node, ""]
|
|
654
678
|
|
|
655
679
|
|
|
@@ -118,13 +118,16 @@ static func inherit(scene_path: String, base_path: String, root_name: String, sc
|
|
|
118
118
|
"%s did not load back as a scene inheriting %s, so it was removed." % [scene_path, base_path]
|
|
119
119
|
}
|
|
120
120
|
GdhSceneFile.refresh(scene_path)
|
|
121
|
-
|
|
121
|
+
var answer: Dictionary = {
|
|
122
122
|
"ok": true,
|
|
123
123
|
"scenePath": scene_path,
|
|
124
124
|
"inherits": base_path,
|
|
125
125
|
"rootNodeType": root_type,
|
|
126
126
|
"rootNodeName": name
|
|
127
127
|
}
|
|
128
|
+
if script != null:
|
|
129
|
+
answer["script"] = script.resource_path
|
|
130
|
+
return answer
|
|
128
131
|
|
|
129
132
|
|
|
130
133
|
## Makes the children of [param node], an instance in [param scene], editable or not, answering why
|
|
@@ -171,6 +171,16 @@ static func place(node: Node, asked: Variant) -> void:
|
|
|
171
171
|
node.get_parent().move_child(node, GdhRead.as_int(asked))
|
|
172
172
|
|
|
173
173
|
|
|
174
|
+
## Whether [param basis] has axes that are not at right angles, which a node kept in place under a
|
|
175
|
+
## parent scaled unevenly across its turn ends up with. A Node2D says so through skew; a Node3D has
|
|
176
|
+
## no property for it, and its position, rotation and scale read as if the axes were square.
|
|
177
|
+
static func sheared(basis: Basis) -> bool:
|
|
178
|
+
var x: Vector3 = basis.x.normalized()
|
|
179
|
+
var y: Vector3 = basis.y.normalized()
|
|
180
|
+
var z: Vector3 = basis.z.normalized()
|
|
181
|
+
return absf(x.dot(y)) > 0.0001 or absf(x.dot(z)) > 0.0001 or absf(y.dot(z)) > 0.0001
|
|
182
|
+
|
|
183
|
+
|
|
174
184
|
## The properties that hold where [param node] is drawn, which a reparent keeping it in place
|
|
175
185
|
## changes: a Control keeps only its position, as Node.reparent keeps it.
|
|
176
186
|
static func placing(node: Node) -> Array[String]:
|
|
@@ -204,6 +204,8 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
|
|
|
204
204
|
var answer: Dictionary = {
|
|
205
205
|
"ok": true, "scenePath": scene_path, "rootNodeType": asked_type, "rootNodeName": root_node_name
|
|
206
206
|
}
|
|
207
|
+
if not script_path.is_empty():
|
|
208
|
+
answer["script"] = script_path
|
|
207
209
|
if not had_folder:
|
|
208
210
|
answer["madeFolder"] = scene_path.get_base_dir()
|
|
209
211
|
return answer
|
|
@@ -299,6 +301,11 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
299
301
|
"nodePath": new_path,
|
|
300
302
|
"index": new_node.get_index(),
|
|
301
303
|
}
|
|
304
|
+
if not properties.is_empty():
|
|
305
|
+
var held: Dictionary = {}
|
|
306
|
+
for key: Variant in properties:
|
|
307
|
+
held[str(key)] = _readable(new_node, _properties.read_at(new_node, str(key)))
|
|
308
|
+
answer["properties"] = held
|
|
302
309
|
_properties.note_write(answer, new_node)
|
|
303
310
|
if not adding.is_empty():
|
|
304
311
|
answer.merge(GdhGroups.read(scene, new_node))
|
|
@@ -503,6 +510,7 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
503
510
|
var drawn_at: Dictionary = {}
|
|
504
511
|
for property: String in GdhPlacing.placing(node):
|
|
505
512
|
drawn_at[property] = _properties.read_at(node, property)
|
|
513
|
+
var held_at: Transform3D = (node as Node3D).transform if node is Node3D else Transform3D.IDENTITY
|
|
506
514
|
# Unowned while it moves: a node taken out of the tree keeps its owner, and adding it under the new
|
|
507
515
|
# parent warned that the owner was not above it. Node.reparent warns the same way.
|
|
508
516
|
node.owner = null
|
|
@@ -517,6 +525,20 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
517
525
|
var now: Variant = _properties.read_at(node, property)
|
|
518
526
|
if not GdhSame.same(now, drawn_at[property]):
|
|
519
527
|
also_changed[property] = {"was": _readable(node, drawn_at[property]), "now": _readable(node, now)}
|
|
528
|
+
var sheared: String = ""
|
|
529
|
+
if node is Node3D and GdhPlacing.sheared((node as Node3D).basis):
|
|
530
|
+
var spatial: Node3D = node
|
|
531
|
+
also_changed["transform"] = {
|
|
532
|
+
"was": _readable(node, held_at), "now": _readable(node, spatial.transform)
|
|
533
|
+
}
|
|
534
|
+
sheared = (
|
|
535
|
+
(
|
|
536
|
+
"%s keeps where it is drawn under %s, which is scaled unevenly across the way it is turned,"
|
|
537
|
+
+ " so its basis is sheared: no position, rotation and scale describe it, and transform is"
|
|
538
|
+
+ " what it holds. keepGlobalTransform false keeps its own transform instead."
|
|
539
|
+
)
|
|
540
|
+
% [str(node.name), new_parent_path]
|
|
541
|
+
)
|
|
520
542
|
|
|
521
543
|
var new_path: String = scene.path_of(node)
|
|
522
544
|
var index: int = node.get_index()
|
|
@@ -546,6 +568,8 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
546
568
|
}
|
|
547
569
|
if not also_changed.is_empty():
|
|
548
570
|
answer["alsoChanged"] = also_changed
|
|
571
|
+
if not sheared.is_empty():
|
|
572
|
+
answer["sheared"] = sheared
|
|
549
573
|
if new_path != old_path:
|
|
550
574
|
answer.merge(GdhKin.follow(scene_path, old_path, new_path, carried))
|
|
551
575
|
return answer
|
|
@@ -571,9 +595,8 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
571
595
|
var node: Node = GdhSceneFile.node_at(scene.root, node_path)
|
|
572
596
|
if not node:
|
|
573
597
|
return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
return scene.refuse(foreign)
|
|
598
|
+
# A rename is refused for the scene that makes the node before anything else, since editable
|
|
599
|
+
# children would let this scene keep values on it and still never rename it.
|
|
577
600
|
if properties.has("name") and node != scene.root:
|
|
578
601
|
var fixed: String = scene.fixed(node)
|
|
579
602
|
if not fixed.is_empty():
|
|
@@ -581,6 +604,9 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
581
604
|
var unusable: String = scene.unusable_name(node.get_parent(), str(properties["name"]), node)
|
|
582
605
|
if not unusable.is_empty():
|
|
583
606
|
return scene.refuse(unusable)
|
|
607
|
+
var foreign: String = scene.foreign(node)
|
|
608
|
+
if not foreign.is_empty():
|
|
609
|
+
return scene.refuse(foreign)
|
|
584
610
|
|
|
585
611
|
var dropped: Dictionary = {}
|
|
586
612
|
if editable != null:
|
|
@@ -607,6 +633,12 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
607
633
|
_properties.note_write(answer, node)
|
|
608
634
|
if not adding.is_empty() or not removing.is_empty():
|
|
609
635
|
answer["groups"] = GdhGroups.read(scene, node).get("groups", [])
|
|
636
|
+
var named: Array[String] = []
|
|
637
|
+
for key: Variant in properties:
|
|
638
|
+
var property: String = str(key).get_slice(":", 0)
|
|
639
|
+
if property != "name" and property in node and not named.has(property):
|
|
640
|
+
named.append(property)
|
|
641
|
+
var source: String = scene.values_from(node, old_path)
|
|
610
642
|
var carried: GdhPaths = GdhPaths.new()
|
|
611
643
|
var _rewritten: int = carried.carry(scene, {old_path: new_path} if new_path != old_path else {})
|
|
612
644
|
var written: Dictionary = scene.write(
|
|
@@ -630,11 +662,48 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
630
662
|
if not dropped.is_empty():
|
|
631
663
|
answer["droppedValues"] = dropped["values"]
|
|
632
664
|
answer["droppedNodes"] = dropped["nodes"]
|
|
665
|
+
_note_left_out(answer, scene.written_pack, new_path, named, source)
|
|
633
666
|
if new_path != old_path:
|
|
634
667
|
answer.merge(GdhKin.follow(scene_path, old_path, new_path, carried))
|
|
635
668
|
return answer
|
|
636
669
|
|
|
637
670
|
|
|
671
|
+
## Notes under notStored each of [param named] the file saved from [param pack] does not set on the
|
|
672
|
+
## node at [param at]: a set gave it the value the node has without it, so the packer leaves it out,
|
|
673
|
+
## as the editor does, and it follows [param source], or its default, from then on.
|
|
674
|
+
static func _note_left_out(
|
|
675
|
+
answer: Dictionary, pack: PackedScene, at: String, named: Array[String], source: String
|
|
676
|
+
) -> void:
|
|
677
|
+
var stored: Dictionary = {}
|
|
678
|
+
var state: SceneState = pack.get_state()
|
|
679
|
+
for index: int in state.get_node_count():
|
|
680
|
+
if str(state.get_node_path(index)).trim_prefix("./") == at:
|
|
681
|
+
for property: int in state.get_node_property_count(index):
|
|
682
|
+
stored[str(state.get_node_property_name(index, property))] = true
|
|
683
|
+
var left: Array[String] = []
|
|
684
|
+
for asked: String in named:
|
|
685
|
+
if not stored.has(asked):
|
|
686
|
+
left.append(asked)
|
|
687
|
+
if left.is_empty():
|
|
688
|
+
return
|
|
689
|
+
answer["notStored"] = left
|
|
690
|
+
var names: String = ", ".join(left)
|
|
691
|
+
var verb: String = "holds" if left.size() == 1 else "hold"
|
|
692
|
+
var pronoun: String = "it" if left.size() == 1 else "them"
|
|
693
|
+
if source.is_empty():
|
|
694
|
+
answer["notStoredNote"] = (
|
|
695
|
+
"%s now %s the default, so the file does not store %s." % [names, verb, pronoun]
|
|
696
|
+
)
|
|
697
|
+
else:
|
|
698
|
+
answer["notStoredNote"] = (
|
|
699
|
+
(
|
|
700
|
+
"%s now %s what %s gives this node, so the file does not store %s, and a change there reaches "
|
|
701
|
+
+ "this node."
|
|
702
|
+
)
|
|
703
|
+
% [names, verb, source, pronoun]
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
|
|
638
707
|
func get_node_properties(args: Dictionary) -> Dictionary:
|
|
639
708
|
var project_path: String = str(args.get("projectPath", ""))
|
|
640
709
|
var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
|
|
@@ -265,8 +265,14 @@ func disconnect_signal(args: Dictionary) -> Dictionary:
|
|
|
265
265
|
# A connection that was never there saved the scene unchanged and answered as removed, so a
|
|
266
266
|
# typo in either name read as success.
|
|
267
267
|
if _connection_flags(scene.root, from, signal_name, to, method_name) < 0:
|
|
268
|
+
# The root's path is ".", which before ".method" read as "..method".
|
|
269
|
+
var said_from: String = str(source.name) if source == scene.root else from
|
|
270
|
+
var said_to: String = str(target.name) if target == scene.root else to
|
|
268
271
|
return scene.refuse(
|
|
269
|
-
|
|
272
|
+
(
|
|
273
|
+
"%s has no connection from %s.%s to %s.%s."
|
|
274
|
+
% [scene_path, said_from, signal_name, said_to, method_name]
|
|
275
|
+
)
|
|
270
276
|
)
|
|
271
277
|
source.disconnect(signal_name, Callable(target, method_name))
|
|
272
278
|
|
|
@@ -16,6 +16,10 @@ var refused: String = ""
|
|
|
16
16
|
var raised: Array[String] = []
|
|
17
17
|
## The warnings reported while it ran, a line each.
|
|
18
18
|
var warned: Array[String] = []
|
|
19
|
+
## Where a script error first ended each function it ended, as "res://path:line" by function name.
|
|
20
|
+
## A function the VM ends at a script error gives back its return type's default, 0, "" or null,
|
|
21
|
+
## and its caller goes on with that as if it had been returned.
|
|
22
|
+
var ended_at: Dictionary = {}
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
func _log_message(_message: String, _error: bool) -> void:
|
|
@@ -26,8 +30,8 @@ func _log_message(_message: String, _error: bool) -> void:
|
|
|
26
30
|
# gdlint:ignore = function-arguments-number
|
|
27
31
|
func _log_error(
|
|
28
32
|
function: String,
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
file: String,
|
|
34
|
+
line: int,
|
|
31
35
|
code: String,
|
|
32
36
|
rationale: String,
|
|
33
37
|
_editor_notify: bool,
|
|
@@ -45,3 +49,5 @@ func _log_error(
|
|
|
45
49
|
warned.append(text)
|
|
46
50
|
else:
|
|
47
51
|
raised.append(text)
|
|
52
|
+
if error_type == ERROR_TYPE_SCRIPT and not ended_at.has(function):
|
|
53
|
+
ended_at[function] = "%s:%d" % [file, line]
|
|
@@ -24,6 +24,23 @@ extends RefCounted
|
|
|
24
24
|
|
|
25
25
|
## How many of a node's children a refusal for a path missing below it names.
|
|
26
26
|
const NAMED_CHILDREN: int = 12
|
|
27
|
+
## The types holding their floats as the engine's real numbers, 32-bit unless it was built for 64.
|
|
28
|
+
const SHAPES_OF_REALS: Array[int] = [
|
|
29
|
+
TYPE_VECTOR2,
|
|
30
|
+
TYPE_VECTOR3,
|
|
31
|
+
TYPE_VECTOR4,
|
|
32
|
+
TYPE_RECT2,
|
|
33
|
+
TYPE_PLANE,
|
|
34
|
+
TYPE_QUATERNION,
|
|
35
|
+
TYPE_AABB,
|
|
36
|
+
TYPE_BASIS,
|
|
37
|
+
TYPE_TRANSFORM2D,
|
|
38
|
+
TYPE_TRANSFORM3D,
|
|
39
|
+
TYPE_PROJECTION,
|
|
40
|
+
TYPE_PACKED_VECTOR2_ARRAY,
|
|
41
|
+
TYPE_PACKED_VECTOR3_ARRAY,
|
|
42
|
+
TYPE_PACKED_VECTOR4_ARRAY,
|
|
43
|
+
]
|
|
27
44
|
|
|
28
45
|
|
|
29
46
|
static func as_int(value: Variant, fallback: int = 0) -> int:
|
|
@@ -190,18 +207,26 @@ static func offered(said: String, word: String, names: Array) -> String:
|
|
|
190
207
|
|
|
191
208
|
|
|
192
209
|
## [param said], the refusal for [param property], which [param target] does not declare, with the
|
|
193
|
-
## names it declares nearest it when any are, as a misspelt one is usually one of them
|
|
194
|
-
##
|
|
210
|
+
## names it declares nearest it when any are, as a misspelt one is usually one of them, or else the
|
|
211
|
+
## variables its script declares. The engine's own are not listed when none is near, since an object
|
|
212
|
+
## has a hundred, while a script's are the few a caller most likely meant.
|
|
195
213
|
static func near_property(said: String, target: Object, property: String) -> String:
|
|
196
214
|
var headings: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
|
|
197
215
|
var names: Array[String] = []
|
|
216
|
+
var scripted: Array[String] = []
|
|
198
217
|
for declared: Dictionary in target.get_property_list():
|
|
199
|
-
|
|
218
|
+
var usage: int = as_int(declared.get("usage", 0))
|
|
219
|
+
if not usage & headings:
|
|
200
220
|
names.append(str(declared.get("name", "")))
|
|
221
|
+
if usage & PROPERTY_USAGE_SCRIPT_VARIABLE and not usage & headings:
|
|
222
|
+
scripted.append(str(declared.get("name", "")))
|
|
201
223
|
var close: PackedStringArray = nearest(property, PackedStringArray(names))
|
|
202
|
-
if close.is_empty():
|
|
224
|
+
if not close.is_empty():
|
|
225
|
+
return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
226
|
+
if scripted.is_empty():
|
|
203
227
|
return said
|
|
204
|
-
|
|
228
|
+
scripted.sort()
|
|
229
|
+
return "%s; its script declares %s" % [said, ", ".join(PackedStringArray(scripted))]
|
|
205
230
|
|
|
206
231
|
|
|
207
232
|
## The signals [param object] has that a caller can connect, its own class's first: the engine
|
|
@@ -219,3 +244,34 @@ static func signals_of(object: Object) -> Array:
|
|
|
219
244
|
## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
|
|
220
245
|
static func with_article(word: String) -> String:
|
|
221
246
|
return ("an " if word.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + word
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
## [param value], which the engine holds as a 32-bit float, as the shortest decimal that is still
|
|
250
|
+
## that float. JSON carries a double, and the double a 32-bit 0.2 widens to is 0.200000002980232:
|
|
251
|
+
## the same number, but not the one written or the one the file shows, so a caller comparing what it
|
|
252
|
+
## set against what it read saw them differ. The shorter one, sent back, stores the same 32 bits. A
|
|
253
|
+
## plain float is left alone by the serialiser, since nothing says whether the engine holds it in
|
|
254
|
+
## 32 bits or 64.
|
|
255
|
+
static func single(value: float) -> float:
|
|
256
|
+
if not is_finite(value) or value == 0.0:
|
|
257
|
+
return value
|
|
258
|
+
var held: float = PackedFloat32Array([value])[0]
|
|
259
|
+
var magnitude: int = floori(log(absf(held)) / log(10.0))
|
|
260
|
+
for digits: int in range(1, 10):
|
|
261
|
+
var shift: int = digits - 1 - magnitude
|
|
262
|
+
var tried: float = (
|
|
263
|
+
roundf(held * pow(10.0, shift)) / pow(10.0, shift)
|
|
264
|
+
if shift >= 0
|
|
265
|
+
else roundf(held / pow(10.0, -shift)) * pow(10.0, -shift)
|
|
266
|
+
)
|
|
267
|
+
if PackedFloat32Array([tried])[0] == held:
|
|
268
|
+
return tried
|
|
269
|
+
return held
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
## Whether a value of [param type] holds its floats in 32 bits: a colour always, and the vectors and
|
|
273
|
+
## the shapes built of them unless the engine was built for 64.
|
|
274
|
+
static func holds_singles(type: int) -> bool:
|
|
275
|
+
if type in [TYPE_COLOR, TYPE_PACKED_COLOR_ARRAY, TYPE_PACKED_FLOAT32_ARRAY]:
|
|
276
|
+
return true
|
|
277
|
+
return not OS.has_feature("double") and type in SHAPES_OF_REALS
|