gdharness 1.1.21 → 1.1.23

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.
Files changed (51) hide show
  1. package/build/cli.js +1470 -726
  2. package/build/godot/addons/gdharness_editor/bridge_client.gd +12 -2
  3. package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
  4. package/build/godot/addons/gdharness_editor/reading.gd +17 -0
  5. package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
  6. package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
  7. package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
  8. package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
  9. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
  10. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
  11. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
  12. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
  13. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
  14. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
  15. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
  16. package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
  17. package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
  18. package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +15 -5
  20. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +51 -5
  21. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
  22. package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
  23. package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
  24. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +89 -16
  25. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +107 -343
  26. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
  27. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
  28. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
  29. package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
  30. package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
  31. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +61 -28
  32. package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
  33. package/build/godot/operations/audio_buses.gd +47 -11
  34. package/build/godot/operations/class_cache.gd +18 -2
  35. package/build/godot/operations/classdb_queries.gd +28 -17
  36. package/build/godot/operations/dependencies.gd +128 -17
  37. package/build/godot/operations/file_walk.gd +20 -0
  38. package/build/godot/operations/gdscript_analysis.gd +59 -20
  39. package/build/godot/operations/gdscript_authoring.gd +127 -60
  40. package/build/godot/operations/godot_operations.gd +24 -5
  41. package/build/godot/operations/import_pipeline.gd +179 -19
  42. package/build/godot/operations/logger.gd +2 -2
  43. package/build/godot/operations/plugins.gd +88 -70
  44. package/build/godot/operations/project_config.gd +41 -6
  45. package/build/godot/operations/project_diagnostics.gd +27 -11
  46. package/build/godot/operations/reading.gd +17 -0
  47. package/build/godot/operations/resource_files.gd +60 -17
  48. package/build/godot/operations/serialisation.gd +116 -7
  49. package/build/index.js +1385 -553
  50. package/build/keeper.js +4 -0
  51. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
1
  extends RefCounted
2
2
 
3
3
  ## What the server asks about the running tree: its shape, one node, the nodes matching a
4
- ## question, where one is on screen, a property set, a method called, the metrics.
4
+ ## question, where one is on screen, a property's value, the metrics. What it changes, a property
5
+ ## written or a method called, is in runtime_changes.gd.
5
6
 
6
7
  const Paths = preload("runtime_paths.gd")
7
8
  const Read = preload("reading.gd")
@@ -13,7 +14,6 @@ const Words = preload("runtime_words.gd")
13
14
  ## The most nodes one find answers with, unless asked for fewer: enough for any real query and
14
15
  ## far short of the tree dump a query exists to avoid.
15
16
  const FIND_LIMIT: int = 100
16
- const FIND_LIMIT_CEILING: int = 1000
17
17
 
18
18
  ## What a find can be narrowed by, which is also what it refuses to answer without. Named once so
19
19
  ## the refusal lists the same set the walk reads.
@@ -23,6 +23,11 @@ const FILTERS: PackedStringArray = ["class", "script", "name", "group", "says"]
23
23
  ## thousand is a tree somebody pointed this at by mistake.
24
24
  const READ_LIMIT: int = 500
25
25
 
26
+ ## The most either can be asked for. A limit above it is refused rather than cut to it: the ceiling
27
+ ## was the default for a read, so the remedy its own answer gave, raising the limit, changed nothing
28
+ ## and said nothing, and a caller told to raise a number that stays put concludes the screen moved.
29
+ const LIMIT_CEILING: int = 5000
30
+
26
31
  var _host: Node
27
32
  var _values: Values
28
33
 
@@ -87,7 +92,11 @@ func find_nodes(params: Dictionary) -> Dictionary:
87
92
  wanted[filter] = str(params.get(filter, ""))
88
93
  var wanted_property: String = str(params.get("property", ""))
89
94
  var include_hidden: bool = Read.as_bool(params.get("include_hidden", true), true)
90
- var limit: int = clampi(Read.as_int(params.get("limit", FIND_LIMIT), FIND_LIMIT), 1, FIND_LIMIT_CEILING)
95
+ var limit: int = Read.as_int(params.get("limit", FIND_LIMIT), FIND_LIMIT)
96
+ if limit < 1 or limit > LIMIT_CEILING:
97
+ return {
98
+ "type": "error", "message": "limit is from 1 to %d, and %d was asked for" % [LIMIT_CEILING, limit]
99
+ }
91
100
 
92
101
  if not _anything_asked(wanted):
93
102
  return {"type": "error", "message": "find_nodes needs at least one of " + ", ".join(FILTERS)}
@@ -115,6 +124,10 @@ func find_nodes(params: Dictionary) -> Dictionary:
115
124
  for filter: String in wanted:
116
125
  widened[filter] = widened_says if filter == "says" else wanted[filter]
117
126
  var nearly_said: int = 0
127
+ # The near misses the same find would leave out as hidden, kept apart: the note says what a
128
+ # query would find, and counting these in it named a query that, asked, found none of them.
129
+ var nearly_hidden: int = 0
130
+ var nearly_said_hidden: int = 0
118
131
  var hidden: int = 0
119
132
  while not pending.is_empty():
120
133
  var node: Node = pending.pop_front()
@@ -128,13 +141,19 @@ func find_nodes(params: Dictionary) -> Dictionary:
128
141
  else:
129
142
  found.append(_found(node, wanted_property))
130
143
  elif rest and literal and str(node.name).containsn(wanted["name"]):
131
- nearly += 1
144
+ if include_hidden or shown(node):
145
+ nearly += 1
146
+ else:
147
+ nearly_hidden += 1
132
148
  elif (
133
149
  not widened_says.is_empty()
134
150
  and _named(node, wanted["name"])
135
151
  and _matches_apart_from_name(node, widened)
136
152
  ):
137
- nearly_said += 1
153
+ if include_hidden or shown(node):
154
+ nearly_said += 1
155
+ else:
156
+ nearly_said_hidden += 1
138
157
  # Internal children included, which they were not. A ConfirmationDialog builds its Yes and
139
158
  # its No as internal nodes, and a ScrollContainer its bars, so a find over a screen for
140
159
  # every Button came back without the two buttons the player is being asked to press:
@@ -150,21 +169,39 @@ func find_nodes(params: Dictionary) -> Dictionary:
150
169
  # Nothing found is the one answer that cannot be told apart from having asked the wrong
151
170
  # question, and a name written without a wildcard is the way an agent writes "contains".
152
171
  # Said only when it changes the answer, so a genuine nothing stays a plain nothing.
153
- if found.is_empty() and nearly > 0:
154
- var holding: String = "names contain" if nearly > 1 else "name contains"
155
- notes.append(
156
- (
157
- 'name is matched as a glob against the whole name; %d node %s "%s", which "*%s*" would find'
158
- % [nearly, holding, wanted["name"], wanted["name"]]
172
+ if found.is_empty() and nearly + nearly_hidden > 0:
173
+ var named_near: int = nearly if nearly > 0 else nearly_hidden
174
+ var holding: String = "names contain" if named_near > 1 else "name contains"
175
+ (
176
+ notes
177
+ . append(
178
+ (
179
+ 'name is matched as a glob against the whole name; %d %snode %s "%s", which "*%s*"%s would find'
180
+ % [
181
+ named_near,
182
+ "" if nearly > 0 else "hidden ",
183
+ holding,
184
+ wanted["name"],
185
+ wanted["name"],
186
+ "" if nearly > 0 else " with includeHidden true",
187
+ ]
188
+ )
159
189
  )
160
190
  )
161
- if found.is_empty() and nearly_said > 0:
191
+ if found.is_empty() and nearly_said + nearly_said_hidden > 0:
192
+ var said_near: int = nearly_said if nearly_said > 0 else nearly_said_hidden
162
193
  (
163
194
  notes
164
195
  . append(
165
196
  (
166
- 'says with a wildcard is matched as a glob against a node\'s whole text; "%s" would find %d node%s'
167
- % [widened_says, nearly_said, "" if nearly_said == 1 else "s"]
197
+ 'says with a wildcard is matched as a glob against a node\'s whole text; "%s"%s would find %d %snode%s'
198
+ % [
199
+ widened_says,
200
+ "" if nearly_said > 0 else " with includeHidden true",
201
+ said_near,
202
+ "" if nearly_said > 0 else "hidden ",
203
+ "" if said_near == 1 else "s",
204
+ ]
168
205
  )
169
206
  )
170
207
  )
@@ -183,17 +220,52 @@ func find_nodes(params: Dictionary) -> Dictionary:
183
220
  return answer
184
221
 
185
222
 
186
- ## Whether the player can see [param node], its ancestors counted.
223
+ ## Whether the player can see [param node], by the rule the engine draws by.
187
224
  ##
188
- ## Not [method CanvasItem.is_visible_in_tree] on its own, because only the nodes that draw have it:
189
- ## a plain Node sitting between a hidden panel and a label has no visibility to ask about, and the
190
- ## label answers that it is visible while nothing of it is on screen. Walking up is what makes a
191
- ## row hidden because the panel holding it is hidden, which is what somebody checking a screen is
192
- ## asking about. A wait on words asks the same, so it reads this too.
225
+ ## A CanvasItem's visibility comes down through CanvasItem parents only, and one whose parent is not
226
+ ## a CanvasItem starts again from its canvas: measured on 4.7.2, a Label under a plain Node under a
227
+ ## hidden panel, and one under a hidden Node3D, both answer visible in tree, while one straight under
228
+ ## the panel does not. Walking every ancestor called those two hidden, so their words were left off
229
+ ## a screen that showed them and a wait on them ran out. What a CanvasItem's rule leaves out is its
230
+ ## canvas: a hidden layer hides everything drawn on it while each item keeps its own flag, and a
231
+ ## hidden window everything in it. A wait on words asks the same, so it reads this too.
193
232
  static func shown(node: Node) -> bool:
233
+ var item: CanvasItem = node as CanvasItem
234
+ if item != null:
235
+ return item.is_visible_in_tree() and _on_a_shown_canvas(item)
236
+ var spatial: Node3D = node as Node3D
237
+ if spatial != null:
238
+ return spatial.is_visible_in_tree() and _in_shown_windows(spatial)
239
+ var layer: CanvasLayer = node as CanvasLayer
240
+ if layer != null:
241
+ return layer.visible and _in_shown_windows(layer)
242
+ var window: Window = node as Window
243
+ if window != null:
244
+ return window.visible and _in_shown_windows(window.get_parent())
245
+ # A node that draws nothing is shown when what it sits in is.
246
+ var parent: Node = node.get_parent()
247
+ return parent == null or shown(parent)
248
+
249
+
250
+ ## Whether the canvas [param item] is drawn on is showing: its layer, and the windows around it.
251
+ static func _on_a_shown_canvas(item: CanvasItem) -> bool:
252
+ var walk: Node = item.get_parent()
253
+ while walk != null:
254
+ var layer: CanvasLayer = walk as CanvasLayer
255
+ if layer != null:
256
+ return layer.visible and _in_shown_windows(layer)
257
+ if walk is Viewport:
258
+ return _in_shown_windows(walk)
259
+ walk = walk.get_parent()
260
+ return true
261
+
262
+
263
+ ## Whether every window [param node] sits in is showing, itself included when it is one.
264
+ static func _in_shown_windows(node: Node) -> bool:
194
265
  var walk: Node = node
195
266
  while walk != null:
196
- if not _drawn(walk):
267
+ var window: Window = walk as Window
268
+ if window != null and not window.visible:
197
269
  return false
198
270
  walk = walk.get_parent()
199
271
  return true
@@ -303,7 +375,11 @@ static func _script_is(attached: Script, wanted: String) -> bool:
303
375
  func read_text(params: Dictionary) -> Dictionary:
304
376
  var root_path: String = str(params.get("root", "/root"))
305
377
  var include_hidden: bool = Read.as_bool(params.get("include_hidden", false))
306
- var limit: int = clampi(Read.as_int(params.get("limit", READ_LIMIT), READ_LIMIT), 1, READ_LIMIT)
378
+ var limit: int = Read.as_int(params.get("limit", READ_LIMIT), READ_LIMIT)
379
+ if limit < 1 or limit > LIMIT_CEILING:
380
+ return {
381
+ "type": "error", "message": "limit is from 1 to %d, and %d was asked for" % [LIMIT_CEILING, limit]
382
+ }
307
383
 
308
384
  var reached: Dictionary = Values.node_at(_host.get_tree().root, root_path)
309
385
  if reached.has("message"):
@@ -337,36 +413,22 @@ func read_text(params: Dictionary) -> Dictionary:
337
413
  ## until it is opened, and reading a closed menu would put every item on the screen.
338
414
  ## Fills [param into] up to [param most] lines and answers with how many further lines the rest of
339
415
  ## the subtree says, which is what the caller is told rather than left to infer.
416
+ ##
417
+ ## Asked of each node rather than cut off at the first hidden one, since a hidden panel does not
418
+ ## hide a label reached through a plain Node: see [method shown].
340
419
  func _read_into(node: Node, include_hidden: bool, most: int, into: Array[String]) -> int:
341
- if not include_hidden and not _drawn(node):
342
- return 0
343
420
  var left_out: int = 0
344
- for said: String in Words.lines_said_by(node):
345
- if into.size() < most:
346
- into.append(said)
347
- else:
348
- left_out += 1
421
+ if include_hidden or shown(node):
422
+ for said: String in Words.lines_said_by(node):
423
+ if into.size() < most:
424
+ into.append(said)
425
+ else:
426
+ left_out += 1
349
427
  for child: Node in node.get_children(true):
350
428
  left_out += _read_into(child, include_hidden, most, into)
351
429
  return left_out
352
430
 
353
431
 
354
- ## Whether [param node] is on the screen at all, for the three kinds of thing that can be hidden.
355
- ##
356
- ## A Node3D among them because a hidden one draws nothing, Label3D included: reading a screen or
357
- ## finding what is on it counted a hidden 3D subtree as showing, which is the one answer neither
358
- ## question wants.
359
- static func _drawn(node: Node) -> bool:
360
- var control: CanvasItem = node as CanvasItem
361
- if control != null:
362
- return control.visible
363
- var spatial: Node3D = node as Node3D
364
- if spatial != null:
365
- return spatial.visible
366
- var window: Window = node as Window
367
- return window == null or window.visible
368
-
369
-
370
432
  ## Where a node is on screen: a Control's rectangle, a Node2D's position, or the place a 3D node
371
433
  ## is drawn in, in both the canvas coordinates the node reports and the window pixels input
372
434
  ## arrives in. The two differ whenever the project stretches its viewport, which is what made a
@@ -562,304 +624,6 @@ func get_property(params: Dictionary) -> Dictionary:
562
624
  }
563
625
 
564
626
 
565
- func set_property(params: Dictionary) -> Dictionary:
566
- var node_path: String = str(params.get("path", ""))
567
- var property: String = str(params.get("property", ""))
568
- var value: Variant = params.get("value")
569
-
570
- if node_path.is_empty() or property.is_empty():
571
- return {"type": "error", "message": "Node path and property required"}
572
-
573
- var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
574
- if standing.has("message"):
575
- return standing
576
- var node: Node = standing["node"]
577
-
578
- # Through a path as well, for the reason [method Paths.walk_to] gives, and read back off the same
579
- # holder afterwards: a set that does not take says so by answering with the old value, which is
580
- # how a typed container refusing a write is told from one accepting it.
581
- var reached: Dictionary = Paths.walk_to(node, node_path, property)
582
- if reached.has("message"):
583
- return reached
584
-
585
- var holder: Variant = reached["holder"]
586
- var named: String = reached["name"]
587
- # A call can be walked through and read, and is not a place: what it returns is the method's
588
- # to hand out, and writing "into" it would set nothing the game keeps.
589
- if not Paths.method_of(named).is_empty():
590
- return {
591
- "type": "error",
592
- "message":
593
- (
594
- "%s:%s is a call, and a call is not a place to write: name a property"
595
- % [reached["called"], named]
596
- )
597
- }
598
- if not Paths.can_read(holder, named):
599
- return {"type": "error", "message": Paths.nothing_there(holder, named, str(reached["called"]))}
600
- var old_value: Variant = Paths.read_under(holder, named)
601
- # The same rule the call path holds: a value that cannot become what the property holds is
602
- # refused rather than written. Writing it means the engine picks something, and what it picks
603
- # for a word where a number goes is zero, which the answer then reports as the new value.
604
- var slot: Dictionary = Values.slot_declared(holder, named)
605
- var wanted: int = slot["type"] if slot["type"] != TYPE_NIL else typeof(old_value)
606
- var given: Variant
607
- if wanted == TYPE_OBJECT and (value is String or value is Dictionary):
608
- var named_object: Dictionary = _object_named(node, node_path, value, str(slot["class"]))
609
- if named_object.has("message"):
610
- return {
611
- "type": "error",
612
- "message": "%s.%s holds an object: %s" % [reached["called"], named, named_object["message"]]
613
- }
614
- given = named_object["object"]
615
- else:
616
- given = _values.fitted(value, wanted)
617
- if not Values.acceptable(given, wanted):
618
- return {
619
- "type": "error",
620
- "message":
621
- (
622
- "%s.%s holds %s and the value given is %s, which cannot become one."
623
- % [reached["called"], named, type_string(wanted), type_string(typeof(given))]
624
- )
625
- }
626
- var typed: Dictionary = _values.typed_like(given, old_value, _element_object.bind(node, node_path))
627
- if typed.has("message"):
628
- return {
629
- "type": "error",
630
- "message":
631
- "%s.%s is a typed %s: %s." % [reached["called"], named, _sort_of(old_value), typed["message"]]
632
- }
633
- given = typed["value"]
634
- # The write alone, setter included, which is where a property's cost is.
635
- var began: int = Time.get_ticks_usec()
636
- Paths.write(holder, named, given)
637
- var elapsed: int = Time.get_ticks_usec() - began
638
- var now: Variant = Paths.read_under(holder, named)
639
- # Read back rather than trusted. The engine drops a write it will not take without a word, and
640
- # the answer then showed the old value as the new one, shaped as a success: a typed container
641
- # refusing a plain list did exactly that. A value the property changed on the way in, a setter
642
- # clamping it, still changed it, so only a write that left the property as it was is refused.
643
- # And only one that asked for a change: the value the property already held, written again, left
644
- # it as it was because nothing was asked of it.
645
- var already: bool = _held_already(given, old_value)
646
- if (
647
- Values.comparable(given, old_value)
648
- and not already
649
- and Values.comparable(now, old_value)
650
- and now == old_value
651
- ):
652
- return {
653
- "type": "error",
654
- "message":
655
- (
656
- (
657
- "%s.%s was given %s and still holds %s: the engine kept what it had,"
658
- + " which a property it will not write does."
659
- )
660
- % [
661
- reached["called"],
662
- named,
663
- JSON.stringify(_values.serialize(given)),
664
- JSON.stringify(_values.serialize(now))
665
- ]
666
- )
667
- }
668
-
669
- var answer: Dictionary = {
670
- "type": "property_set",
671
- "path": node_path,
672
- "property": property,
673
- "old_value": _values.serialize(old_value),
674
- "new_value": _values.serialize(now),
675
- "elapsed_usec": elapsed
676
- }
677
- if already and Values.comparable(now, old_value) and now == old_value:
678
- answer["unchanged"] = true
679
- return answer
680
-
681
-
682
- ## Whether [param given] is the value [param held] already is, at the precision the property keeps.
683
- ##
684
- ## A float arrives as 64 bits and most engine properties keep 32: Camera3D.h_offset given -1.1
685
- ## reads back as -1.10000002384186, so comparing the two exactly took a write of the value already
686
- ## held for one the engine refused (#780). Rounding both to 32 bits is exact for a 32-bit property.
687
- ## A 64-bit one can take a write this calls the same and read back changed, and the caller calls a
688
- ## write unchanged only when it reads back as it was. What that cannot tell apart is a 64-bit
689
- ## property whose setter ignored a value differing from the held one below 32 bits: it is answered
690
- ## unchanged rather than refused. Vectors and colours hold 32-bit parts in the value itself, so they
691
- ## already compare at the property's precision.
692
- static func _held_already(given: Variant, held: Variant) -> bool:
693
- if not Values.comparable(given, held):
694
- return false
695
- if given == held:
696
- return true
697
- if typeof(given) != TYPE_FLOAT or typeof(held) != TYPE_FLOAT:
698
- return false
699
- return PackedFloat32Array([given])[0] == PackedFloat32Array([held])[0]
700
-
701
-
702
- func call_method(params: Dictionary) -> Dictionary:
703
- var node_path: String = str(params.get("path", ""))
704
- var method: String = str(params.get("method", ""))
705
- var args: Array = params.get("args", [])
706
-
707
- if node_path.is_empty() or method.is_empty():
708
- return {"type": "error", "message": "Node path and method required"}
709
-
710
- var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
711
- if standing.has("message"):
712
- return standing
713
- var node: Node = standing["node"]
714
-
715
- # Through a path as well, for the reason [method Paths.walk_to] gives. What a game does hangs off its
716
- # nodes as much as its state does, so reading `_game:run:day` while being unable to call
717
- # `_game:run:advance` answers half of what a node holds and refuses the other half.
718
- var reached: Dictionary = Paths.walk_to(node, node_path, method)
719
- if reached.has("message"):
720
- return reached
721
-
722
- # A list or a map answers the calls a path makes on one, and nothing else: said in those terms
723
- # rather than as "has no method", which reads as a misspelling of a method that was never going
724
- # to be there, and with the element spelled out, since a method on what it holds is the usual aim.
725
- if not reached["holder"] is Object:
726
- var sort: String = "a list"
727
- if reached["holder"] is Dictionary:
728
- sort = "a map"
729
- elif Paths.packed(reached["holder"]):
730
- sort = "a packed list"
731
- var spelled: String = str(reached["name"])
732
- if Paths.method_of(spelled).is_empty():
733
- spelled += "()"
734
- if args.is_empty() and Paths.can_read(reached["holder"], spelled):
735
- var read_from: int = Time.get_ticks_usec()
736
- var answered: Variant = Paths.read_under(reached["holder"], spelled)
737
- var read_for: int = Time.get_ticks_usec() - read_from
738
- return {
739
- "type": "method_result",
740
- "path": node_path,
741
- "method": method,
742
- "result": _values.serialize(answered),
743
- "elapsed_usec": read_for,
744
- }
745
- var why: String = (
746
- "%s:%s takes no arguments" % [reached["called"], spelled]
747
- if Paths.can_read(reached["holder"], spelled)
748
- else Paths.nothing_there(reached["holder"], spelled, str(reached["called"]))
749
- )
750
- return {
751
- "type": "error",
752
- "message":
753
- (
754
- "%s. A method of what %s holds is called on the element, as in %s:0:%s"
755
- % [why, sort, reached["called"], reached["name"]]
756
- )
757
- }
758
- var holder: Object = reached["holder"]
759
- var named: String = reached["name"]
760
- # The method a call ends on is the one being called whether or not it carries the brackets a
761
- # step along the way would: "get_viewport():gui_get_focus_owner()" is the same call as without
762
- # the last pair.
763
- if not Paths.method_of(named).is_empty():
764
- named = Paths.method_of(named)
765
- if not holder.has_method(named):
766
- return {"type": "error", "message": "%s has no method %s" % [reached["called"], named]}
767
-
768
- # Checked before the call rather than left to it. `callv` raises inside the game when an
769
- # argument cannot be converted, and an error raised in a game somebody is playing holds it at a
770
- # debugger break: every later call then reports a game that is not responding, which points
771
- # nowhere near the argument. A wrong argument costs a refusal, never the session's game.
772
- var deserialized_args: Array = []
773
- for index: int in args.size():
774
- var wants: int = _values.parameter_type(holder, named, index)
775
- if wants == TYPE_OBJECT and (args[index] is String or args[index] is Dictionary):
776
- var declared: String = _values.parameter_class(holder, named, index)
777
- var named_object: Dictionary = _object_named(node, node_path, args[index], declared)
778
- if named_object.has("message"):
779
- return {
780
- "type": "error",
781
- "message":
782
- "%s.%s argument %d: %s" % [reached["called"], named, index + 1, named_object["message"]]
783
- }
784
- deserialized_args.append(named_object["object"])
785
- continue
786
- var given: Variant = _values.fitted(args[index], wants)
787
- if not Values.acceptable(given, wants):
788
- return {
789
- "type": "error",
790
- "message":
791
- (
792
- "%s.%s takes %s as argument %d and was given %s, which cannot be converted."
793
- % [reached["called"], named, type_string(wants), index + 1, type_string(typeof(given))]
794
- )
795
- }
796
- # A typed list or map is built as one, since a plain one handed to such a parameter raises
797
- # inside the game, which is what every check here exists to stop.
798
- var container: Variant = _values.parameter_container(holder, named, index)
799
- if container != null:
800
- var typed: Dictionary = _values.typed_like(
801
- given, container, _element_object.bind(node, node_path)
802
- )
803
- if typed.has("message"):
804
- return {
805
- "type": "error",
806
- "message":
807
- (
808
- "%s.%s takes a typed %s as argument %d: %s."
809
- % [reached["called"], named, _sort_of(container), index + 1, typed["message"]]
810
- )
811
- }
812
- given = typed["value"]
813
- deserialized_args.append(given)
814
-
815
- # Timed around the call alone, since the bridge's round trip is a second or more and swamps what
816
- # one click's work costs; the frame metrics read whichever frame is last, which is not this one.
817
- var began: int = Time.get_ticks_usec()
818
- var result: Variant = holder.callv(named, deserialized_args)
819
- var elapsed: int = Time.get_ticks_usec() - began
820
-
821
- return {
822
- "type": "method_result",
823
- "path": node_path,
824
- "method": method,
825
- "result": _values.serialize(result),
826
- "elapsed_usec": elapsed
827
- }
828
-
829
-
830
- ## [method _object_named] with the path first, which is the order [method Values.typed_like] calls
831
- ## an element resolver in.
832
- func _element_object(given: Variant, declared: String, node: Node, node_path: String) -> Dictionary:
833
- return _object_named(node, node_path, given, declared)
834
-
835
-
836
- ## "list" or "map", for a refusal about a typed container.
837
- static func _sort_of(container: Variant) -> String:
838
- return "list" if container is Array else "map"
839
-
840
-
841
- ## The object [param given] names for a slot or a parameter declared as [param declared], as
842
- ## `{"object": ...}`, or `{"message": ...}` saying why it names none that fits.
843
- ##
844
- ## JSON cannot carry an object, so one the game holds is named by its path, read from the node the
845
- ## call or the write was made on, and handed over as that instance rather than a copy: a method
846
- ## given a piece of the game's state acts on the piece the game keeps, and a slot written with one
847
- ## holds the game's own.
848
- ##
849
- ## The text null names no object, which is what an object slot or parameter takes to be emptied. A
850
- ## caller's null arrived as that text in #772 and #783, and was looked up as a node called "null".
851
- func _object_named(node: Node, node_path: String, given: Variant, declared: String) -> Dictionary:
852
- if given is String and str(given).strip_edges() == "null":
853
- return {"object": null}
854
- var found: Dictionary = Paths.object_at(_host.get_tree().root, node, node_path, given)
855
- if found.has("message"):
856
- return found
857
- var instance: Object = found["object"]
858
- if not declared.is_empty() and not Values.is_a(instance, declared):
859
- return {"message": "%s is %s, not a %s" % [str(given), Values.class_of(instance), declared]}
860
- return found
861
-
862
-
863
627
  func get_metrics(params: Dictionary) -> Dictionary:
864
628
  var metrics: Array = params.get("metrics", [])
865
629
  var all: Dictionary = {
@@ -17,20 +17,32 @@ extends RefCounted
17
17
  ## sit inside another viewport. A SubViewport drawn some other way, onto a mesh for one, has no
18
18
  ## place on the screen to carry the point to, and the point stays in it.
19
19
  static func reach(viewport: Viewport, point: Vector2) -> Dictionary:
20
+ var out: Array[Dictionary] = steps_out(viewport, point)
21
+ return out.back()
22
+
23
+
24
+ ## Every viewport a pointer at [param point] in [param viewport] passes through on the way out to
25
+ ## the one it arrives at, innermost first, as {"viewport", "point", "through"}: the point in that
26
+ ## viewport's space, and the embedded [Window] or the [SubViewportContainer] in it that shows the
27
+ ## step before, null for the first. What [method reach] answers is the last of them.
28
+ static func steps_out(viewport: Viewport, point: Vector2) -> Array[Dictionary]:
29
+ var out: Array[Dictionary] = [{"viewport": viewport, "point": point, "through": null}]
20
30
  while true:
21
31
  var window: Window = viewport as Window
22
32
  if window != null and window.is_embedded() and window.get_parent() != null:
23
33
  point += Vector2(window.position)
24
34
  viewport = window.get_parent().get_viewport()
35
+ out.append({"viewport": viewport, "point": point, "through": window})
25
36
  continue
26
37
  var shown_by: SubViewportContainer = viewport.get_parent() as SubViewportContainer
27
38
  if viewport is SubViewport and shown_by != null:
28
39
  var scaled: Vector2 = point * float(shown_by.stretch_shrink) if shown_by.stretch else point
29
40
  point = shown_by.get_global_transform_with_canvas() * scaled
30
41
  viewport = shown_by.get_viewport()
42
+ out.append({"viewport": viewport, "point": point, "through": shown_by})
31
43
  continue
32
44
  break
33
- return {"viewport": viewport, "point": point}
45
+ return out
34
46
 
35
47
 
36
48
  ## Where [param point], in [param viewport]'s own coordinates, is in the pixels of the window the