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.
@@ -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
- if script != null:
130
- answer["script"] = script.resource_path
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
- if not scene.kept(saved, loaded, _properties.read_at(node, property)):
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
- static func stroke_of(params: Dictionary) -> Variant:
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 "mouse_drag needs x, y, toX and toY: %s is missing." % key
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 answer: Dictionary = states(tree, state_machine_path, player, player_path)
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, state_machine_path: String, player: AnimationPlayer, player_path: String
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 != null and not found:
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 header.begins_with("var ") or header.begins_with("static var "):
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