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.
- package/build/cli.js +1470 -726
- package/build/godot/addons/gdharness_editor/bridge_client.gd +12 -2
- package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
- package/build/godot/addons/gdharness_editor/reading.gd +17 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
- package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
- package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +15 -5
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +51 -5
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +89 -16
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +107 -343
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +61 -28
- package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
- package/build/godot/operations/audio_buses.gd +47 -11
- package/build/godot/operations/class_cache.gd +18 -2
- package/build/godot/operations/classdb_queries.gd +28 -17
- package/build/godot/operations/dependencies.gd +128 -17
- package/build/godot/operations/file_walk.gd +20 -0
- package/build/godot/operations/gdscript_analysis.gd +59 -20
- package/build/godot/operations/gdscript_authoring.gd +127 -60
- package/build/godot/operations/godot_operations.gd +24 -5
- package/build/godot/operations/import_pipeline.gd +179 -19
- package/build/godot/operations/logger.gd +2 -2
- package/build/godot/operations/plugins.gd +88 -70
- package/build/godot/operations/project_config.gd +41 -6
- package/build/godot/operations/project_diagnostics.gd +27 -11
- package/build/godot/operations/reading.gd +17 -0
- package/build/godot/operations/resource_files.gd +60 -17
- package/build/godot/operations/serialisation.gd +116 -7
- package/build/index.js +1385 -553
- package/build/keeper.js +4 -0
- 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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
167
|
-
% [
|
|
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],
|
|
223
|
+
## Whether the player can see [param node], by the rule the engine draws by.
|
|
187
224
|
##
|
|
188
|
-
##
|
|
189
|
-
## a
|
|
190
|
-
##
|
|
191
|
-
##
|
|
192
|
-
##
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
into.
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
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
|