gdharness 1.3.9 → 1.3.11
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 +4740 -3212
- package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
- package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +102 -18
- package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
- package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
- package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
- package/build/godot/addons/gdharness_editor/reading.gd +51 -1
- package/build/godot/addons/gdharness_editor/scene_file.gd +77 -140
- package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
- package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +11 -22
- package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
- package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +28 -5
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +39 -20
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -362
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +147 -492
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -3
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +79 -5
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +183 -37
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
- package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +51 -1
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +185 -30
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +86 -79
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +309 -22
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +163 -20
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +5 -2
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +146 -5
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +11 -22
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
- package/build/godot/operations/animation_reads.gd +115 -4
- package/build/godot/operations/audio_buses.gd +93 -7
- package/build/godot/operations/classdb_queries.gd +119 -7
- package/build/godot/operations/declared_values.gd +272 -0
- package/build/godot/operations/dependencies.gd +154 -41
- package/build/godot/operations/editor_tools.gd +22 -14
- package/build/godot/operations/export_presets.gd +602 -4
- package/build/godot/operations/file_reads.gd +102 -18
- package/build/godot/operations/gdscript_authoring.gd +11 -1
- package/build/godot/operations/godot_operations.gd +16 -2
- package/build/godot/operations/import_pipeline.gd +20 -221
- package/build/godot/operations/input_actions.gd +108 -47
- package/build/godot/operations/project_config.gd +83 -69
- package/build/godot/operations/project_diagnostics.gd +4 -0
- package/build/godot/operations/project_validation.gd +266 -0
- package/build/godot/operations/property_paths.gd +89 -16
- package/build/godot/operations/property_values.gd +180 -121
- package/build/godot/operations/reading.gd +51 -1
- package/build/godot/operations/scene_connections.gd +222 -0
- package/build/godot/operations/serialisation.gd +11 -22
- package/build/godot/operations/setting_reads.gd +121 -0
- package/build/godot/operations/value_shapes.gd +128 -1
- package/build/godot/tests/listed.gd +73 -13
- package/build/keeper.js +1371 -706
- package/package.json +1 -1
|
@@ -125,6 +125,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
125
125
|
var root: Node = reached["node"]
|
|
126
126
|
|
|
127
127
|
var found: Array[Dictionary] = []
|
|
128
|
+
var found_nodes: Array[Node] = []
|
|
128
129
|
var pending: Array[Node] = [root]
|
|
129
130
|
var truncated: bool = false
|
|
130
131
|
# Counted while the tree is already being walked, for the answer below: how many nodes this
|
|
@@ -148,24 +149,28 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
148
149
|
var nearly_hidden: int = 0
|
|
149
150
|
var nearly_said_hidden: int = 0
|
|
150
151
|
var hidden: int = 0
|
|
152
|
+
var out_of_view: int = 0
|
|
151
153
|
while not pending.is_empty():
|
|
152
154
|
var node: Node = pending.pop_front()
|
|
153
155
|
var rest: bool = _matches_apart_from_name(node, wanted)
|
|
154
156
|
if rest and _named(node, names):
|
|
155
157
|
if not include_hidden and not shown(node):
|
|
156
158
|
hidden += 1
|
|
159
|
+
elif not include_hidden and not GdhScreen.in_view(node):
|
|
160
|
+
out_of_view += 1
|
|
157
161
|
elif found.size() >= limit:
|
|
158
162
|
truncated = true
|
|
159
163
|
break
|
|
160
164
|
else:
|
|
161
165
|
found.append(_found(node, wanted_property))
|
|
166
|
+
found_nodes.append(node)
|
|
162
167
|
elif rest and _contains_any(str(node.name), plain_names):
|
|
163
|
-
if include_hidden or
|
|
168
|
+
if include_hidden or readable(node):
|
|
164
169
|
nearly += 1
|
|
165
170
|
else:
|
|
166
171
|
nearly_hidden += 1
|
|
167
172
|
elif not widened_says.is_empty() and _named(node, names) and _matches_apart_from_name(node, widened):
|
|
168
|
-
if include_hidden or
|
|
173
|
+
if include_hidden or readable(node):
|
|
169
174
|
nearly_said += 1
|
|
170
175
|
else:
|
|
171
176
|
nearly_said_hidden += 1
|
|
@@ -181,6 +186,39 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
181
186
|
|
|
182
187
|
var answer: Dictionary = {"type": "nodes", "count": found.size(), "truncated": truncated, "nodes": found}
|
|
183
188
|
var notes: Array[String] = []
|
|
189
|
+
# Each still listed, since a caller may mean one of them, and marked, so one sign drawn as a label
|
|
190
|
+
# and its outline copies is not read as four signs. The outermost match it copies, since a find
|
|
191
|
+
# under one of the copies has left the label they all copy outside it.
|
|
192
|
+
var copies: int = 0
|
|
193
|
+
for index: int in range(found_nodes.size()):
|
|
194
|
+
var at: int = -1
|
|
195
|
+
for original: Node in GdhWords.copied_through(found_nodes[index]):
|
|
196
|
+
var matched: int = found_nodes.find(original)
|
|
197
|
+
if matched != -1:
|
|
198
|
+
at = matched
|
|
199
|
+
if at != -1:
|
|
200
|
+
found[index]["copyOf"] = found[at]["path"]
|
|
201
|
+
copies += 1
|
|
202
|
+
if copies > 0:
|
|
203
|
+
answer["distinct"] = found.size() - copies
|
|
204
|
+
(
|
|
205
|
+
notes
|
|
206
|
+
. append(
|
|
207
|
+
(
|
|
208
|
+
(
|
|
209
|
+
"%d of the %d matches %s drawn under a match saying the same words, as an outline or a"
|
|
210
|
+
+ " glow is drawn, and %s it under copyOf: %d distinct"
|
|
211
|
+
)
|
|
212
|
+
% [
|
|
213
|
+
copies,
|
|
214
|
+
found.size(),
|
|
215
|
+
"is a copy" if copies == 1 else "are copies",
|
|
216
|
+
"names" if copies == 1 else "name",
|
|
217
|
+
found.size() - copies,
|
|
218
|
+
]
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
)
|
|
184
222
|
# Nothing found is the one answer that cannot be told apart from having asked the wrong
|
|
185
223
|
# question, and a name written without a wildcard is the way an agent writes "contains".
|
|
186
224
|
# Said only when it changes the answer, so a genuine nothing stays a plain nothing.
|
|
@@ -200,7 +238,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
200
238
|
'name is matched as a glob against the whole name; %d %snode %s %s, which "%s"%s would find'
|
|
201
239
|
% [
|
|
202
240
|
named_near,
|
|
203
|
-
"" if nearly > 0 else "hidden ",
|
|
241
|
+
"" if nearly > 0 else "hidden or out-of-view ",
|
|
204
242
|
holding,
|
|
205
243
|
" or ".join(PackedStringArray(near_words)),
|
|
206
244
|
GdhSays.joined_at_bars(opened),
|
|
@@ -220,7 +258,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
220
258
|
widened_says,
|
|
221
259
|
"" if nearly_said > 0 else " with includeHidden true",
|
|
222
260
|
said_near,
|
|
223
|
-
"" if nearly_said > 0 else "hidden ",
|
|
261
|
+
"" if nearly_said > 0 else "hidden or out-of-view ",
|
|
224
262
|
"" if said_near == 1 else "s",
|
|
225
263
|
]
|
|
226
264
|
)
|
|
@@ -236,6 +274,17 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
236
274
|
% [hidden, "" if hidden == 1 else "s"]
|
|
237
275
|
)
|
|
238
276
|
)
|
|
277
|
+
if out_of_view > 0:
|
|
278
|
+
answer["out_of_view"] = out_of_view
|
|
279
|
+
notes.append(
|
|
280
|
+
(
|
|
281
|
+
(
|
|
282
|
+
"%d matching node%s drawn outside the viewport or scrolled out of a ScrollContainer"
|
|
283
|
+
+ " and left out; includeHidden true answers with them too"
|
|
284
|
+
)
|
|
285
|
+
% [out_of_view, "" if out_of_view == 1 else "s"]
|
|
286
|
+
)
|
|
287
|
+
)
|
|
239
288
|
if not notes.is_empty():
|
|
240
289
|
answer["note"] = " ".join(notes)
|
|
241
290
|
return answer
|
|
@@ -268,6 +317,13 @@ static func shown(node: Node) -> bool:
|
|
|
268
317
|
return parent == null or shown(parent)
|
|
269
318
|
|
|
270
319
|
|
|
320
|
+
## Whether the player can read [param node]: [method shown], and drawn inside its viewport and every
|
|
321
|
+
## ScrollContainer holding it, see [method GdhScreen.in_view]. A click by words asks [method shown]
|
|
322
|
+
## alone, since it scrolls what it presses into view.
|
|
323
|
+
static func readable(node: Node) -> bool:
|
|
324
|
+
return shown(node) and GdhScreen.in_view(node)
|
|
325
|
+
|
|
326
|
+
|
|
271
327
|
## Whether the canvas [param item] is drawn on is showing: its layer, and the windows around it.
|
|
272
328
|
static func _on_a_shown_canvas(item: CanvasItem) -> bool:
|
|
273
329
|
var walk: Node = item.get_parent()
|
|
@@ -426,15 +482,46 @@ func read_text(params: Dictionary) -> Dictionary:
|
|
|
426
482
|
# and a bare `truncated: true` was looked straight past twice before the limit was suspected.
|
|
427
483
|
# A number is what sends somebody back.
|
|
428
484
|
var lines: Array[String] = []
|
|
429
|
-
var
|
|
430
|
-
|
|
485
|
+
var tally: Dictionary[String, int] = {"omitted": 0, "out_of_view": 0, "copies": 0}
|
|
486
|
+
_read_into(root, include_hidden, limit, lines, tally)
|
|
487
|
+
var answer: Dictionary = {
|
|
431
488
|
"type": "text",
|
|
432
489
|
"root": root_path,
|
|
433
490
|
"lines": lines,
|
|
434
491
|
"count": lines.size(),
|
|
435
|
-
"truncated":
|
|
436
|
-
"omitted":
|
|
492
|
+
"truncated": tally["omitted"] > 0,
|
|
493
|
+
"omitted": tally["omitted"],
|
|
437
494
|
}
|
|
495
|
+
var notes: Array[String] = []
|
|
496
|
+
if tally["out_of_view"] > 0:
|
|
497
|
+
answer["out_of_view"] = tally["out_of_view"]
|
|
498
|
+
notes.append(
|
|
499
|
+
(
|
|
500
|
+
(
|
|
501
|
+
"%d line%s drawn outside the viewport or scrolled out of a ScrollContainer, where the"
|
|
502
|
+
+ " player cannot read them, left out; includeHidden true answers with them too"
|
|
503
|
+
)
|
|
504
|
+
% [tally["out_of_view"], "" if tally["out_of_view"] == 1 else "s"]
|
|
505
|
+
)
|
|
506
|
+
)
|
|
507
|
+
if tally["copies"] > 0:
|
|
508
|
+
answer["copies"] = tally["copies"]
|
|
509
|
+
notes.append(
|
|
510
|
+
(
|
|
511
|
+
(
|
|
512
|
+
"%d line%s drawn as a copy under a node saying the same words, as an outline or a glow"
|
|
513
|
+
+ " is drawn, read once with the words %s"
|
|
514
|
+
)
|
|
515
|
+
% [
|
|
516
|
+
tally["copies"],
|
|
517
|
+
"" if tally["copies"] == 1 else "s",
|
|
518
|
+
"it copies" if tally["copies"] == 1 else "they copy"
|
|
519
|
+
]
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
if not notes.is_empty():
|
|
523
|
+
answer["note"] = " ".join(notes)
|
|
524
|
+
return answer
|
|
438
525
|
|
|
439
526
|
|
|
440
527
|
## Walks [param node] depth first, which is the order the screen is laid out in and the order a
|
|
@@ -445,22 +532,33 @@ func read_text(params: Dictionary) -> Dictionary:
|
|
|
445
532
|
## screen full of forms answered with every label on it and none of the values in it. The hidden
|
|
446
533
|
## check covers a [Window] for the same walk: a dropdown's popup is a child that is not drawn
|
|
447
534
|
## until it is opened, and reading a closed menu would put every item on the screen.
|
|
448
|
-
## Fills [param into] up to [param most] lines and
|
|
449
|
-
## the subtree says,
|
|
535
|
+
## Fills [param into] up to [param most] lines and counts in [param tally] how many further lines the
|
|
536
|
+
## rest of the subtree says, under omitted, and how many it says out of view, under out_of_view, which
|
|
537
|
+
## is what the caller is told rather than left to infer.
|
|
450
538
|
##
|
|
451
539
|
## Asked of each node rather than cut off at the first hidden one, since a hidden panel does not
|
|
452
540
|
## hide a label reached through a plain Node: see [method shown].
|
|
453
|
-
func _read_into(
|
|
454
|
-
|
|
541
|
+
func _read_into(
|
|
542
|
+
node: Node, include_hidden: bool, most: int, into: Array[String], tally: Dictionary[String, int]
|
|
543
|
+
) -> void:
|
|
455
544
|
if include_hidden or shown(node):
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
545
|
+
var said: Array[String] = GdhWords.lines_said_by(node)
|
|
546
|
+
# A copy drawn under the words it copies, as an outline or a glow is, reads once. Only while
|
|
547
|
+
# what it copies is itself read, so an original hidden or drawn out of view does not take the
|
|
548
|
+
# words with it.
|
|
549
|
+
var original: Node = GdhWords.copied_from(node) if not said.is_empty() else null
|
|
550
|
+
if original != null and (include_hidden or readable(original)):
|
|
551
|
+
tally["copies"] += said.size()
|
|
552
|
+
elif not include_hidden and not GdhScreen.in_view(node):
|
|
553
|
+
tally["out_of_view"] += said.size()
|
|
554
|
+
else:
|
|
555
|
+
for line: String in said:
|
|
556
|
+
if into.size() < most:
|
|
557
|
+
into.append(line)
|
|
558
|
+
else:
|
|
559
|
+
tally["omitted"] += 1
|
|
461
560
|
for child: Node in node.get_children(true):
|
|
462
|
-
|
|
463
|
-
return left_out
|
|
561
|
+
_read_into(child, include_hidden, most, into, tally)
|
|
464
562
|
|
|
465
563
|
|
|
466
564
|
## Where a node is on screen: a Control's rectangle, a Node2D's position, or the place a 3D node
|
|
@@ -488,6 +586,7 @@ func get_rect(params: Dictionary) -> Dictionary:
|
|
|
488
586
|
"type": "rect",
|
|
489
587
|
"path": node_path,
|
|
490
588
|
"visible": control.is_visible_in_tree(),
|
|
589
|
+
"in_view": GdhScreen.in_view(control),
|
|
491
590
|
"canvas": _values.serialize(canvas_rect),
|
|
492
591
|
"window":
|
|
493
592
|
_values.serialize(
|
|
@@ -501,13 +600,20 @@ func get_rect(params: Dictionary) -> Dictionary:
|
|
|
501
600
|
var item: Node2D = node
|
|
502
601
|
var canvas_position: Vector2 = item.get_global_transform_with_canvas().origin
|
|
503
602
|
var window_position: Vector2 = GdhScreen.in_window(item.get_viewport(), canvas_position)
|
|
504
|
-
|
|
603
|
+
var at: Dictionary = {
|
|
505
604
|
"type": "point",
|
|
506
605
|
"path": node_path,
|
|
507
606
|
"visible": item.is_visible_in_tree(),
|
|
508
607
|
"canvas": _values.serialize(canvas_position),
|
|
509
608
|
"window": _values.serialize(window_position),
|
|
510
609
|
}
|
|
610
|
+
for local: Rect2 in _drawn_rect(item):
|
|
611
|
+
var covered: Rect2 = item.get_global_transform_with_canvas() * local
|
|
612
|
+
at["covers"] = {
|
|
613
|
+
"canvas": _values.serialize(covered),
|
|
614
|
+
"window": _values.serialize(GdhScreen.rect_in_window(item.get_viewport(), covered)),
|
|
615
|
+
}
|
|
616
|
+
return at
|
|
511
617
|
if node is Node3D:
|
|
512
618
|
var spatial: Node3D = node
|
|
513
619
|
return _in_the_frame(node_path, spatial)
|
|
@@ -516,6 +622,43 @@ func get_rect(params: Dictionary) -> Dictionary:
|
|
|
516
622
|
}
|
|
517
623
|
|
|
518
624
|
|
|
625
|
+
## The rectangle a 2D node's own geometry takes up in its own coordinates, as the one entry of the
|
|
626
|
+
## list, which is empty for a node that has none of its own: a sprite's texture, a collision shape's
|
|
627
|
+
## shape, the points of a polygon or a line. A Node2D answered only where it stands, so the extent of
|
|
628
|
+
## a floor or a sprite to aim inside was a read of its shape and some arithmetic.
|
|
629
|
+
static func _drawn_rect(item: Node2D) -> Array[Rect2]:
|
|
630
|
+
var drawn: Array[Rect2] = []
|
|
631
|
+
var sprite: Sprite2D = item as Sprite2D
|
|
632
|
+
if sprite != null:
|
|
633
|
+
if sprite.texture != null:
|
|
634
|
+
drawn.append(sprite.get_rect())
|
|
635
|
+
return drawn
|
|
636
|
+
var shaped: CollisionShape2D = item as CollisionShape2D
|
|
637
|
+
if shaped != null:
|
|
638
|
+
if shaped.shape != null:
|
|
639
|
+
drawn.append(shaped.shape.get_rect())
|
|
640
|
+
return drawn
|
|
641
|
+
var points: PackedVector2Array = []
|
|
642
|
+
var offset: Vector2 = Vector2.ZERO
|
|
643
|
+
if item is Polygon2D:
|
|
644
|
+
var polygon: Polygon2D = item
|
|
645
|
+
points = polygon.polygon
|
|
646
|
+
offset = polygon.offset
|
|
647
|
+
elif item is CollisionPolygon2D:
|
|
648
|
+
var outline: CollisionPolygon2D = item
|
|
649
|
+
points = outline.polygon
|
|
650
|
+
elif item is Line2D:
|
|
651
|
+
var line: Line2D = item
|
|
652
|
+
points = line.points
|
|
653
|
+
if points.is_empty():
|
|
654
|
+
return drawn
|
|
655
|
+
var bounds: Rect2 = Rect2(points[0] + offset, Vector2.ZERO)
|
|
656
|
+
for point: Vector2 in points:
|
|
657
|
+
bounds = bounds.expand(point + offset)
|
|
658
|
+
drawn.append(bounds)
|
|
659
|
+
return drawn
|
|
660
|
+
|
|
661
|
+
|
|
519
662
|
## Where a 3D node is in the frame drawing it: the point to aim at, and the rectangle its own
|
|
520
663
|
## geometry covers, each in canvas coordinates and in window pixels.
|
|
521
664
|
##
|
|
@@ -29,9 +29,13 @@ static func steps_out(viewport: Viewport, point: Vector2) -> Array[Dictionary]:
|
|
|
29
29
|
var out: Array[Dictionary] = [{"viewport": viewport, "point": point, "through": null}]
|
|
30
30
|
while true:
|
|
31
31
|
var window: Window = viewport as Window
|
|
32
|
-
if window != null and window.is_embedded()
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
var host: Viewport = embedder_of(window) if window != null and window.is_embedded() else null
|
|
33
|
+
if host != null:
|
|
34
|
+
# A window's canvas is drawn into its embedder through its own final transform, which is
|
|
35
|
+
# where a content scale goes: one at 4 drew a button at four times the point it was clicked
|
|
36
|
+
# at, and the click missed it.
|
|
37
|
+
point = Vector2(window.position) + window.get_final_transform() * point
|
|
38
|
+
viewport = host
|
|
35
39
|
out.append({"viewport": viewport, "point": point, "through": window})
|
|
36
40
|
continue
|
|
37
41
|
var shown_by: SubViewportContainer = viewport.get_parent() as SubViewportContainer
|
|
@@ -45,6 +49,19 @@ static func steps_out(viewport: Viewport, point: Vector2) -> Array[Dictionary]:
|
|
|
45
49
|
return out
|
|
46
50
|
|
|
47
51
|
|
|
52
|
+
## The viewport [param window] is embedded in and placed by, found as the engine finds it, which
|
|
53
|
+
## does not offer it to scripts: the nearest viewport above that embeds subwindows. That is not
|
|
54
|
+
## always its parent's: a window under a window that does not embed is embedded in the one above.
|
|
55
|
+
static func embedder_of(window: Window) -> Viewport:
|
|
56
|
+
var above: Node = window.get_parent()
|
|
57
|
+
# A viewport's get_viewport is itself, so a window's parent that is one is asked first.
|
|
58
|
+
var viewport: Viewport = above.get_viewport() if above != null else null
|
|
59
|
+
while viewport != null and not viewport.is_embedding_subwindows():
|
|
60
|
+
above = viewport.get_parent()
|
|
61
|
+
viewport = above.get_viewport() if above != null else null
|
|
62
|
+
return viewport
|
|
63
|
+
|
|
64
|
+
|
|
48
65
|
## Where [param point], in [param viewport]'s own coordinates, is in the pixels of the window the
|
|
49
66
|
## game is shown in, which is what a mouse event names.
|
|
50
67
|
static func in_window(viewport: Viewport, point: Vector2) -> Vector2:
|
|
@@ -60,3 +77,32 @@ static func rect_in_window(viewport: Viewport, rect: Rect2) -> Rect2:
|
|
|
60
77
|
var start: Vector2 = in_window(viewport, rect.position)
|
|
61
78
|
var end: Vector2 = in_window(viewport, rect.end)
|
|
62
79
|
return Rect2(start.min(end), (end - start).abs())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
## Whether any of [param node] is drawn where the player can see it: inside its viewport, and inside
|
|
83
|
+
## every ScrollContainer between it and the viewport, each of which clips what it holds. A label
|
|
84
|
+
## placed past the window's edge was read out as written on the screen. Only a Control has a box to
|
|
85
|
+
## ask about, so anything else is in view.
|
|
86
|
+
static func in_view(node: Node) -> bool:
|
|
87
|
+
var control: Control = node as Control
|
|
88
|
+
return control == null or shown_part(control) != null
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
## The part of [param control] the player can see, in its viewport's canvas, or null when there is
|
|
92
|
+
## none: its drawn box cut to the viewport and to every ScrollContainer between, each cut taken from
|
|
93
|
+
## what the last one left. A control in view of an inner list and of the outer one can still sit
|
|
94
|
+
## where the two do not overlap.
|
|
95
|
+
static func shown_part(control: Control) -> Variant:
|
|
96
|
+
var part: Rect2 = control.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, control.size)
|
|
97
|
+
var clips: Array[Rect2] = [control.get_viewport().get_visible_rect()]
|
|
98
|
+
var walking: Node = control.get_parent()
|
|
99
|
+
while walking != null and not (walking is Viewport):
|
|
100
|
+
var holder: ScrollContainer = walking as ScrollContainer
|
|
101
|
+
if holder != null:
|
|
102
|
+
clips.append(holder.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, holder.size))
|
|
103
|
+
walking = walking.get_parent()
|
|
104
|
+
for clip: Rect2 in clips:
|
|
105
|
+
if not part.intersects(clip, true):
|
|
106
|
+
return null
|
|
107
|
+
part = part.intersection(clip)
|
|
108
|
+
return part
|
|
@@ -762,9 +762,12 @@ static func _moved_by(node: Node, moves: Dictionary[ScrollContainer, Vector2]) -
|
|
|
762
762
|
## target's own viewport, a confirmation over a settings dialog did not cover the dialog's OK, and
|
|
763
763
|
## a click by words tied it with the confirmation's own.
|
|
764
764
|
static func _window_over(target: Control, point: Vector2, window: Window) -> bool:
|
|
765
|
-
|
|
765
|
+
# Not passed over for holding the target in the tree: a window under an embedded window is
|
|
766
|
+
# embedded beside it, not in it, so the one holding it can be stacked over it. The target's own
|
|
767
|
+
# window is the step out below, and is never over it.
|
|
768
|
+
if window.get_parent() == null:
|
|
766
769
|
return false
|
|
767
|
-
var host: Viewport =
|
|
770
|
+
var host: Viewport = GdhScreen.embedder_of(window)
|
|
768
771
|
for step: Dictionary in GdhScreen.steps_out(target.get_viewport(), point):
|
|
769
772
|
if step["viewport"] != host:
|
|
770
773
|
continue
|
|
@@ -8,6 +8,7 @@ extends RefCounted
|
|
|
8
8
|
## only true of a running game: reaching a node by path, and fitting an argument to the type the
|
|
9
9
|
## method it is going to declares.
|
|
10
10
|
|
|
11
|
+
const GdhDeclared = preload("declared_values.gd")
|
|
11
12
|
const GdhRead = preload("reading.gd")
|
|
12
13
|
const GdhSerialisation = preload("serialisation.gd")
|
|
13
14
|
const GdhShapes = preload("value_shapes.gd")
|
|
@@ -80,9 +81,18 @@ static func node_at(root: Node, path: String) -> Dictionary:
|
|
|
80
81
|
|
|
81
82
|
## "Node not found" for [param path], with the deepest node of it the game has and the children that
|
|
82
83
|
## node has instead, as a scene's refusal names them: a mistyped step was otherwise found by reading
|
|
83
|
-
## the whole tree.
|
|
84
|
+
## the whole tree. A path written from the current scene rather than from the root, as a scene's
|
|
85
|
+
## tools take one, is given the path that reaches it.
|
|
84
86
|
static func _not_found(root: Node, path: String) -> String:
|
|
85
87
|
var said: String = "Node not found: " + path
|
|
88
|
+
var scene: Node = root.get_tree().current_scene if root.is_inside_tree() else null
|
|
89
|
+
if not path.begins_with("/") and scene != null:
|
|
90
|
+
var meant: Node = scene.get_node_or_null(path)
|
|
91
|
+
if meant != null:
|
|
92
|
+
return (
|
|
93
|
+
"%s. Paths here go from the root, /root, not from the current scene, so this one is %s."
|
|
94
|
+
% [said, str(meant.get_path())]
|
|
95
|
+
)
|
|
86
96
|
var parts: PackedStringArray = path.split("/", false)
|
|
87
97
|
var at: Node = root
|
|
88
98
|
var step: int = 0
|
|
@@ -220,13 +230,43 @@ static func no_action(action: String) -> String:
|
|
|
220
230
|
## counting the action counted the key, and the answer had said only that a key went in.
|
|
221
231
|
static func actions_of(event: InputEvent) -> Array[String]:
|
|
222
232
|
var named: Array[String] = []
|
|
233
|
+
# A motion matches every action on its axis, either way and at any distance, which would name
|
|
234
|
+
# ui_left for a stick held right; Input presses only those its direction reaches past their
|
|
235
|
+
# deadzone.
|
|
236
|
+
var moved: bool = event is InputEventJoypadMotion
|
|
223
237
|
for action: StringName in InputMap.get_actions():
|
|
224
|
-
if InputMap.event_is_action(event, action):
|
|
238
|
+
if event.is_action_pressed(action) if moved else InputMap.event_is_action(event, action):
|
|
225
239
|
named.append(str(action))
|
|
226
240
|
named.sort()
|
|
227
241
|
return named
|
|
228
242
|
|
|
229
243
|
|
|
244
|
+
## The path of the Control holding the keyboard focus in [param host]'s viewport, or "" when none
|
|
245
|
+
## does. Read before an event goes in, since pressing a focused button can move the focus.
|
|
246
|
+
static func focus_path(host: Node) -> String:
|
|
247
|
+
var holding: Control = host.get_viewport().gui_get_focus_owner()
|
|
248
|
+
return "" if holding == null else str(holding.get_path())
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
## [param said] with [param focused], the Control that had the focus, under focused. A key, an action
|
|
252
|
+
## or a pad's event reaches that Control before the game's _unhandled_input: Space on a focused Button
|
|
253
|
+
## pressed it and the game's own handler counted the key as well, and a stick pushed down moved a
|
|
254
|
+
## focused list's selection and never reached the game, with nothing in the answer saying why.
|
|
255
|
+
##
|
|
256
|
+
## Where the focus is once the event has been handled goes under focused_after when it moved, null
|
|
257
|
+
## when nothing holds it then: ui_down on a focused button hands the focus to its neighbour, and an
|
|
258
|
+
## answer naming only the button that took the press read as the focus never having moved.
|
|
259
|
+
static func focus_noted(said: Dictionary, focused: String, host: Node) -> Dictionary:
|
|
260
|
+
if not focused.is_empty():
|
|
261
|
+
said["focused"] = focused
|
|
262
|
+
var now: String = focus_path(host)
|
|
263
|
+
if now.is_empty() and not focused.is_empty():
|
|
264
|
+
said["focused_after"] = null
|
|
265
|
+
elif now != focused:
|
|
266
|
+
said["focused_after"] = now
|
|
267
|
+
return said
|
|
268
|
+
|
|
269
|
+
|
|
230
270
|
## Presses the buttons under [param root] whose Shortcut [param action] matches, as the player's key
|
|
231
271
|
## bound to it would, and answers their paths under "shortcuts" and the one that took the press
|
|
232
272
|
## under "shortcut_pressed". A Viewport hands a shortcut only keys, joypad buttons and
|
|
@@ -277,6 +317,20 @@ static func press_shortcuts(root: Node, action: String) -> Dictionary:
|
|
|
277
317
|
return {"shortcuts": paths, "shortcut_pressed": pressed}
|
|
278
318
|
|
|
279
319
|
|
|
320
|
+
## The methods [param object] answers to, as the engine lists them: for a script itself, the static
|
|
321
|
+
## functions it declares before the methods every script has. The engine's list for a script holds
|
|
322
|
+
## only those, so a static function called on its class was read as taking nothing.
|
|
323
|
+
static func methods_of(object: Object) -> Array[Dictionary]:
|
|
324
|
+
var listed: Array[Dictionary] = []
|
|
325
|
+
if object is Script:
|
|
326
|
+
var script: Script = object
|
|
327
|
+
for entry: Dictionary in script.get_script_method_list():
|
|
328
|
+
if GdhRead.as_int(entry.get("flags", 0)) & METHOD_FLAG_STATIC:
|
|
329
|
+
listed.append(entry)
|
|
330
|
+
listed.append_array(object.get_method_list())
|
|
331
|
+
return listed
|
|
332
|
+
|
|
333
|
+
|
|
280
334
|
## What [param path] names: a node under [param root], as [method node_at] reads it, or an engine
|
|
281
335
|
## singleton by its name. Answers the object under "object", and under "node" the node that objects
|
|
282
336
|
## named in a value are found from, which for a singleton is [param root].
|
|
@@ -302,6 +356,32 @@ static func target_at(root: Node, path: String) -> Dictionary:
|
|
|
302
356
|
return {"object": standing["node"], "node": standing["node"]}
|
|
303
357
|
|
|
304
358
|
|
|
359
|
+
## The script [param path] names for a call of one of its static functions, as [method target_at]
|
|
360
|
+
## answers a node, with [param root] as the node objects named in its arguments are found from: a
|
|
361
|
+
## script by its res:// path, or the global class of that name where no node is at [param path].
|
|
362
|
+
## Answers {} when it names a node or no class, and a message for a res:// path holding no script.
|
|
363
|
+
## A class no node carries could not be called at all, and each way of naming it read as a node or
|
|
364
|
+
## a method misspelt.
|
|
365
|
+
static func class_at(root: Node, path: String) -> Dictionary:
|
|
366
|
+
var script_path: String = path
|
|
367
|
+
if not path.begins_with("res://"):
|
|
368
|
+
if not path.is_valid_ascii_identifier() or not node_at(root, path).has("message"):
|
|
369
|
+
return {}
|
|
370
|
+
script_path = ""
|
|
371
|
+
for declared: Dictionary in ProjectSettings.get_global_class_list():
|
|
372
|
+
if str(declared.get("class", "")) == path:
|
|
373
|
+
script_path = str(declared.get("path", ""))
|
|
374
|
+
if script_path.is_empty():
|
|
375
|
+
return {}
|
|
376
|
+
var loaded: Resource = load(script_path) if ResourceLoader.exists(script_path) else null
|
|
377
|
+
if not loaded is Script:
|
|
378
|
+
return {
|
|
379
|
+
"type": "error",
|
|
380
|
+
"message": "No script is at %s to call a static function of." % script_path,
|
|
381
|
+
}
|
|
382
|
+
return {"object": loaded, "node": root}
|
|
383
|
+
|
|
384
|
+
|
|
305
385
|
## Whether two values can be compared without the comparison itself failing.
|
|
306
386
|
static func comparable(one: Variant, other: Variant) -> bool:
|
|
307
387
|
return GdhSerialisation.comparable(one, other)
|
|
@@ -352,7 +432,7 @@ static func remedy(value: Variant, type: int) -> String:
|
|
|
352
432
|
## The declared type of one parameter of a method, or TYPE_NIL when the method or the parameter
|
|
353
433
|
## is not there, which reads as "no opinion".
|
|
354
434
|
func parameter_type(object: Object, method: String, index: int) -> int:
|
|
355
|
-
for entry: Dictionary in object
|
|
435
|
+
for entry: Dictionary in methods_of(object):
|
|
356
436
|
if entry.get("name", "") != method:
|
|
357
437
|
continue
|
|
358
438
|
var params: Array = entry.get("args", [])
|
|
@@ -363,10 +443,59 @@ func parameter_type(object: Object, method: String, index: int) -> int:
|
|
|
363
443
|
return TYPE_NIL
|
|
364
444
|
|
|
365
445
|
|
|
446
|
+
## The values of the enum argument [param index] of [param method] declares, by name, as
|
|
447
|
+
## `{"enum": "Mood", "values": {...}}`, or {} when it declares none. The engine gives such an argument
|
|
448
|
+
## the type int and the enum's name, as res://main.gd.Mood or Node.ProcessMode.
|
|
449
|
+
func parameter_enum(object: Object, method: String, index: int) -> Dictionary:
|
|
450
|
+
for entry: Dictionary in methods_of(object):
|
|
451
|
+
if entry.get("name", "") != method:
|
|
452
|
+
continue
|
|
453
|
+
var params: Array = entry.get("args", [])
|
|
454
|
+
if index < 0 or index >= params.size():
|
|
455
|
+
return {}
|
|
456
|
+
var parameter: Dictionary = params[index]
|
|
457
|
+
if GdhRead.as_int(parameter.get("usage", 0)) & PROPERTY_USAGE_CLASS_IS_ENUM == 0:
|
|
458
|
+
return {}
|
|
459
|
+
var qualified: String = str(parameter.get("class_name", ""))
|
|
460
|
+
var enum_name: String = qualified.get_slice(".", qualified.get_slice_count(".") - 1)
|
|
461
|
+
var values: Dictionary = GdhDeclared.enum_declared_as(qualified)
|
|
462
|
+
if values.is_empty():
|
|
463
|
+
values = _enum_along(object if object is Script else object.get_script(), enum_name)
|
|
464
|
+
if values.is_empty():
|
|
465
|
+
return {}
|
|
466
|
+
return {"enum": enum_name, "values": values}
|
|
467
|
+
return {}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
## The enum [param named] declared by [param script] or a script it extends, or {}. A script built in
|
|
471
|
+
## a scene or made at run time names its enums by an id nothing loads, gdscript://-92233….gd.Mood
|
|
472
|
+
## measured on 4.7.2, and a method of the object called declares its enums along this chain.
|
|
473
|
+
static func _enum_along(script: Variant, named: String) -> Dictionary:
|
|
474
|
+
if not script is Script:
|
|
475
|
+
return {}
|
|
476
|
+
var current: Script = script
|
|
477
|
+
while current != null:
|
|
478
|
+
var found: Variant = current.get_script_constant_map().get(named)
|
|
479
|
+
if found is Dictionary:
|
|
480
|
+
var values: Dictionary = found
|
|
481
|
+
return values
|
|
482
|
+
current = current.get_base_script()
|
|
483
|
+
return {}
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
## An empty list typed as what the packed array [param type] holds, for its elements to be fitted
|
|
487
|
+
## against one by one, or null for any other type.
|
|
488
|
+
static func packed_template(type: int) -> Variant:
|
|
489
|
+
if not GdhSerialisation.LIST_TYPES.has(type):
|
|
490
|
+
return null
|
|
491
|
+
var element: int = GdhShapes.PACKED_ELEMENTS[type]
|
|
492
|
+
return Array([], element, &"", null)
|
|
493
|
+
|
|
494
|
+
|
|
366
495
|
## The class a method declares for argument [param index], or "" when it names none: an untyped
|
|
367
496
|
## parameter, a Variant, or a type that is not an object.
|
|
368
497
|
func parameter_class(object: Object, method: String, index: int) -> String:
|
|
369
|
-
for entry: Dictionary in object
|
|
498
|
+
for entry: Dictionary in methods_of(object):
|
|
370
499
|
if entry.get("name", "") != method:
|
|
371
500
|
continue
|
|
372
501
|
var params: Array = entry.get("args", [])
|
|
@@ -512,7 +641,7 @@ func fitted(value: Variant, type: int) -> Variant:
|
|
|
512
641
|
## An empty list or map typed the way argument [param index] of [param method] declares, or null
|
|
513
642
|
## when that argument is not a typed container. What [method typed_like] builds against.
|
|
514
643
|
func parameter_container(object: Object, method: String, index: int) -> Variant:
|
|
515
|
-
for entry: Dictionary in object
|
|
644
|
+
for entry: Dictionary in methods_of(object):
|
|
516
645
|
if entry.get("name", "") != method:
|
|
517
646
|
continue
|
|
518
647
|
var params: Array = entry.get("args", [])
|
|
@@ -654,6 +783,18 @@ func _element(item: Variant, builtin: int, declared: String, script: Script, res
|
|
|
654
783
|
if named.has("message"):
|
|
655
784
|
return {"message": str(named["message"]) + "."}
|
|
656
785
|
return {"value": named["object"]}
|
|
786
|
+
# Fitted alone, 4.5 into an Array[int] went in as 4, as a single value is refused for.
|
|
787
|
+
if cuts(item, builtin):
|
|
788
|
+
return {
|
|
789
|
+
"message":
|
|
790
|
+
(
|
|
791
|
+
"is %s, which %s cannot hold without changing it."
|
|
792
|
+
% [JSON.stringify(serialize(item), "", false), cut_by(builtin)]
|
|
793
|
+
)
|
|
794
|
+
}
|
|
795
|
+
var rounded: String = inexact(item, builtin)
|
|
796
|
+
if not rounded.is_empty():
|
|
797
|
+
return {"message": "is too large to be sure of: %s." % rounded}
|
|
657
798
|
var fitted_item: Variant = fitted(item, builtin)
|
|
658
799
|
if not acceptable(fitted_item, builtin):
|
|
659
800
|
# The value as it was sent rather than its engine type: JSON numbers arrive as floats, so a 7
|