gdharness 1.3.41 → 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 +69 -38
- package/build/godot/addons/gdharness_editor/animation_reads.gd +31 -4
- package/build/godot/addons/gdharness_editor/file_reads.gd +34 -0
- package/build/godot/addons/gdharness_editor/headless_check.gd +29 -0
- 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 +64 -1
- 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/operations/animation_reads.gd +31 -4
- package/build/godot/operations/file_reads.gd +34 -0
- package/build/godot/operations/gdscript_analysis.gd +19 -1
- package/build/godot/operations/gdscript_authoring.gd +55 -3
- package/build/godot/operations/input_actions.gd +1 -0
- package/build/godot/operations/project_config.gd +93 -0
- package/package.json +1 -1
|
@@ -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 "
|
|
@@ -317,6 +327,7 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
317
327
|
_properties.note_write(answer, new_node)
|
|
318
328
|
if not adding.is_empty():
|
|
319
329
|
answer.merge(GdhGroups.read_added(new_node, adding))
|
|
330
|
+
var id: int = new_node.get_instance_id()
|
|
320
331
|
var written: Dictionary = scene.write(
|
|
321
332
|
func(saved: Node) -> String:
|
|
322
333
|
var unkept: String = _kept(
|
|
@@ -329,6 +340,8 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
329
340
|
if not written.is_empty():
|
|
330
341
|
return written
|
|
331
342
|
|
|
343
|
+
_note_put_back(answer, scene, id, new_path)
|
|
344
|
+
_note_loads_as(answer, scene, new_path)
|
|
332
345
|
if packed != null:
|
|
333
346
|
answer["instanceOf"] = instance_path
|
|
334
347
|
return answer
|
|
@@ -519,6 +532,7 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
519
532
|
for property: String in GdhPlacing.placing(node):
|
|
520
533
|
drawn_at[property] = _properties.read_at(node, property)
|
|
521
534
|
var held_at: Transform3D = (node as Node3D).transform if node is Node3D else Transform3D.IDENTITY
|
|
535
|
+
var stored_before: Dictionary = GdhHeadlessCheck.stored(node)
|
|
522
536
|
# Unowned while it moves: a node taken out of the tree keeps its owner, and adding it under the new
|
|
523
537
|
# parent warned that the owner was not above it. Node.reparent warns the same way.
|
|
524
538
|
node.owner = null
|
|
@@ -528,6 +542,11 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
528
542
|
GdhPlacing.own(node, root)
|
|
529
543
|
if GdhRead.as_bool(args.get("keepGlobalTransform", true)):
|
|
530
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)
|
|
531
550
|
var also_changed: Dictionary = {}
|
|
532
551
|
for property: String in drawn_at:
|
|
533
552
|
var now: Variant = _properties.read_at(node, property)
|
|
@@ -644,6 +663,13 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
644
663
|
var new_path: String = scene.path_of(node)
|
|
645
664
|
var answer: Dictionary = {"ok": true, "nodePath": new_path, "properties": held}
|
|
646
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
|
|
647
673
|
if not adding.is_empty() or not removing.is_empty():
|
|
648
674
|
answer["groups"] = GdhGroups.read(scene, node).get("groups", [])
|
|
649
675
|
var named: Array[String] = []
|
|
@@ -654,6 +680,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
654
680
|
var source: String = scene.values_from(node, old_path)
|
|
655
681
|
var carried: GdhPaths = GdhPaths.new()
|
|
656
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()
|
|
657
684
|
var written: Dictionary = scene.write(
|
|
658
685
|
func(saved: Node) -> String:
|
|
659
686
|
var unkept: String = _kept(scene, saved, node, properties.keys())
|
|
@@ -675,12 +702,48 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
675
702
|
if not dropped.is_empty():
|
|
676
703
|
answer["droppedValues"] = dropped["values"]
|
|
677
704
|
answer["droppedNodes"] = dropped["nodes"]
|
|
705
|
+
_note_put_back(answer, scene, id, new_path)
|
|
706
|
+
_note_loads_as(answer, scene, new_path)
|
|
678
707
|
_note_left_out(answer, scene.written_pack, new_path, named, source)
|
|
679
708
|
if new_path != old_path:
|
|
680
709
|
answer.merge(GdhKin.follow(scene_path, old_path, new_path, carried))
|
|
681
710
|
return answer
|
|
682
711
|
|
|
683
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
|
+
|
|
684
747
|
## Notes under notStored each of [param named] the file saved from [param pack] does not set on the
|
|
685
748
|
## node at [param at]: a set gave it the value the node has without it, so the packer leaves it out,
|
|
686
749
|
## as the editor does, and it follows [param source], or its default, from then on.
|
|
@@ -26,6 +26,7 @@ const CHANGES: String = "runtime_changes.gd"
|
|
|
26
26
|
const INPUT: String = "runtime_input.gd"
|
|
27
27
|
const TYPING: String = "runtime_typing.gd"
|
|
28
28
|
const PAD: String = "runtime_pad.gd"
|
|
29
|
+
const TOUCH: String = "runtime_touch.gd"
|
|
29
30
|
const CAPTURE: String = "runtime_capture.gd"
|
|
30
31
|
const WAITS: String = "runtime_waits.gd"
|
|
31
32
|
const ACCESSIBILITY: String = "runtime_accessibility.gd"
|
|
@@ -52,6 +53,8 @@ const MODULE_COMMANDS: Dictionary = {
|
|
|
52
53
|
"inject_mouse_drag": [INPUT, "inject_mouse_drag"],
|
|
53
54
|
"inject_joypad_button": [PAD, "inject_joypad_button"],
|
|
54
55
|
"inject_joypad_motion": [PAD, "inject_joypad_motion"],
|
|
56
|
+
"inject_touch": [TOUCH, "inject_touch"],
|
|
57
|
+
"inject_touch_drag": [TOUCH, "inject_touch_drag"],
|
|
55
58
|
"click": [INPUT, "click"],
|
|
56
59
|
"choose": [TYPING, "choose"],
|
|
57
60
|
"wait_frames": [WAITS, "wait_frames"],
|
|
@@ -218,7 +221,7 @@ func _module(file: String) -> Object:
|
|
|
218
221
|
match file:
|
|
219
222
|
QUERIES:
|
|
220
223
|
made = script.new(self, values, _frames)
|
|
221
|
-
TYPING, PAD, ACCESSIBILITY:
|
|
224
|
+
TYPING, PAD, TOUCH, ACCESSIBILITY:
|
|
222
225
|
made = script.new(self)
|
|
223
226
|
_:
|
|
224
227
|
made = script.new(self, values)
|
|
@@ -96,11 +96,12 @@ static func where(aim: Dictionary, viewport: Viewport, points: Array[Vector2]) -
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
## The points of a drag: x, y, then each of via, then toX, toY. Answers the String saying what is
|
|
99
|
-
## wrong instead, since a point left out would otherwise read as the window's corner.
|
|
100
|
-
|
|
99
|
+
## wrong instead, since a point left out would otherwise read as the window's corner. [param op] is
|
|
100
|
+
## the drag the refusal names.
|
|
101
|
+
static func stroke_of(params: Dictionary, op: String = "mouse_drag") -> Variant:
|
|
101
102
|
for key: String in ["x", "y", "toX", "toY"]:
|
|
102
103
|
if not params.has(key):
|
|
103
|
-
return "
|
|
104
|
+
return "%s needs x, y, toX and toY: %s is missing." % [op, key]
|
|
104
105
|
var points: Array[Vector2] = [Vector2(GdhRead.as_float(params["x"]), GdhRead.as_float(params["y"]))]
|
|
105
106
|
var via: Variant = params.get("via", [])
|
|
106
107
|
if not via is Array:
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## What reaches the running game the way a touchscreen reaches it: a finger pressed, held, lifted
|
|
4
|
+
## and dragged, as InputEventScreenTouch and InputEventScreenDrag.
|
|
5
|
+
##
|
|
6
|
+
## The events themselves rather than a mouse standing in for them: input code tells a finger from a
|
|
7
|
+
## mouse by the event's class, or by the mouse events the engine makes from a touch, which carry
|
|
8
|
+
## InputEvent.DEVICE_ID_EMULATION. A mouse_click carries the mouse's own device, so a game switching
|
|
9
|
+
## its glyphs or focus on a touch never saw one. Sent through Input for the game's window, so the
|
|
10
|
+
## engine makes those mouse events itself where input_devices/pointing/emulate_mouse_from_touch is on.
|
|
11
|
+
|
|
12
|
+
const GdhRead = preload("reading.gd")
|
|
13
|
+
const GdhValues = preload("runtime_values.gd")
|
|
14
|
+
const GdhMouse = preload("runtime_mouse.gd")
|
|
15
|
+
|
|
16
|
+
const EMULATE_SETTING: String = "input_devices/pointing/emulate_mouse_from_touch"
|
|
17
|
+
|
|
18
|
+
var _host: Node
|
|
19
|
+
# The finger the engine makes mouse events from, as Input keeps it for the touches sent here: the
|
|
20
|
+
# first one pressed while none is, until it lifts. -1 for none.
|
|
21
|
+
var _emulating: int = -1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
func _init(host: Node) -> void:
|
|
25
|
+
_host = host
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Presses a finger at a position, and lifts it a frame later unless asked to hold it, as
|
|
29
|
+
## inject_mouse_click does a button: a finger left down is a touch that never ends.
|
|
30
|
+
func inject_touch(params: Dictionary) -> Dictionary:
|
|
31
|
+
var root: Window = _host.get_tree().root
|
|
32
|
+
var aimed: Variant = GdhMouse.aim_of(params, root)
|
|
33
|
+
if aimed is String:
|
|
34
|
+
return {"type": "error", "message": aimed}
|
|
35
|
+
var aim: Dictionary = aimed
|
|
36
|
+
var point: Variant = GdhMouse.read_point(params, "x", "y")
|
|
37
|
+
if point is String:
|
|
38
|
+
return {"type": "error", "message": point}
|
|
39
|
+
var finger: Variant = _finger_of(params)
|
|
40
|
+
if finger is String:
|
|
41
|
+
return {"type": "error", "message": finger}
|
|
42
|
+
var index: int = finger
|
|
43
|
+
var holding: Variant = GdhValues.held_for(params)
|
|
44
|
+
if holding is String:
|
|
45
|
+
return {"type": "error", "message": holding}
|
|
46
|
+
var frames: int = holding
|
|
47
|
+
var local: Vector2 = point
|
|
48
|
+
var placed: Dictionary = GdhMouse.carried(aim, local, root)
|
|
49
|
+
var viewport: Viewport = placed["viewport"]
|
|
50
|
+
var position: Vector2 = placed["position"]
|
|
51
|
+
var held: bool = GdhRead.as_bool(params.get("pressed", true), true)
|
|
52
|
+
var whole: bool = not params.has("pressed")
|
|
53
|
+
|
|
54
|
+
var at: Array[Vector2] = [position]
|
|
55
|
+
var where: Dictionary = GdhMouse.where(aim, viewport, at)
|
|
56
|
+
var emulates: bool = _send(viewport, _touch(position, index, whole or held))
|
|
57
|
+
var lasted: Variant = null
|
|
58
|
+
if whole:
|
|
59
|
+
lasted = await GdhValues.hold(_host.get_tree(), frames)
|
|
60
|
+
var _lifted: bool = _send(viewport, _touch(position, index, false))
|
|
61
|
+
await _host.get_tree().process_frame
|
|
62
|
+
|
|
63
|
+
var answer: Dictionary = {
|
|
64
|
+
"type": "input_injected",
|
|
65
|
+
"input_type": "touch",
|
|
66
|
+
"index": index,
|
|
67
|
+
"position": [position.x, position.y],
|
|
68
|
+
"held_frames": GdhValues.held_answer(whole, frames),
|
|
69
|
+
"held_for": lasted,
|
|
70
|
+
"pressed": held and not whole,
|
|
71
|
+
"whole": whole,
|
|
72
|
+
"emulates_mouse": emulates,
|
|
73
|
+
}
|
|
74
|
+
answer.merge(where)
|
|
75
|
+
return answer
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
## Drags a finger from one point to another through any between: the touch, one drag each process
|
|
79
|
+
## frame along the stroke, and the lift where the last drag left it, a frame after it. Each drag goes
|
|
80
|
+
## in a frame of its own because the engine merges the drags it is given within one frame, as it
|
|
81
|
+
## merges a mouse's motions.
|
|
82
|
+
func inject_touch_drag(params: Dictionary) -> Dictionary:
|
|
83
|
+
var root: Window = _host.get_tree().root
|
|
84
|
+
var aimed: Variant = GdhMouse.aim_of(params, root)
|
|
85
|
+
if aimed is String:
|
|
86
|
+
return {"type": "error", "message": aimed}
|
|
87
|
+
var aim: Dictionary = aimed
|
|
88
|
+
var stroke: Variant = GdhMouse.stroke_of(params, "touch_drag")
|
|
89
|
+
if stroke is String:
|
|
90
|
+
return {"type": "error", "message": stroke}
|
|
91
|
+
var finger: Variant = _finger_of(params)
|
|
92
|
+
if finger is String:
|
|
93
|
+
return {"type": "error", "message": finger}
|
|
94
|
+
var index: int = finger
|
|
95
|
+
var frames: int = GdhRead.as_int(params.get("frames", 0), 0)
|
|
96
|
+
if frames < 1 or frames > GdhValues.MOST_HELD:
|
|
97
|
+
return {
|
|
98
|
+
"type": "error", "message": "frames is %d, and it takes 1 to %d." % [frames, GdhValues.MOST_HELD]
|
|
99
|
+
}
|
|
100
|
+
# Carried once, before the touch: a finger drags across the screen, not along a node that moves.
|
|
101
|
+
var named: Array[Vector2] = stroke
|
|
102
|
+
var points: Array[Vector2] = []
|
|
103
|
+
var viewport: Viewport = root
|
|
104
|
+
for local: Vector2 in named:
|
|
105
|
+
var placed: Dictionary = GdhMouse.carried(aim, local, root)
|
|
106
|
+
viewport = placed["viewport"]
|
|
107
|
+
points.append(placed["position"])
|
|
108
|
+
|
|
109
|
+
var lengths: Array[float] = GdhMouse.lengths_of(points)
|
|
110
|
+
var start: Vector2 = points[0]
|
|
111
|
+
var end: Vector2 = points[points.size() - 1]
|
|
112
|
+
var where: Dictionary = GdhMouse.where(aim, viewport, points)
|
|
113
|
+
var emulates: bool = _send(viewport, _touch(start, index, true))
|
|
114
|
+
var last: Vector2 = start
|
|
115
|
+
var travelled: Vector2 = Vector2.ZERO
|
|
116
|
+
for step: int in range(1, frames + 1):
|
|
117
|
+
await _host.get_tree().process_frame
|
|
118
|
+
if not is_instance_valid(viewport):
|
|
119
|
+
return {
|
|
120
|
+
"type": "error",
|
|
121
|
+
"message":
|
|
122
|
+
(
|
|
123
|
+
(
|
|
124
|
+
"%s was freed after the touch and %d of %d drags, so the rest of the stroke"
|
|
125
|
+
+ " and the lift went nowhere"
|
|
126
|
+
)
|
|
127
|
+
% [where.get("window", "The window"), step - 1, frames]
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
var at: Vector2 = GdhMouse.along(points, lengths, float(step) / frames)
|
|
131
|
+
var relative: Vector2 = at - last
|
|
132
|
+
last = at
|
|
133
|
+
travelled += relative
|
|
134
|
+
var _dragged: bool = _send(viewport, _drag(at, relative, index))
|
|
135
|
+
await _host.get_tree().process_frame
|
|
136
|
+
var _lifted: bool = _send(viewport, _touch(end, index, false))
|
|
137
|
+
await _host.get_tree().process_frame
|
|
138
|
+
|
|
139
|
+
var answer: Dictionary = {
|
|
140
|
+
"type": "input_injected",
|
|
141
|
+
"input_type": "touch_drag",
|
|
142
|
+
"index": index,
|
|
143
|
+
"from": [start.x, start.y],
|
|
144
|
+
"to": [end.x, end.y],
|
|
145
|
+
"motions": frames,
|
|
146
|
+
"relative_total": [travelled.x, travelled.y],
|
|
147
|
+
"emulates_mouse": emulates,
|
|
148
|
+
}
|
|
149
|
+
answer.merge(where)
|
|
150
|
+
return answer
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
## The finger [param params].index names, 0 when none is given, or the String refusing it.
|
|
154
|
+
static func _finger_of(params: Dictionary) -> Variant:
|
|
155
|
+
if not params.has("index"):
|
|
156
|
+
return 0
|
|
157
|
+
var given: Variant = params["index"]
|
|
158
|
+
var number: float = GdhRead.as_float(given)
|
|
159
|
+
if not (given is int or given is float) or number != roundf(number):
|
|
160
|
+
return "index is the finger, a whole number from 0, and was %s." % JSON.stringify(given)
|
|
161
|
+
var index: int = roundi(number)
|
|
162
|
+
if index < 0:
|
|
163
|
+
return "index is the finger, a whole number from 0, and was %d." % index
|
|
164
|
+
return index
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
## Sends [param event] into [param viewport]: through Input for the game's window, and pushed into
|
|
168
|
+
## any other, since Input reaches only the game's window. Answers whether the engine makes a mouse
|
|
169
|
+
## event from it, which it does in Input, with the setting on, for one finger at a time.
|
|
170
|
+
func _send(viewport: Viewport, event: InputEvent) -> bool:
|
|
171
|
+
if viewport != _host.get_tree().root:
|
|
172
|
+
if is_instance_valid(viewport):
|
|
173
|
+
viewport.push_input(event)
|
|
174
|
+
return false
|
|
175
|
+
Input.parse_input_event(event)
|
|
176
|
+
if not GdhRead.as_bool(ProjectSettings.get_setting(EMULATE_SETTING, true), true):
|
|
177
|
+
return false
|
|
178
|
+
var touch: InputEventScreenTouch = event as InputEventScreenTouch
|
|
179
|
+
if touch == null:
|
|
180
|
+
return (event as InputEventScreenDrag).index == _emulating
|
|
181
|
+
if touch.pressed:
|
|
182
|
+
if _emulating != -1:
|
|
183
|
+
return false
|
|
184
|
+
_emulating = touch.index
|
|
185
|
+
return true
|
|
186
|
+
if touch.index != _emulating:
|
|
187
|
+
return false
|
|
188
|
+
_emulating = -1
|
|
189
|
+
return true
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
static func _touch(position: Vector2, index: int, pressed: bool) -> InputEventScreenTouch:
|
|
193
|
+
var event: InputEventScreenTouch = InputEventScreenTouch.new()
|
|
194
|
+
event.index = index
|
|
195
|
+
event.position = position
|
|
196
|
+
event.pressed = pressed
|
|
197
|
+
return event
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
## A drag carrying [param relative] as the movement since the last, and the speed that is at the
|
|
201
|
+
## frame rate the game runs at.
|
|
202
|
+
func _drag(position: Vector2, relative: Vector2, index: int) -> InputEventScreenDrag:
|
|
203
|
+
var event: InputEventScreenDrag = InputEventScreenDrag.new()
|
|
204
|
+
event.index = index
|
|
205
|
+
event.position = position
|
|
206
|
+
event.relative = relative
|
|
207
|
+
event.screen_relative = relative
|
|
208
|
+
var seconds: float = maxf(_host.get_process_delta_time(), 1.0 / 60.0)
|
|
209
|
+
event.velocity = relative / seconds
|
|
210
|
+
event.screen_velocity = event.velocity
|
|
211
|
+
return event
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://ciivqlhs2dev0
|
|
@@ -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] = []
|
|
@@ -517,6 +517,18 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
517
517
|
scripted[str(declared.get("name", ""))] = true
|
|
518
518
|
var serialiser: GdhSerialisation = GdhSerialisation.new()
|
|
519
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
|
+
)
|
|
520
532
|
# A Curve's points are read off one with none, since the engine has no default for point_0.
|
|
521
533
|
var fresh: Object = (
|
|
522
534
|
ClassDB.instantiate(loaded.get_class()) if GdhShapes.STORED_APART.has(loaded.get_class()) else null
|
|
@@ -538,6 +550,8 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
538
550
|
)
|
|
539
551
|
if GdhSame.same(value, default):
|
|
540
552
|
continue
|
|
553
|
+
if from_file and not set_in_file.has(property):
|
|
554
|
+
continue
|
|
541
555
|
properties[property] = serialiser.serialize_value(value)
|
|
542
556
|
var answer: Dictionary = {
|
|
543
557
|
"ok": true, "resourcePath": res_path, "resourceType": loaded.get_class(), "properties": properties
|
|
@@ -552,6 +566,26 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
552
566
|
return answer
|
|
553
567
|
|
|
554
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
|
+
|
|
555
589
|
## The resource [param file] keeps under [param id], found in [param value] or anything it holds.
|
|
556
590
|
## The engine loads such a path only while the file is cached, and a read loads the file uncached,
|
|
557
591
|
## so it is looked for among what the file holds. A resource of another file is neither taken nor
|
|
@@ -249,6 +249,16 @@ func _members(
|
|
|
249
249
|
i += 1
|
|
250
250
|
stripped = (stripped + " " + GdhPatterns.without_comment(lines[i])).strip_edges()
|
|
251
251
|
header = GdhPatterns.without_annotations(stripped)
|
|
252
|
+
# A value that opens a triple-quoted string ends where the string does, and read from its
|
|
253
|
+
# first line alone it was the opening quotes and the text up to the line break.
|
|
254
|
+
if in_multiline_string and (header.begins_with("const ") or _declares_variable(header)):
|
|
255
|
+
while i + 1 < lines.size():
|
|
256
|
+
i += 1
|
|
257
|
+
stripped += "\n" + lines[i]
|
|
258
|
+
if _triple_quotes(lines[i]) % 2 == 1:
|
|
259
|
+
break
|
|
260
|
+
in_multiline_string = false
|
|
261
|
+
header = GdhPatterns.without_annotations(stripped)
|
|
252
262
|
var declaration: RegExMatch = GdhPatterns.declared_class(header)
|
|
253
263
|
var entry: Dictionary = {}
|
|
254
264
|
if declaration != null:
|
|
@@ -273,7 +283,7 @@ func _members(
|
|
|
273
283
|
elif header.begins_with("enum "):
|
|
274
284
|
entry = _parse_enum(header, at + 1)
|
|
275
285
|
own.enums.append(entry)
|
|
276
|
-
elif
|
|
286
|
+
elif _declares_variable(header):
|
|
277
287
|
# The whole line here, not the header: which annotations a variable carries is the
|
|
278
288
|
# answer rather than noise in front of it, and `@export_range(0, 1)` is the hint.
|
|
279
289
|
entry = _parse_variable(stripped, at + 1)
|
|
@@ -602,6 +612,10 @@ func _parse_variable(line: String, line_num: int) -> Dictionary:
|
|
|
602
612
|
return {"name": "", "line": line_num}
|
|
603
613
|
|
|
604
614
|
var var_text: String = line.substr(var_pos + 4).strip_edges()
|
|
615
|
+
# A colon closing the line opens the variable's setter and getter, and was read as the end of
|
|
616
|
+
# its default: `= Kind.ROUND:`.
|
|
617
|
+
if var_text.ends_with(":"):
|
|
618
|
+
var_text = var_text.left(-1).strip_edges()
|
|
605
619
|
var name: String = ""
|
|
606
620
|
var type_hint: String = ""
|
|
607
621
|
var default_value: String = ""
|
|
@@ -847,6 +861,10 @@ func _collect_dependencies(line: String, into: Array[String]) -> void:
|
|
|
847
861
|
|
|
848
862
|
|
|
849
863
|
# How many triple quotes of either kind [param line] holds.
|
|
864
|
+
static func _declares_variable(header: String) -> bool:
|
|
865
|
+
return header.begins_with("var ") or header.begins_with("static var ")
|
|
866
|
+
|
|
867
|
+
|
|
850
868
|
static func _triple_quotes(line: String) -> int:
|
|
851
869
|
return line.count('"""') + line.count("'''")
|
|
852
870
|
|