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
|
@@ -10,6 +10,9 @@ const GdhSerialisation = preload("../serialisation.gd")
|
|
|
10
10
|
|
|
11
11
|
## What every emission meets when a connection cannot call its method.
|
|
12
12
|
const FAILS: String = ", so every emission fails to call it."
|
|
13
|
+
## The engine's CONNECT_INHERITED, which GDScript is not given: on a connection an instanced or
|
|
14
|
+
## inherited scene makes, as a scene holding one loads it.
|
|
15
|
+
const CONNECT_INHERITED: int = 32
|
|
13
16
|
## The names Object.ConnectFlags gives each bit. The number alone left a reader guessing that 2 is
|
|
14
17
|
## the persistence every connection in a saved scene carries.
|
|
15
18
|
const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
|
|
@@ -18,9 +21,7 @@ const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
|
|
|
18
21
|
Object.CONNECT_ONE_SHOT: "one_shot",
|
|
19
22
|
Object.CONNECT_REFERENCE_COUNTED: "reference_counted",
|
|
20
23
|
Object.CONNECT_APPEND_SOURCE_OBJECT: "append_source_object",
|
|
21
|
-
|
|
22
|
-
# makes inside itself, as a scene holding one loads it.
|
|
23
|
-
32: "inherited",
|
|
24
|
+
CONNECT_INHERITED: "inherited",
|
|
24
25
|
}
|
|
25
26
|
## The flags a caller may give: the engine's own CONNECT_INHERITED marks a connection an instanced
|
|
26
27
|
## scene makes, and is never one a scene makes for itself.
|
|
@@ -295,7 +296,9 @@ static func _misfit(source: Node, signal_name: String, callable: Callable, flags
|
|
|
295
296
|
break
|
|
296
297
|
if flags & Object.CONNECT_APPEND_SOURCE_OBJECT:
|
|
297
298
|
passed.append({"name": "source", "type": TYPE_OBJECT, "class_name": source.get_class()})
|
|
298
|
-
|
|
299
|
+
var emitted: int = passed.size()
|
|
300
|
+
var unbound: int = callable.get_unbound_arguments_count()
|
|
301
|
+
passed = passed.slice(0, maxi(emitted - unbound, 0))
|
|
299
302
|
var from_signal: int = passed.size()
|
|
300
303
|
for bound: Variant in callable.get_bound_arguments():
|
|
301
304
|
var bound_class: String = ""
|
|
@@ -303,43 +306,58 @@ static func _misfit(source: Node, signal_name: String, callable: Callable, flags
|
|
|
303
306
|
var object: Object = bound
|
|
304
307
|
bound_class = object.get_class()
|
|
305
308
|
passed.append({"name": "bound", "type": typeof(bound), "class_name": bound_class})
|
|
309
|
+
var binds: int = passed.size() - from_signal
|
|
310
|
+
|
|
311
|
+
# What the method is called with, said in the parts that make it: an answer counting the bound
|
|
312
|
+
# arguments among the signal's said the signal passed more than it declares.
|
|
313
|
+
var called: String = "%s passes %s" % [signal_name, _count(emitted)]
|
|
314
|
+
if unbound > 0 or binds > 0:
|
|
315
|
+
var changed: Array[String] = []
|
|
316
|
+
if unbound > 0:
|
|
317
|
+
changed.append("unbinds %d" % unbound)
|
|
318
|
+
if binds > 0:
|
|
319
|
+
changed.append("binds %d" % binds)
|
|
320
|
+
called += (
|
|
321
|
+
" and the connection %s, so %s is called with %s, and"
|
|
322
|
+
% [" and ".join(PackedStringArray(changed)), method, _count(passed.size())]
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
called += ", and " + method
|
|
306
326
|
|
|
307
327
|
var parameters: Array = taken.get("args", [])
|
|
308
328
|
var defaults: Array = taken.get("default_args", [])
|
|
309
329
|
var needed: int = parameters.size() - defaults.size()
|
|
310
330
|
if passed.size() < needed:
|
|
311
|
-
return "%s
|
|
331
|
+
return "%s needs %d%s" % [called, needed, FAILS]
|
|
312
332
|
if passed.size() > parameters.size():
|
|
313
333
|
var most: String = "none" if parameters.is_empty() else "at most %d" % parameters.size()
|
|
314
334
|
# Said with the misfit, since unbinds is what a connection to a method wanting less is for, and
|
|
315
|
-
# only when the extra arguments are the signal's: a
|
|
335
|
+
# only when the extra arguments are the signal's: a scene's connection unbinds the arguments
|
|
336
|
+
# the signal passes before it adds the bound ones, so a bound one is never dropped.
|
|
316
337
|
var extra: int = passed.size() - parameters.size()
|
|
317
338
|
var remedy: String = (
|
|
318
339
|
(
|
|
319
|
-
"
|
|
320
|
-
% [
|
|
321
|
-
extra + callable.get_unbound_arguments_count(),
|
|
322
|
-
"argument" if extra == 1 else "%d arguments" % extra,
|
|
323
|
-
"is" if extra == 1 else "are",
|
|
324
|
-
]
|
|
340
|
+
" Connect it with unbinds %d to leave out the last %s the signal passes."
|
|
341
|
+
% [extra + unbound, "argument" if extra == 1 else "%d arguments" % extra]
|
|
325
342
|
)
|
|
326
343
|
if extra <= from_signal
|
|
327
344
|
else ""
|
|
328
345
|
)
|
|
329
|
-
return
|
|
330
|
-
"%s passes %s, and %s takes %s%s%s"
|
|
331
|
-
% [signal_name, _count(passed.size()), method, most, FAILS, remedy]
|
|
332
|
-
)
|
|
346
|
+
return "%s takes %s%s%s" % [called, most, FAILS, remedy]
|
|
333
347
|
for at: int in passed.size():
|
|
334
348
|
var given: Dictionary = passed[at]
|
|
335
349
|
var parameter: Dictionary = parameters[at]
|
|
336
350
|
if not _unrelated(given, parameter):
|
|
337
351
|
continue
|
|
352
|
+
var whose: String = (
|
|
353
|
+
"%s passes argument %d" % [signal_name, at + 1]
|
|
354
|
+
if at < from_signal
|
|
355
|
+
else "the connection binds argument %d" % (at + 1)
|
|
356
|
+
)
|
|
338
357
|
return (
|
|
339
|
-
"%s
|
|
358
|
+
"%s as %s, and %s takes it as %s: %s%s"
|
|
340
359
|
% [
|
|
341
|
-
|
|
342
|
-
at + 1,
|
|
360
|
+
whose,
|
|
343
361
|
_type_named(given),
|
|
344
362
|
method,
|
|
345
363
|
str(parameter.get("name", "")),
|
|
@@ -380,6 +398,25 @@ static func _type_named(declared: Dictionary) -> String:
|
|
|
380
398
|
return type_string(type)
|
|
381
399
|
|
|
382
400
|
|
|
401
|
+
## The scene that makes the connection [param source] holds with [param flags], as a path, or "" for
|
|
402
|
+
## one this scene makes. The engine marks such a connection inherited as the scene holding it loads:
|
|
403
|
+
## from an instance, the scene instanced, and from the root of an inherited scene, the one it
|
|
404
|
+
## inherits.
|
|
405
|
+
static func _made_in(scene: GdhSceneFile, source: Node, flags: int) -> String:
|
|
406
|
+
if not flags & CONNECT_INHERITED:
|
|
407
|
+
return ""
|
|
408
|
+
if source != scene.root and not source.scene_file_path.is_empty():
|
|
409
|
+
return source.scene_file_path
|
|
410
|
+
var holder: Node = source if source == scene.root else source.owner
|
|
411
|
+
while holder != null and holder != scene.root and holder.scene_file_path.is_empty():
|
|
412
|
+
holder = holder.owner
|
|
413
|
+
if holder != null and holder != scene.root:
|
|
414
|
+
return holder.scene_file_path
|
|
415
|
+
var packed: PackedScene = load(scene.path) as PackedScene
|
|
416
|
+
var base: SceneState = packed.get_state().get_base_scene_state() if packed != null else null
|
|
417
|
+
return base.get_path() if base != null else "a scene this one inherits"
|
|
418
|
+
|
|
419
|
+
|
|
383
420
|
static func _count(arguments: int) -> String:
|
|
384
421
|
if arguments == 0:
|
|
385
422
|
return "no arguments"
|
|
@@ -428,6 +465,21 @@ func disconnect_signal(args: Dictionary) -> Dictionary:
|
|
|
428
465
|
% [scene_path, said_from, signal_name, said_to, method_name]
|
|
429
466
|
)
|
|
430
467
|
)
|
|
468
|
+
var made_in: String = _made_in(scene, source, GdhRead.as_int(held["flags"]))
|
|
469
|
+
if not made_in.is_empty():
|
|
470
|
+
var named_from: String = str(source.name) if source == scene.root else from
|
|
471
|
+
var named_to: String = str(target.name) if target == scene.root else to
|
|
472
|
+
var made_by: String = (
|
|
473
|
+
"The connection from %s.%s to %s.%s is made by %s"
|
|
474
|
+
% [named_from, signal_name, named_to, method_name, made_in]
|
|
475
|
+
)
|
|
476
|
+
return scene.refuse(
|
|
477
|
+
(
|
|
478
|
+
made_by
|
|
479
|
+
+ ", and this scene saves nothing about it: disconnect it there, which changes every"
|
|
480
|
+
+ " scene using it."
|
|
481
|
+
)
|
|
482
|
+
)
|
|
431
483
|
var callable: Callable = held["callable"]
|
|
432
484
|
source.disconnect(signal_name, callable)
|
|
433
485
|
|
|
@@ -498,6 +550,9 @@ func list_connections(args: Dictionary) -> Dictionary:
|
|
|
498
550
|
if target_obj is Node:
|
|
499
551
|
target_path = str(root.get_path_to(target_obj as Node))
|
|
500
552
|
editable = editable and scene.foreign(target_obj as Node).is_empty()
|
|
553
|
+
# A connection an instanced scene makes on its own root sits on a node this scene owns,
|
|
554
|
+
# so the ends alone read it as one this scene could change.
|
|
555
|
+
var made_in: String = _made_in(scene, node, flags)
|
|
501
556
|
var listed: Dictionary = {
|
|
502
557
|
"sourceNodePath": path,
|
|
503
558
|
"signalName": signal_name,
|
|
@@ -505,11 +560,11 @@ func list_connections(args: Dictionary) -> Dictionary:
|
|
|
505
560
|
"methodName": str(callable.get_method()),
|
|
506
561
|
"flags": flags,
|
|
507
562
|
"flagNames": flag_names(flags),
|
|
508
|
-
|
|
509
|
-
# scene can change.
|
|
510
|
-
"editable": editable,
|
|
563
|
+
"editable": editable and made_in.is_empty(),
|
|
511
564
|
"methodFound": target_obj != null and target_obj.has_method(callable.get_method()),
|
|
512
565
|
}
|
|
566
|
+
if not made_in.is_empty():
|
|
567
|
+
listed["madeIn"] = made_in
|
|
513
568
|
var bound: Dictionary = {
|
|
514
569
|
"binds": callable.get_bound_arguments(),
|
|
515
570
|
"unbinds": callable.get_unbound_arguments_count(),
|
|
@@ -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
|
|
@@ -112,11 +112,15 @@ static func field_before(focused: Control) -> Dictionary:
|
|
|
112
112
|
## Whether [param focused], read as [param before], stopped being edited, as a [LineEdit] does when
|
|
113
113
|
## it is submitted: it keeps the focus and drops every key after, so a caller reading only the focus
|
|
114
114
|
## typed next into nothing.
|
|
115
|
-
|
|
115
|
+
##
|
|
116
|
+
## [param focused] is a Variant because the key can free it, as Escape closing the panel a field is
|
|
117
|
+
## on does, and a parameter typed Control refuses a freed object before the check here can run.
|
|
118
|
+
static func ended_editing(focused: Variant, before: Dictionary) -> bool:
|
|
116
119
|
var was: bool = before.get("editing", false)
|
|
117
120
|
if not was or not is_instance_valid(focused):
|
|
118
121
|
return false
|
|
119
|
-
var
|
|
122
|
+
var held: Object = focused
|
|
123
|
+
var field: LineEdit = held as LineEdit
|
|
120
124
|
return field != null and not field.is_editing()
|
|
121
125
|
|
|
122
126
|
|
|
@@ -124,15 +128,18 @@ static func ended_editing(focused: Control, before: Dictionary) -> bool:
|
|
|
124
128
|
## typed_into and holds where its text changed, and editing_ended where it stopped being edited. A
|
|
125
129
|
## field being edited takes a printable key as typing and keeps it from the game's _unhandled_input,
|
|
126
130
|
## so Space into a name field pressed no jump, and an answer naming only the focus did not say why.
|
|
127
|
-
|
|
131
|
+
## A Variant for the reason [method ended_editing] gives.
|
|
132
|
+
static func field_noted(said: Dictionary, focused: Variant, before: Dictionary) -> Dictionary:
|
|
128
133
|
if before.is_empty() or not is_instance_valid(focused):
|
|
129
134
|
return said
|
|
130
|
-
var
|
|
131
|
-
var
|
|
135
|
+
var held: Object = focused
|
|
136
|
+
var control: Control = held as Control
|
|
137
|
+
var field: LineEdit = control as LineEdit
|
|
138
|
+
var now: String = field.text if field != null else (control as TextEdit).text
|
|
132
139
|
if now != before["text"]:
|
|
133
|
-
said["typed_into"] = str(
|
|
134
|
-
said["holds"] = _what_it_holds(
|
|
135
|
-
if ended_editing(
|
|
140
|
+
said["typed_into"] = str(control.get_path())
|
|
141
|
+
said["holds"] = _what_it_holds(control)
|
|
142
|
+
if ended_editing(control, before):
|
|
136
143
|
said["editing_ended"] = true
|
|
137
144
|
return said
|
|
138
145
|
|
|
@@ -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] = []
|
|
@@ -428,7 +428,10 @@ func _declared_class_name(path: String) -> String:
|
|
|
428
428
|
func _usage_files(kinds: Array) -> Array[String]:
|
|
429
429
|
var found: Array[String] = []
|
|
430
430
|
for kind: Variant in kinds:
|
|
431
|
-
|
|
431
|
+
# As project_search reads an extension: ".tscn" searched no file, and the answer said
|
|
432
|
+
# nothing used a script its scene attaches.
|
|
433
|
+
var named: String = str(kind).trim_prefix(".").to_lower()
|
|
434
|
+
match named:
|
|
432
435
|
"godot":
|
|
433
436
|
found.append("res://project.godot")
|
|
434
437
|
"cfg":
|
|
@@ -436,7 +439,7 @@ func _usage_files(kinds: Array) -> Array[String]:
|
|
|
436
439
|
if FileAccess.file_exists("res://export_presets.cfg"):
|
|
437
440
|
found.append("res://export_presets.cfg")
|
|
438
441
|
_:
|
|
439
|
-
found.append_array(_files.find_files("res://", "." +
|
|
442
|
+
found.append_array(_files.find_files("res://", "." + named))
|
|
440
443
|
return found
|
|
441
444
|
|
|
442
445
|
|
|
@@ -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
|