gdharness 1.1.21 → 1.1.22
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 +1036 -525
- 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 +8 -4
- 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 +28 -8
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +7 -299
- 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 +20 -9
- 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/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 +25 -10
- 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 +949 -435
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
extends Logger
|
|
2
|
+
|
|
3
|
+
## What the engine reports on the main thread while one call runs, added for that call alone.
|
|
4
|
+
##
|
|
5
|
+
## A call the engine refuses, given too few arguments or one it will not convert, comes back from
|
|
6
|
+
## `callv` as null, which is also what a method that ran and returned nothing answers, and the
|
|
7
|
+
## engine's reason goes only to the game's output. Measured on 4.7.2: each refusal is one error from
|
|
8
|
+
## `callv` whose text begins with [constant REFUSED]. Anything else reported while the call runs is
|
|
9
|
+
## the method's own, raised on its way through.
|
|
10
|
+
|
|
11
|
+
const REFUSED: String = "Error calling method from 'callv': "
|
|
12
|
+
|
|
13
|
+
## The engine's reason for refusing the call, without [constant REFUSED], or "" when it ran.
|
|
14
|
+
var refused: String = ""
|
|
15
|
+
## What was reported while it ran, a line each, warnings marked as such.
|
|
16
|
+
var raised: Array[String] = []
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
func _log_message(_message: String, _error: bool) -> void:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# The engine's own signature for a Logger, eight parameters and not this project's to shape.
|
|
24
|
+
# gdlint:ignore = function-arguments-number
|
|
25
|
+
func _log_error(
|
|
26
|
+
function: String,
|
|
27
|
+
_file: String,
|
|
28
|
+
_line: int,
|
|
29
|
+
code: String,
|
|
30
|
+
rationale: String,
|
|
31
|
+
_editor_notify: bool,
|
|
32
|
+
error_type: int,
|
|
33
|
+
_script_backtraces: Array[ScriptBacktrace],
|
|
34
|
+
) -> void:
|
|
35
|
+
# Another thread's report in the same moment is not the call's.
|
|
36
|
+
if OS.get_thread_caller_id() != OS.get_main_thread_id():
|
|
37
|
+
return
|
|
38
|
+
var text: String = rationale if not rationale.is_empty() else code
|
|
39
|
+
if function == "callv" and text.begins_with(REFUSED) and refused.is_empty():
|
|
40
|
+
refused = text.trim_prefix(REFUSED)
|
|
41
|
+
return
|
|
42
|
+
raised.append(("WARNING: " if error_type == ERROR_TYPE_WARNING else "") + text)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://b7rsw2ahrpmhl
|
|
@@ -52,6 +52,23 @@ static func as_float(value: Variant, fallback: float = 0.0) -> float:
|
|
|
52
52
|
if value is String:
|
|
53
53
|
var text: String = value
|
|
54
54
|
return text.to_float()
|
|
55
|
+
if value is Dictionary:
|
|
56
|
+
var tagged: Dictionary = value
|
|
57
|
+
if str(tagged.get("_type", "")) == "float":
|
|
58
|
+
return non_finite(str(tagged.get("value", "")), fallback)
|
|
59
|
+
return fallback
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## The float a non-finite tag names, "inf", "-inf" or "nan", or [param fallback] for anything else.
|
|
63
|
+
## JSON has no spelling for these, so the serialiser tags them rather than let them arrive as null.
|
|
64
|
+
static func non_finite(word: String, fallback: float = 0.0) -> float:
|
|
65
|
+
match word:
|
|
66
|
+
"inf":
|
|
67
|
+
return INF
|
|
68
|
+
"-inf":
|
|
69
|
+
return -INF
|
|
70
|
+
"nan":
|
|
71
|
+
return NAN
|
|
55
72
|
return fallback
|
|
56
73
|
|
|
57
74
|
|
|
@@ -11,10 +11,12 @@ extends Node
|
|
|
11
11
|
## id and the reply carries it back.
|
|
12
12
|
|
|
13
13
|
const Capture = preload("runtime_capture.gd")
|
|
14
|
+
const Changes = preload("runtime_changes.gd")
|
|
14
15
|
const ErrorReport = preload("error_report.gd")
|
|
15
16
|
const InputCommands = preload("runtime_input.gd")
|
|
16
17
|
const Queries = preload("runtime_queries.gd")
|
|
17
18
|
const Read = preload("reading.gd")
|
|
19
|
+
const Typing = preload("runtime_typing.gd")
|
|
18
20
|
const Values = preload("runtime_values.gd")
|
|
19
21
|
const Waits = preload("runtime_waits.gd")
|
|
20
22
|
|
|
@@ -48,7 +50,9 @@ var values: Values = Values.new()
|
|
|
48
50
|
# rather than by reference, and a module referenced only by the Callables in the command table
|
|
49
51
|
# would be freed on the way out of _init, leaving every command "unknown".
|
|
50
52
|
var _queries: Queries = Queries.new(self, values)
|
|
53
|
+
var _changes: Changes = Changes.new(self, values)
|
|
51
54
|
var _input: InputCommands = InputCommands.new(self, values)
|
|
55
|
+
var _typing: Typing = Typing.new(self)
|
|
52
56
|
var _capture: Capture = Capture.new(self)
|
|
53
57
|
var _waits: Waits = Waits.new(self, values)
|
|
54
58
|
|
|
@@ -82,18 +86,18 @@ func _init() -> void:
|
|
|
82
86
|
"read_text": _queries.read_text,
|
|
83
87
|
"get_rect": _queries.get_rect,
|
|
84
88
|
"get_property": _queries.get_property,
|
|
85
|
-
"set_property":
|
|
86
|
-
"call_method":
|
|
89
|
+
"set_property": _changes.set_property,
|
|
90
|
+
"call_method": _changes.call_method,
|
|
87
91
|
"get_metrics": _queries.get_metrics,
|
|
88
92
|
"capture_screenshot": _capture.capture_screenshot,
|
|
89
93
|
"capture_viewport": _capture.capture_viewport,
|
|
90
94
|
"inject_action": _input.inject_action,
|
|
91
95
|
"inject_key": _input.inject_key,
|
|
92
|
-
"inject_text":
|
|
96
|
+
"inject_text": _typing.inject_text,
|
|
93
97
|
"inject_mouse_click": _input.inject_mouse_click,
|
|
94
98
|
"inject_mouse_motion": _input.inject_mouse_motion,
|
|
95
99
|
"click": _input.click,
|
|
96
|
-
"choose":
|
|
100
|
+
"choose": _typing.choose,
|
|
97
101
|
"wait_frames": _waits.wait_frames,
|
|
98
102
|
"wait_signal": _waits.wait_signal,
|
|
99
103
|
"wait_until": _waits.wait_until,
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## What the server changes in the running game: a property written, a method called. Both take values
|
|
4
|
+
## from the wire, so both fit them to what the game declares and refuse what cannot fit, and both read
|
|
5
|
+
## back what the engine did rather than trusting that it did it.
|
|
6
|
+
|
|
7
|
+
const CallErrors = preload("call_errors.gd")
|
|
8
|
+
const Paths = preload("runtime_paths.gd")
|
|
9
|
+
const Read = preload("reading.gd")
|
|
10
|
+
const Values = preload("runtime_values.gd")
|
|
11
|
+
|
|
12
|
+
## The most errors a call's answer carries, which is enough to see what the method ran into and short
|
|
13
|
+
## of a method reporting one per element of a list of thousands.
|
|
14
|
+
const ERRORS_KEPT: int = 20
|
|
15
|
+
|
|
16
|
+
var _host: Node
|
|
17
|
+
var _values: Values
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## The host is the autoload, which is how the tree is reached: it is not in one when the
|
|
21
|
+
## modules are built, and a fixture may put it in one later.
|
|
22
|
+
func _init(host: Node, values: Values) -> void:
|
|
23
|
+
_host = host
|
|
24
|
+
_values = values
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
func set_property(params: Dictionary) -> Dictionary:
|
|
28
|
+
var node_path: String = str(params.get("path", ""))
|
|
29
|
+
var property: String = str(params.get("property", ""))
|
|
30
|
+
var value: Variant = params.get("value")
|
|
31
|
+
|
|
32
|
+
if node_path.is_empty() or property.is_empty():
|
|
33
|
+
return {"type": "error", "message": "Node path and property required"}
|
|
34
|
+
|
|
35
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
36
|
+
if standing.has("message"):
|
|
37
|
+
return standing
|
|
38
|
+
var node: Node = standing["node"]
|
|
39
|
+
|
|
40
|
+
# Through a path as well, for the reason [method Paths.walk_to] gives, and read back off the same
|
|
41
|
+
# holder afterwards: a set that does not take says so by answering with the old value, which is
|
|
42
|
+
# how a typed container refusing a write is told from one accepting it.
|
|
43
|
+
var reached: Dictionary = Paths.walk_to(node, node_path, property)
|
|
44
|
+
if reached.has("message"):
|
|
45
|
+
return reached
|
|
46
|
+
|
|
47
|
+
var holder: Variant = reached["holder"]
|
|
48
|
+
var named: String = reached["name"]
|
|
49
|
+
# A call can be walked through and read, and is not a place: what it returns is the method's
|
|
50
|
+
# to hand out, and writing "into" it would set nothing the game keeps.
|
|
51
|
+
if not Paths.method_of(named).is_empty():
|
|
52
|
+
return {
|
|
53
|
+
"type": "error",
|
|
54
|
+
"message":
|
|
55
|
+
(
|
|
56
|
+
"%s:%s is a call, and a call is not a place to write: name a property"
|
|
57
|
+
% [reached["called"], named]
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
if not Paths.can_read(holder, named):
|
|
61
|
+
return {"type": "error", "message": Paths.nothing_there(holder, named, str(reached["called"]))}
|
|
62
|
+
var old_value: Variant = Paths.read_under(holder, named)
|
|
63
|
+
# The same rule the call path holds: a value that cannot become what the property holds is
|
|
64
|
+
# refused rather than written. Writing it means the engine picks something, and what it picks
|
|
65
|
+
# for a word where a number goes is zero, which the answer then reports as the new value.
|
|
66
|
+
#
|
|
67
|
+
# A slot declaring nothing, an untyped or Variant property or an element of an untyped list, takes
|
|
68
|
+
# anything, and the engine stores what it is given there. What it holds now still says how a value
|
|
69
|
+
# of the same kind is meant, 5 into an int staying an int, and whether a path names an object, but
|
|
70
|
+
# a value of another kind is stored as sent: a word into one holding a number is the word, and a
|
|
71
|
+
# number into one holding a word is the number, not its text. The text null empties it when it
|
|
72
|
+
# holds an object or nothing, as a wait reads it on the same slot.
|
|
73
|
+
var slot: Dictionary = Values.slot_declared(holder, named)
|
|
74
|
+
var declared: int = slot["type"]
|
|
75
|
+
var declared_script: Script = slot["script"]
|
|
76
|
+
var wanted: int = declared if declared != TYPE_NIL else typeof(old_value)
|
|
77
|
+
var holds_objects: bool = (
|
|
78
|
+
declared == TYPE_OBJECT or (declared == TYPE_NIL and (old_value == null or old_value is Object))
|
|
79
|
+
)
|
|
80
|
+
var given: Variant
|
|
81
|
+
if holds_objects and names_no_object(value):
|
|
82
|
+
given = null
|
|
83
|
+
elif wanted == TYPE_OBJECT and (value is String or value is Dictionary):
|
|
84
|
+
var named_object: Dictionary = _object_named(
|
|
85
|
+
node, node_path, value, str(slot["class"]), declared_script
|
|
86
|
+
)
|
|
87
|
+
if named_object.has("message"):
|
|
88
|
+
return {
|
|
89
|
+
"type": "error",
|
|
90
|
+
"message": "%s.%s holds an object: %s" % [reached["called"], named, named_object["message"]]
|
|
91
|
+
}
|
|
92
|
+
given = named_object["object"]
|
|
93
|
+
else:
|
|
94
|
+
var as_sent: Variant = _values.fitted(value, TYPE_NIL)
|
|
95
|
+
if declared == TYPE_NIL and not Values.acceptable(as_sent, wanted):
|
|
96
|
+
given = as_sent
|
|
97
|
+
else:
|
|
98
|
+
given = _values.fitted(value, wanted)
|
|
99
|
+
if not Values.acceptable(given, wanted):
|
|
100
|
+
return {
|
|
101
|
+
"type": "error",
|
|
102
|
+
"message":
|
|
103
|
+
(
|
|
104
|
+
"%s.%s holds %s and the value given is %s, which cannot become one."
|
|
105
|
+
% [reached["called"], named, type_string(wanted), type_string(typeof(given))]
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
var typed: Dictionary = _values.typed_like(given, old_value, _element_object.bind(node, node_path))
|
|
109
|
+
if typed.has("message"):
|
|
110
|
+
return {
|
|
111
|
+
"type": "error",
|
|
112
|
+
"message":
|
|
113
|
+
"%s.%s is a typed %s: %s." % [reached["called"], named, _sort_of(old_value), typed["message"]]
|
|
114
|
+
}
|
|
115
|
+
given = typed["value"]
|
|
116
|
+
# The write alone, setter included, which is where a property's cost is.
|
|
117
|
+
var began: int = Time.get_ticks_usec()
|
|
118
|
+
Paths.write(holder, named, given)
|
|
119
|
+
var elapsed: int = Time.get_ticks_usec() - began
|
|
120
|
+
var now: Variant = Paths.read_under(holder, named)
|
|
121
|
+
# Read back rather than trusted. The engine drops a write it will not take without a word, and
|
|
122
|
+
# the answer then showed the old value as the new one, shaped as a success: a typed container
|
|
123
|
+
# refusing a plain list did exactly that. A value the property changed on the way in, a setter
|
|
124
|
+
# clamping it, still changed it, so only a write that left the property as it was is refused.
|
|
125
|
+
# And only one that asked for a change: the value the property already held, written again, left
|
|
126
|
+
# it as it was because nothing was asked of it.
|
|
127
|
+
#
|
|
128
|
+
# Said as what was seen, because two causes look the same from here: the engine refusing the write,
|
|
129
|
+
# and a setter that ran and left the value where it was, clamping it, say. The one this can name
|
|
130
|
+
# is an object slot checked only by the engine class a script extends (see
|
|
131
|
+
# [method Values.slot_declared]), where an object of that class with another script is refused.
|
|
132
|
+
#
|
|
133
|
+
# Judged by what reads back alone, so a value of another kind than the one held, which cannot be
|
|
134
|
+
# compared with it, is still caught when the engine drops it.
|
|
135
|
+
var already: bool = _held_already(given, old_value)
|
|
136
|
+
var unmoved: bool = Values.comparable(now, old_value) and now == old_value
|
|
137
|
+
if unmoved and not already:
|
|
138
|
+
var why: String = (
|
|
139
|
+
"the write left it as it was, which is what the engine refusing it and a setter keeping the"
|
|
140
|
+
+ " value both look like"
|
|
141
|
+
)
|
|
142
|
+
if declared == TYPE_OBJECT and given is Object and declared_script == null:
|
|
143
|
+
var refused_object: Object = given
|
|
144
|
+
why += (
|
|
145
|
+
(
|
|
146
|
+
". %s is declared as %s, which is also how the engine names a script class with no"
|
|
147
|
+
+ " class_name; if it is one, the object given has the right class and another script:"
|
|
148
|
+
+ " it is %s"
|
|
149
|
+
)
|
|
150
|
+
% [named, str(slot["class"]), Values.class_of(refused_object)]
|
|
151
|
+
)
|
|
152
|
+
return {
|
|
153
|
+
"type": "error",
|
|
154
|
+
"message":
|
|
155
|
+
(
|
|
156
|
+
"%s.%s was given %s and reads %s afterwards, as it did before: %s."
|
|
157
|
+
% [
|
|
158
|
+
reached["called"],
|
|
159
|
+
named,
|
|
160
|
+
JSON.stringify(_values.serialize(given)),
|
|
161
|
+
JSON.stringify(_values.serialize(now)),
|
|
162
|
+
why,
|
|
163
|
+
]
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
var answer: Dictionary = {
|
|
168
|
+
"type": "property_set",
|
|
169
|
+
"path": node_path,
|
|
170
|
+
"property": property,
|
|
171
|
+
"old_value": _values.serialize(old_value),
|
|
172
|
+
"new_value": _values.serialize(now),
|
|
173
|
+
"elapsed_usec": elapsed
|
|
174
|
+
}
|
|
175
|
+
if already and unmoved:
|
|
176
|
+
answer["unchanged"] = true
|
|
177
|
+
return answer
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
## Whether [param given] is the value [param held] already is, at the precision the property keeps.
|
|
181
|
+
##
|
|
182
|
+
## A float arrives as 64 bits and most engine properties keep 32: Camera3D.h_offset given -1.1
|
|
183
|
+
## reads back as -1.10000002384186, so comparing the two exactly took a write of the value already
|
|
184
|
+
## held for one the engine refused (#780). Rounding both to 32 bits is exact for a 32-bit property.
|
|
185
|
+
## A 64-bit one can take a write this calls the same and read back changed, and the caller calls a
|
|
186
|
+
## write unchanged only when it reads back as it was. What that cannot tell apart is a 64-bit
|
|
187
|
+
## property whose setter ignored a value differing from the held one below 32 bits: it is answered
|
|
188
|
+
## unchanged rather than refused. Vectors and colours hold 32-bit parts in the value itself, so they
|
|
189
|
+
## already compare at the property's precision.
|
|
190
|
+
static func _held_already(given: Variant, held: Variant) -> bool:
|
|
191
|
+
if not Values.comparable(given, held):
|
|
192
|
+
return false
|
|
193
|
+
if given == held:
|
|
194
|
+
return true
|
|
195
|
+
if typeof(given) != TYPE_FLOAT or typeof(held) != TYPE_FLOAT:
|
|
196
|
+
return false
|
|
197
|
+
return PackedFloat32Array([given])[0] == PackedFloat32Array([held])[0]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
func call_method(params: Dictionary) -> Dictionary:
|
|
201
|
+
var node_path: String = str(params.get("path", ""))
|
|
202
|
+
var method: String = str(params.get("method", ""))
|
|
203
|
+
var args: Array = params.get("args", [])
|
|
204
|
+
|
|
205
|
+
if node_path.is_empty() or method.is_empty():
|
|
206
|
+
return {"type": "error", "message": "Node path and method required"}
|
|
207
|
+
|
|
208
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
209
|
+
if standing.has("message"):
|
|
210
|
+
return standing
|
|
211
|
+
var node: Node = standing["node"]
|
|
212
|
+
|
|
213
|
+
# Through a path as well, for the reason [method Paths.walk_to] gives. What a game does hangs off its
|
|
214
|
+
# nodes as much as its state does, so reading `_game:run:day` while being unable to call
|
|
215
|
+
# `_game:run:advance` answers half of what a node holds and refuses the other half.
|
|
216
|
+
var reached: Dictionary = Paths.walk_to(node, node_path, method)
|
|
217
|
+
if reached.has("message"):
|
|
218
|
+
return reached
|
|
219
|
+
|
|
220
|
+
# A list or a map answers the calls a path makes on one, and nothing else: said in those terms
|
|
221
|
+
# rather than as "has no method", which reads as a misspelling of a method that was never going
|
|
222
|
+
# to be there, and with the element spelled out, since a method on what it holds is the usual aim.
|
|
223
|
+
if not reached["holder"] is Object:
|
|
224
|
+
var sort: String = "a list"
|
|
225
|
+
if reached["holder"] is Dictionary:
|
|
226
|
+
sort = "a map"
|
|
227
|
+
elif Paths.packed(reached["holder"]):
|
|
228
|
+
sort = "a packed list"
|
|
229
|
+
var spelled: String = str(reached["name"])
|
|
230
|
+
if Paths.method_of(spelled).is_empty():
|
|
231
|
+
spelled += "()"
|
|
232
|
+
if args.is_empty() and Paths.can_read(reached["holder"], spelled):
|
|
233
|
+
var read_from: int = Time.get_ticks_usec()
|
|
234
|
+
var answered: Variant = Paths.read_under(reached["holder"], spelled)
|
|
235
|
+
var read_for: int = Time.get_ticks_usec() - read_from
|
|
236
|
+
return {
|
|
237
|
+
"type": "method_result",
|
|
238
|
+
"path": node_path,
|
|
239
|
+
"method": method,
|
|
240
|
+
"result": _values.serialize(answered),
|
|
241
|
+
"elapsed_usec": read_for,
|
|
242
|
+
}
|
|
243
|
+
var why: String = (
|
|
244
|
+
"%s:%s takes no arguments" % [reached["called"], spelled]
|
|
245
|
+
if Paths.can_read(reached["holder"], spelled)
|
|
246
|
+
else Paths.nothing_there(reached["holder"], spelled, str(reached["called"]))
|
|
247
|
+
)
|
|
248
|
+
return {
|
|
249
|
+
"type": "error",
|
|
250
|
+
"message":
|
|
251
|
+
(
|
|
252
|
+
"%s. A method of what %s holds is called on the element, as in %s:0:%s"
|
|
253
|
+
% [why, sort, reached["called"], reached["name"]]
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
var holder: Object = reached["holder"]
|
|
257
|
+
var named: String = reached["name"]
|
|
258
|
+
# The method a call ends on is the one being called whether or not it carries the brackets a
|
|
259
|
+
# step along the way would: "get_viewport():gui_get_focus_owner()" is the same call as without
|
|
260
|
+
# the last pair.
|
|
261
|
+
if not Paths.method_of(named).is_empty():
|
|
262
|
+
named = Paths.method_of(named)
|
|
263
|
+
if not holder.has_method(named):
|
|
264
|
+
return {"type": "error", "message": "%s has no method %s" % [reached["called"], named]}
|
|
265
|
+
|
|
266
|
+
# Checked before the call rather than left to it. `callv` raises inside the game when an
|
|
267
|
+
# argument cannot be converted, and an error raised in a game somebody is playing holds it at a
|
|
268
|
+
# debugger break: every later call then reports a game that is not responding, which points
|
|
269
|
+
# nowhere near the argument. A wrong argument costs a refusal, never the session's game.
|
|
270
|
+
var count: String = _argument_count_refused(holder, named, args.size())
|
|
271
|
+
if not count.is_empty():
|
|
272
|
+
return {"type": "error", "message": "%s.%s %s" % [reached["called"], named, count]}
|
|
273
|
+
var deserialized_args: Array = []
|
|
274
|
+
# The object arguments checked only against the engine class, by position from 1, for the refusal
|
|
275
|
+
# the engine may still make: see [method Values.slot_declared].
|
|
276
|
+
var by_engine_class: Dictionary = {}
|
|
277
|
+
for index: int in args.size():
|
|
278
|
+
var wants: int = _values.parameter_type(holder, named, index)
|
|
279
|
+
if wants == TYPE_OBJECT and (args[index] is String or args[index] is Dictionary):
|
|
280
|
+
var declared: String = _values.parameter_class(holder, named, index)
|
|
281
|
+
var named_object: Dictionary = _object_named(node, node_path, args[index], declared)
|
|
282
|
+
if named_object.has("message"):
|
|
283
|
+
return {
|
|
284
|
+
"type": "error",
|
|
285
|
+
"message":
|
|
286
|
+
"%s.%s argument %d: %s" % [reached["called"], named, index + 1, named_object["message"]]
|
|
287
|
+
}
|
|
288
|
+
var resolved: Object = named_object["object"]
|
|
289
|
+
if resolved != null and ClassDB.class_exists(declared):
|
|
290
|
+
by_engine_class[index + 1] = [declared, Values.class_of(resolved)]
|
|
291
|
+
deserialized_args.append(named_object["object"])
|
|
292
|
+
continue
|
|
293
|
+
var given: Variant = _values.fitted(args[index], wants)
|
|
294
|
+
if not Values.acceptable(given, wants):
|
|
295
|
+
return {
|
|
296
|
+
"type": "error",
|
|
297
|
+
"message":
|
|
298
|
+
(
|
|
299
|
+
"%s.%s takes %s as argument %d and was given %s, which cannot be converted."
|
|
300
|
+
% [reached["called"], named, type_string(wants), index + 1, type_string(typeof(given))]
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
# A typed list or map is built as one, since a plain one handed to such a parameter raises
|
|
304
|
+
# inside the game, which is what every check here exists to stop.
|
|
305
|
+
var container: Variant = _values.parameter_container(holder, named, index)
|
|
306
|
+
if container != null:
|
|
307
|
+
var typed: Dictionary = _values.typed_like(
|
|
308
|
+
given, container, _element_object.bind(node, node_path)
|
|
309
|
+
)
|
|
310
|
+
if typed.has("message"):
|
|
311
|
+
return {
|
|
312
|
+
"type": "error",
|
|
313
|
+
"message":
|
|
314
|
+
(
|
|
315
|
+
"%s.%s takes a typed %s as argument %d: %s."
|
|
316
|
+
% [reached["called"], named, _sort_of(container), index + 1, typed["message"]]
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
given = typed["value"]
|
|
320
|
+
deserialized_args.append(given)
|
|
321
|
+
|
|
322
|
+
# Timed around the call alone, since the bridge's round trip is a second or more and swamps what
|
|
323
|
+
# one click's work costs; the frame metrics read whichever frame is last, which is not this one.
|
|
324
|
+
# Listened to as well, because a call the engine refused returns null like one that ran.
|
|
325
|
+
var caught: CallErrors = CallErrors.new()
|
|
326
|
+
OS.add_logger(caught)
|
|
327
|
+
var began: int = Time.get_ticks_usec()
|
|
328
|
+
var result: Variant = holder.callv(named, deserialized_args)
|
|
329
|
+
var elapsed: int = Time.get_ticks_usec() - began
|
|
330
|
+
OS.remove_logger(caught)
|
|
331
|
+
|
|
332
|
+
if not caught.refused.is_empty():
|
|
333
|
+
var message: String = (
|
|
334
|
+
"%s.%s did not run: the engine refused the call: %s"
|
|
335
|
+
% [reached["called"], named, caught.refused.trim_suffix(".")]
|
|
336
|
+
)
|
|
337
|
+
var position: RegExMatch = RegEx.create_from_string(r"Cannot convert argument (\d+)").search(
|
|
338
|
+
caught.refused
|
|
339
|
+
)
|
|
340
|
+
if position != null and by_engine_class.has(int(position.get_string(1))):
|
|
341
|
+
var checked: Array = by_engine_class[int(position.get_string(1))]
|
|
342
|
+
message += (
|
|
343
|
+
(
|
|
344
|
+
". The parameter is declared as %s, which is also how the engine names a script class"
|
|
345
|
+
+ " with no class_name; if it is one, the object given has the right class and another"
|
|
346
|
+
+ " script: it is %s"
|
|
347
|
+
)
|
|
348
|
+
% checked
|
|
349
|
+
)
|
|
350
|
+
return {"type": "error", "message": message + "."}
|
|
351
|
+
|
|
352
|
+
var answer: Dictionary = {
|
|
353
|
+
"type": "method_result",
|
|
354
|
+
"path": node_path,
|
|
355
|
+
"method": method,
|
|
356
|
+
"result": _values.serialize(result),
|
|
357
|
+
"elapsed_usec": elapsed
|
|
358
|
+
}
|
|
359
|
+
if not caught.raised.is_empty():
|
|
360
|
+
answer["errors"] = caught.raised.slice(0, ERRORS_KEPT)
|
|
361
|
+
if caught.raised.size() > ERRORS_KEPT:
|
|
362
|
+
answer["errors_not_shown"] = caught.raised.size() - ERRORS_KEPT
|
|
363
|
+
return answer
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
## Why [param given] arguments are the wrong number for [param method], or "" when they are not.
|
|
367
|
+
##
|
|
368
|
+
## Asked before the call, because `callv` given the wrong number refuses inside the game and returns
|
|
369
|
+
## null, and the method list says how many a method takes: its parameters less those with defaults,
|
|
370
|
+
## up to all of them, or any number past that for one taking varargs.
|
|
371
|
+
static func _argument_count_refused(holder: Object, method: String, given: int) -> String:
|
|
372
|
+
for entry: Dictionary in holder.get_method_list():
|
|
373
|
+
if str(entry.get("name", "")) != method:
|
|
374
|
+
continue
|
|
375
|
+
var parameters: Array = entry.get("args", [])
|
|
376
|
+
var defaults: Array = entry.get("default_args", [])
|
|
377
|
+
var most: int = parameters.size()
|
|
378
|
+
var least: int = most - defaults.size()
|
|
379
|
+
var any_more: bool = (Read.as_int(entry.get("flags", 0), 0) & METHOD_FLAG_VARARG) != 0
|
|
380
|
+
if given >= least and (given <= most or any_more):
|
|
381
|
+
return ""
|
|
382
|
+
var takes: String = "%d" % least
|
|
383
|
+
if any_more:
|
|
384
|
+
takes = "at least %d" % least
|
|
385
|
+
elif most != least:
|
|
386
|
+
takes = "%d to %d" % [least, most]
|
|
387
|
+
var one: bool = least == 1 and (most == least or any_more)
|
|
388
|
+
return "takes %s argument%s and was given %d" % [takes, "" if one else "s", given]
|
|
389
|
+
return ""
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
## [method _object_named] with the path first, which is the order [method Values.typed_like] calls
|
|
393
|
+
## an element resolver in.
|
|
394
|
+
func _element_object(
|
|
395
|
+
given: Variant, declared: String, script: Script, node: Node, node_path: String
|
|
396
|
+
) -> Dictionary:
|
|
397
|
+
return _object_named(node, node_path, given, declared, script)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
## "list" or "map", for a refusal about a typed container.
|
|
401
|
+
static func _sort_of(container: Variant) -> String:
|
|
402
|
+
return "list" if container is Array else "map"
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
## The object [param given] names for a slot or a parameter declared as [param declared], as
|
|
406
|
+
## `{"object": ...}`, or `{"message": ...}` saying why it names none that fits.
|
|
407
|
+
##
|
|
408
|
+
## JSON cannot carry an object, so one the game holds is named by its path, read from the node the
|
|
409
|
+
## call or the write was made on, and handed over as that instance rather than a copy: a method
|
|
410
|
+
## given a piece of the game's state acts on the piece the game keeps, and a slot written with one
|
|
411
|
+
## holds the game's own.
|
|
412
|
+
##
|
|
413
|
+
## The text null names no object, which is what an object slot or parameter takes to be emptied. A
|
|
414
|
+
## caller's null arrived as that text in #772 and #783, and was looked up as a node called "null".
|
|
415
|
+
##
|
|
416
|
+
## Checked against [param script] where the declaration hands one over, which only a typed container
|
|
417
|
+
## does: see [method Values.is_a].
|
|
418
|
+
func _object_named(
|
|
419
|
+
node: Node, node_path: String, given: Variant, declared: String, script: Script = null
|
|
420
|
+
) -> Dictionary:
|
|
421
|
+
if names_no_object(given):
|
|
422
|
+
return {"object": null}
|
|
423
|
+
var found: Dictionary = Paths.object_at(_host.get_tree().root, node, node_path, given)
|
|
424
|
+
if found.has("message"):
|
|
425
|
+
return found
|
|
426
|
+
var instance: Object = found["object"]
|
|
427
|
+
if script != null and not Values.is_a(instance, declared, script):
|
|
428
|
+
var wanted: String = "an instance of %s" % Values.script_named(script)
|
|
429
|
+
if not script.get_global_name().is_empty():
|
|
430
|
+
wanted = "a %s" % script.get_global_name()
|
|
431
|
+
return {"message": "%s is %s, not %s" % [str(given), Values.class_of(instance), wanted]}
|
|
432
|
+
if script == null and not declared.is_empty() and not Values.is_a(instance, declared):
|
|
433
|
+
return {"message": "%s is %s, not a %s" % [str(given), Values.class_of(instance), declared]}
|
|
434
|
+
return found
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
## Whether [param given] is the text null, which is how a caller's null arrives. Public because a
|
|
438
|
+
## wait reads the same text the same way.
|
|
439
|
+
static func names_no_object(given: Variant) -> bool:
|
|
440
|
+
return given is String and str(given).strip_edges() == "null"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://c4w4bcnxrw1tx
|