gdharness 1.2.28 → 1.2.30
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 +395 -169
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +259 -31
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +168 -98
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +33 -7
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +10 -3
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +89 -15
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +3 -3
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +142 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +1 -13
- package/build/index.js +384 -169
- package/build/keeper.js +20 -17
- package/package.json +1 -1
|
@@ -32,15 +32,16 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
32
32
|
if node_path.is_empty() or property.is_empty():
|
|
33
33
|
return {"type": "error", "message": "Node path and property required"}
|
|
34
34
|
|
|
35
|
-
var standing: Dictionary = Values.
|
|
35
|
+
var standing: Dictionary = Values.target_at(_host.get_tree().root, node_path)
|
|
36
36
|
if standing.has("message"):
|
|
37
37
|
return standing
|
|
38
38
|
var node: Node = standing["node"]
|
|
39
|
+
var target: Object = standing["object"]
|
|
39
40
|
|
|
40
41
|
# Through a path as well, for the reason [method Paths.walk_to] gives, and read back off the same
|
|
41
42
|
# holder afterwards: a set that does not take says so by answering with the old value, which is
|
|
42
43
|
# how a typed container refusing a write is told from one accepting it.
|
|
43
|
-
var reached: Dictionary = Paths.walk_to(
|
|
44
|
+
var reached: Dictionary = Paths.walk_to(target, node_path, property)
|
|
44
45
|
if reached.has("message"):
|
|
45
46
|
return reached
|
|
46
47
|
|
|
@@ -60,70 +61,35 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
60
61
|
if not Paths.can_read(holder, named):
|
|
61
62
|
return {"type": "error", "message": Paths.nothing_there(holder, named, str(reached["called"]))}
|
|
62
63
|
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
64
|
var slot: Dictionary = Values.slot_declared(holder, named)
|
|
74
65
|
var declared: int = slot["type"]
|
|
75
66
|
var declared_script: Script = slot["script"]
|
|
76
|
-
var
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
)
|
|
87
|
-
if
|
|
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.%s"
|
|
105
|
-
% [
|
|
106
|
-
reached["called"],
|
|
107
|
-
named,
|
|
108
|
-
type_string(wanted),
|
|
109
|
-
type_string(typeof(given)),
|
|
110
|
-
_written_as(wanted)
|
|
111
|
-
]
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
var typed: Dictionary = _values.typed_like(given, old_value, _element_object.bind(node, node_path))
|
|
115
|
-
if typed.has("message"):
|
|
116
|
-
return {
|
|
117
|
-
"type": "error",
|
|
118
|
-
"message":
|
|
119
|
-
"%s.%s is a typed %s: %s." % [reached["called"], named, _sort_of(old_value), typed["message"]]
|
|
120
|
-
}
|
|
121
|
-
given = typed["value"]
|
|
67
|
+
var fitting: Dictionary = _value_for(value, slot, old_value, reached, node, node_path)
|
|
68
|
+
if fitting.has("message"):
|
|
69
|
+
return fitting
|
|
70
|
+
var given: Variant = fitting["value"]
|
|
71
|
+
# A component of a vector or a transform is written into a copy, which is put back where it was
|
|
72
|
+
# read from, setter and all; the read back is then taken off the game again, since the copy here
|
|
73
|
+
# says only what was written into it.
|
|
74
|
+
var chain: Array[Dictionary] = reached["chain"]
|
|
75
|
+
var copied: bool = Paths.composite(holder)
|
|
76
|
+
if copied:
|
|
77
|
+
var unwritable: String = Paths.unwritable_copy(chain)
|
|
78
|
+
if not unwritable.is_empty():
|
|
79
|
+
return {"type": "error", "message": unwritable}
|
|
122
80
|
# The write alone, setter included, which is where a property's cost is.
|
|
123
81
|
var began: int = Time.get_ticks_usec()
|
|
124
|
-
Paths.write(holder, named, given)
|
|
82
|
+
var written: Variant = Paths.write(holder, named, given)
|
|
83
|
+
if copied:
|
|
84
|
+
Paths.write_back(chain, written)
|
|
125
85
|
var elapsed: int = Time.get_ticks_usec() - began
|
|
126
86
|
var now: Variant = Paths.read_under(holder, named)
|
|
87
|
+
if copied:
|
|
88
|
+
var again: Dictionary = Paths.walk_to(target, node_path, property)
|
|
89
|
+
if again.has("message"):
|
|
90
|
+
return again
|
|
91
|
+
var reread: Variant = again["holder"]
|
|
92
|
+
now = Paths.read_under(reread, named) if Paths.can_read(reread, named) else null
|
|
127
93
|
# Read back rather than trusted. The engine drops a write it will not take without a word, and
|
|
128
94
|
# the answer then showed the old value as the new one, shaped as a success: a typed container
|
|
129
95
|
# refusing a plain list did exactly that. A value the property changed on the way in, a setter
|
|
@@ -183,6 +149,131 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
183
149
|
return answer
|
|
184
150
|
|
|
185
151
|
|
|
152
|
+
## What [param sent] becomes as argument [param index] of the method [param named] on the holder
|
|
153
|
+
## [param reached] ends at, as [method Paths.walk_to] answered it, as `{"value": ...}`, or the refusal
|
|
154
|
+
## saying why it cannot become one: a number the parameter would have to cut, a value that cannot be
|
|
155
|
+
## converted, or a list or map that cannot be built as the typed one the parameter takes.
|
|
156
|
+
func _argument_for(
|
|
157
|
+
sent: Variant, index: int, reached: Dictionary, named: String, node: Node, node_path: String
|
|
158
|
+
) -> Dictionary:
|
|
159
|
+
var holder: Object = reached["holder"]
|
|
160
|
+
var called: String = reached["called"]
|
|
161
|
+
var wants: int = _values.parameter_type(holder, named, index)
|
|
162
|
+
if Values.cuts(sent, wants):
|
|
163
|
+
return {
|
|
164
|
+
"type": "error",
|
|
165
|
+
"message":
|
|
166
|
+
(
|
|
167
|
+
"%s.%s takes %s as argument %d and was given %s, which it would have to be cut to fit"
|
|
168
|
+
% [called, named, type_string(wants), index + 1, JSON.stringify(sent)]
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
var given: Variant = _values.fitted(sent, wants)
|
|
172
|
+
if not Values.acceptable(given, wants):
|
|
173
|
+
return {
|
|
174
|
+
"type": "error",
|
|
175
|
+
"message":
|
|
176
|
+
(
|
|
177
|
+
"%s.%s takes %s as argument %d and was given %s, which cannot be converted.%s"
|
|
178
|
+
% [
|
|
179
|
+
called,
|
|
180
|
+
named,
|
|
181
|
+
type_string(wants),
|
|
182
|
+
index + 1,
|
|
183
|
+
type_string(typeof(given)),
|
|
184
|
+
_written_as(wants)
|
|
185
|
+
]
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
# A typed list or map is built as one, since a plain one handed to such a parameter raises
|
|
189
|
+
# inside the game, which is what every check here exists to stop.
|
|
190
|
+
var container: Variant = _values.parameter_container(holder, named, index)
|
|
191
|
+
if container == null:
|
|
192
|
+
return {"value": given}
|
|
193
|
+
var typed: Dictionary = _values.typed_like(given, container, _element_object.bind(node, node_path))
|
|
194
|
+
if typed.has("message"):
|
|
195
|
+
return {
|
|
196
|
+
"type": "error",
|
|
197
|
+
"message":
|
|
198
|
+
(
|
|
199
|
+
"%s.%s takes a typed %s as argument %d: %s."
|
|
200
|
+
% [called, named, _sort_of(container), index + 1, typed["message"]]
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
return {"value": typed["value"]}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
## What [param value] becomes written into the slot [param reached] ends on, as
|
|
207
|
+
## [method Paths.walk_to] answered it, as `{"value": ...}`, or the refusal saying why it cannot become
|
|
208
|
+
## one.
|
|
209
|
+
##
|
|
210
|
+
## The same rule the call path holds: a value that cannot become what the property holds is
|
|
211
|
+
## refused rather than written. Writing it means the engine picks something, and what it picks
|
|
212
|
+
## for a word where a number goes is zero, which the answer then reports as the new value. So is a
|
|
213
|
+
## number a whole number or a truth would have to cut, since converting one truncates.
|
|
214
|
+
##
|
|
215
|
+
## A slot declaring nothing, an untyped or Variant property or an element of an untyped list, takes
|
|
216
|
+
## anything, and the engine stores what it is given there. What it holds now still says how a value
|
|
217
|
+
## of the same kind is meant, 5 into an int staying an int, and whether a path names an object, but
|
|
218
|
+
## a value of another kind is stored as sent: a word into one holding a number is the word, a
|
|
219
|
+
## fraction into one holding a whole number is the fraction, and a number into one holding a word is
|
|
220
|
+
## the number, not its text. The text null empties it when it holds an object or nothing, as a wait
|
|
221
|
+
## reads it on the same slot.
|
|
222
|
+
func _value_for(
|
|
223
|
+
value: Variant, slot: Dictionary, old_value: Variant, reached: Dictionary, node: Node, node_path: String
|
|
224
|
+
) -> Dictionary:
|
|
225
|
+
var named: String = reached["name"]
|
|
226
|
+
var called: String = reached["called"]
|
|
227
|
+
var declared: int = slot["type"]
|
|
228
|
+
var declared_script: Script = slot["script"]
|
|
229
|
+
var wanted: int = declared if declared != TYPE_NIL else typeof(old_value)
|
|
230
|
+
var holds_objects: bool = (
|
|
231
|
+
declared == TYPE_OBJECT or (declared == TYPE_NIL and (old_value == null or old_value is Object))
|
|
232
|
+
)
|
|
233
|
+
if holds_objects and names_no_object(value):
|
|
234
|
+
return {"value": null}
|
|
235
|
+
if wanted == TYPE_OBJECT and (value is String or value is Dictionary):
|
|
236
|
+
var named_object: Dictionary = _object_named(
|
|
237
|
+
node, node_path, value, str(slot["class"]), declared_script
|
|
238
|
+
)
|
|
239
|
+
if named_object.has("message"):
|
|
240
|
+
return {
|
|
241
|
+
"type": "error",
|
|
242
|
+
"message": "%s.%s holds an object: %s" % [called, named, named_object["message"]]
|
|
243
|
+
}
|
|
244
|
+
return {"value": named_object["object"]}
|
|
245
|
+
var as_sent: Variant = _values.fitted(value, TYPE_NIL)
|
|
246
|
+
var cut: bool = Values.cuts(value, wanted)
|
|
247
|
+
var given: Variant = as_sent
|
|
248
|
+
if declared != TYPE_NIL and cut:
|
|
249
|
+
return {
|
|
250
|
+
"type": "error",
|
|
251
|
+
"message":
|
|
252
|
+
(
|
|
253
|
+
"%s.%s holds %s and the value given is %s, which it would have to be cut to fit"
|
|
254
|
+
% [called, named, type_string(wanted), JSON.stringify(value)]
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
if declared != TYPE_NIL or (not cut and Values.acceptable(as_sent, wanted)):
|
|
258
|
+
given = _values.fitted(value, wanted)
|
|
259
|
+
if not Values.acceptable(given, wanted):
|
|
260
|
+
return {
|
|
261
|
+
"type": "error",
|
|
262
|
+
"message":
|
|
263
|
+
(
|
|
264
|
+
"%s.%s holds %s and the value given is %s, which cannot become one.%s"
|
|
265
|
+
% [called, named, type_string(wanted), type_string(typeof(given)), _written_as(wanted)]
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
var typed: Dictionary = _values.typed_like(given, old_value, _element_object.bind(node, node_path))
|
|
269
|
+
if typed.has("message"):
|
|
270
|
+
return {
|
|
271
|
+
"type": "error",
|
|
272
|
+
"message": "%s.%s is a typed %s: %s." % [called, named, _sort_of(old_value), typed["message"]]
|
|
273
|
+
}
|
|
274
|
+
return {"value": typed["value"]}
|
|
275
|
+
|
|
276
|
+
|
|
186
277
|
## Whether [param given] is the value [param held] already is, at the precision the property keeps.
|
|
187
278
|
##
|
|
188
279
|
## A float arrives as 64 bits and most engine properties keep 32: Camera3D.h_offset given -1.1
|
|
@@ -211,15 +302,16 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
211
302
|
if node_path.is_empty() or method.is_empty():
|
|
212
303
|
return {"type": "error", "message": "Node path and method required"}
|
|
213
304
|
|
|
214
|
-
var standing: Dictionary = Values.
|
|
305
|
+
var standing: Dictionary = Values.target_at(_host.get_tree().root, node_path)
|
|
215
306
|
if standing.has("message"):
|
|
216
307
|
return standing
|
|
217
308
|
var node: Node = standing["node"]
|
|
309
|
+
var target: Object = standing["object"]
|
|
218
310
|
|
|
219
311
|
# Through a path as well, for the reason [method Paths.walk_to] gives. What a game does hangs off its
|
|
220
312
|
# nodes as much as its state does, so reading `_game:run:day` while being unable to call
|
|
221
313
|
# `_game:run:advance` answers half of what a node holds and refuses the other half.
|
|
222
|
-
var reached: Dictionary = Paths.walk_to(
|
|
314
|
+
var reached: Dictionary = Paths.walk_to(target, node_path, method)
|
|
223
315
|
if reached.has("message"):
|
|
224
316
|
return reached
|
|
225
317
|
|
|
@@ -235,6 +327,12 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
235
327
|
var spelled: String = str(reached["name"])
|
|
236
328
|
if Paths.method_of(spelled).is_empty():
|
|
237
329
|
spelled += "()"
|
|
330
|
+
# A vector or a transform has no element to call a method on: it is read by its components.
|
|
331
|
+
if Paths.composite(reached["holder"]):
|
|
332
|
+
return {
|
|
333
|
+
"type": "error",
|
|
334
|
+
"message": Paths.nothing_there(reached["holder"], spelled, str(reached["called"]))
|
|
335
|
+
}
|
|
238
336
|
if args.is_empty() and Paths.can_read(reached["holder"], spelled):
|
|
239
337
|
var read_from: int = Time.get_ticks_usec()
|
|
240
338
|
var answered: Variant = Paths.read_under(reached["holder"], spelled)
|
|
@@ -301,41 +399,10 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
301
399
|
by_engine_class[index + 1] = [declared, Values.class_of(resolved)]
|
|
302
400
|
deserialized_args.append(named_object["object"])
|
|
303
401
|
continue
|
|
304
|
-
var
|
|
305
|
-
if
|
|
306
|
-
return
|
|
307
|
-
|
|
308
|
-
"message":
|
|
309
|
-
(
|
|
310
|
-
"%s.%s takes %s as argument %d and was given %s, which cannot be converted.%s"
|
|
311
|
-
% [
|
|
312
|
-
reached["called"],
|
|
313
|
-
named,
|
|
314
|
-
type_string(wants),
|
|
315
|
-
index + 1,
|
|
316
|
-
type_string(typeof(given)),
|
|
317
|
-
_written_as(wants)
|
|
318
|
-
]
|
|
319
|
-
)
|
|
320
|
-
}
|
|
321
|
-
# A typed list or map is built as one, since a plain one handed to such a parameter raises
|
|
322
|
-
# inside the game, which is what every check here exists to stop.
|
|
323
|
-
var container: Variant = _values.parameter_container(holder, named, index)
|
|
324
|
-
if container != null:
|
|
325
|
-
var typed: Dictionary = _values.typed_like(
|
|
326
|
-
given, container, _element_object.bind(node, node_path)
|
|
327
|
-
)
|
|
328
|
-
if typed.has("message"):
|
|
329
|
-
return {
|
|
330
|
-
"type": "error",
|
|
331
|
-
"message":
|
|
332
|
-
(
|
|
333
|
-
"%s.%s takes a typed %s as argument %d: %s."
|
|
334
|
-
% [reached["called"], named, _sort_of(container), index + 1, typed["message"]]
|
|
335
|
-
)
|
|
336
|
-
}
|
|
337
|
-
given = typed["value"]
|
|
338
|
-
deserialized_args.append(given)
|
|
402
|
+
var argument: Dictionary = _argument_for(args[index], index, reached, named, node, node_path)
|
|
403
|
+
if argument.has("message"):
|
|
404
|
+
return argument
|
|
405
|
+
deserialized_args.append(argument["value"])
|
|
339
406
|
|
|
340
407
|
# Timed around the call alone, since the bridge's round trip is a second or more and swamps what
|
|
341
408
|
# one click's work costs; the frame metrics read whichever frame is last, which is not this one.
|
|
@@ -461,8 +528,11 @@ static func _walk_from(object: Object, path: String) -> Dictionary:
|
|
|
461
528
|
var holder: Variant = object
|
|
462
529
|
var called: String = "the result"
|
|
463
530
|
for named: String in Paths.steps_of(path):
|
|
464
|
-
if not
|
|
465
|
-
return {
|
|
531
|
+
if not Paths.can_hold(holder):
|
|
532
|
+
return {
|
|
533
|
+
"message":
|
|
534
|
+
"%s holds %s, which has nothing to read %s off" % [called, type_string(typeof(holder)), named]
|
|
535
|
+
}
|
|
466
536
|
if not Paths.can_read(holder, named):
|
|
467
537
|
return {"message": Paths.nothing_there(holder, named, called)}
|
|
468
538
|
holder = Paths.read_under(holder, named)
|
|
@@ -122,12 +122,16 @@ func inject_action(params: Dictionary) -> Dictionary:
|
|
|
122
122
|
|
|
123
123
|
if not InputMap.has_action(action):
|
|
124
124
|
return {"type": "error", "message": "Action not found: " + action}
|
|
125
|
+
var holding: Variant = Values.held_for(params)
|
|
126
|
+
if holding is String:
|
|
127
|
+
return {"type": "error", "message": holding}
|
|
128
|
+
var frames: int = holding
|
|
125
129
|
|
|
126
130
|
_say_action(action, whole or held, strength)
|
|
127
131
|
if whole:
|
|
128
|
-
# A frame between the halves, as a click has: a listener that acts on the press and
|
|
129
|
-
# listener that acts on the release both get their own frame to do it in.
|
|
130
|
-
await
|
|
132
|
+
# A frame between the halves at least, as a click has: a listener that acts on the press and
|
|
133
|
+
# a listener that acts on the release both get their own frame to do it in.
|
|
134
|
+
await _held(frames)
|
|
131
135
|
_say_action(action, false, strength)
|
|
132
136
|
await _host.get_tree().process_frame
|
|
133
137
|
|
|
@@ -137,9 +141,16 @@ func inject_action(params: Dictionary) -> Dictionary:
|
|
|
137
141
|
"action": action,
|
|
138
142
|
"pressed": held and not whole,
|
|
139
143
|
"whole": whole,
|
|
144
|
+
"held_frames": Values.held_answer(whole, frames),
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
|
|
148
|
+
## Waits out [param frames] process frames, which is how long a whole press is held down.
|
|
149
|
+
func _held(frames: int) -> void:
|
|
150
|
+
for _frame: int in frames:
|
|
151
|
+
await _host.get_tree().process_frame
|
|
152
|
+
|
|
153
|
+
|
|
143
154
|
func _say_action(action: String, down: bool, strength: float) -> void:
|
|
144
155
|
var event: InputEventAction = InputEventAction.new()
|
|
145
156
|
event.action = action
|
|
@@ -160,6 +171,10 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
160
171
|
if not named.is_empty() and key_label.is_empty():
|
|
161
172
|
key_label = named
|
|
162
173
|
var keycode: int = 0 if keycode_raw is String else Read.as_int(keycode_raw)
|
|
174
|
+
var holding: Variant = Values.held_for(params)
|
|
175
|
+
if holding is String:
|
|
176
|
+
return {"type": "error", "message": holding}
|
|
177
|
+
var frames: int = holding
|
|
163
178
|
|
|
164
179
|
var event: InputEventKey = InputEventKey.new()
|
|
165
180
|
event.pressed = whole or held
|
|
@@ -193,7 +208,7 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
193
208
|
|
|
194
209
|
Input.parse_input_event(event)
|
|
195
210
|
if whole:
|
|
196
|
-
await
|
|
211
|
+
await _held(frames)
|
|
197
212
|
var up: InputEventKey = event.duplicate()
|
|
198
213
|
up.pressed = false
|
|
199
214
|
Input.parse_input_event(up)
|
|
@@ -202,6 +217,7 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
202
217
|
return {
|
|
203
218
|
"type": "input_injected",
|
|
204
219
|
"input_type": "key",
|
|
220
|
+
"held_frames": Values.held_answer(whole, frames),
|
|
205
221
|
"keycode": event.keycode,
|
|
206
222
|
"physical_keycode": event.physical_keycode,
|
|
207
223
|
"shift": event.shift_pressed,
|
|
@@ -266,6 +282,15 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
266
282
|
# than under the pointer, landing true and doing nothing, until a release was sent by hand.
|
|
267
283
|
var notch: bool = held and _mask_of(button) == 0
|
|
268
284
|
var whole: bool = notch or not params.has("pressed")
|
|
285
|
+
var holding: Variant = Values.held_for(params)
|
|
286
|
+
if holding is String:
|
|
287
|
+
return {"type": "error", "message": holding}
|
|
288
|
+
var frames: int = holding
|
|
289
|
+
if notch and params.has("holdFrames"):
|
|
290
|
+
return {
|
|
291
|
+
"type": "error",
|
|
292
|
+
"message": "A wheel step is pressed and let go at once, so holdFrames cannot hold one."
|
|
293
|
+
}
|
|
269
294
|
|
|
270
295
|
# A button pressed somewhere puts the pointer there: the motion after a held press measures
|
|
271
296
|
# its drag from the press, which is where a real pointer would be.
|
|
@@ -274,9 +299,9 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
274
299
|
if notch:
|
|
275
300
|
Input.parse_input_event(_button(position, button, false, false))
|
|
276
301
|
elif whole:
|
|
277
|
-
# A frame between the halves, as inject_action has, so a control that acts on the
|
|
278
|
-
# one that acts on the release each get a frame to do it in.
|
|
279
|
-
await
|
|
302
|
+
# A frame between the halves at least, as inject_action has, so a control that acts on the
|
|
303
|
+
# press and one that acts on the release each get a frame to do it in.
|
|
304
|
+
await _held(frames)
|
|
280
305
|
Input.parse_input_event(_button(position, button, false, false))
|
|
281
306
|
await _host.get_tree().process_frame
|
|
282
307
|
|
|
@@ -284,6 +309,7 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
284
309
|
{
|
|
285
310
|
"type": "input_injected",
|
|
286
311
|
"input_type": "mouse_click",
|
|
312
|
+
"held_frames": Values.held_answer(whole and not notch, frames),
|
|
287
313
|
"position": [position.x, position.y],
|
|
288
314
|
"button": button,
|
|
289
315
|
"pressed": held and not whole,
|
|
@@ -9,6 +9,7 @@ extends RefCounted
|
|
|
9
9
|
## calling the game's own methods. The engine still fires the actions bound to a button from these.
|
|
10
10
|
|
|
11
11
|
const Read = preload("reading.gd")
|
|
12
|
+
const Values = preload("runtime_values.gd")
|
|
12
13
|
|
|
13
14
|
## Controller buttons by the names Godot gives them, for a caller naming one rather than numbering it.
|
|
14
15
|
const JOY_BUTTONS: Dictionary[String, int] = {
|
|
@@ -62,18 +63,24 @@ func inject_joypad_button(params: Dictionary) -> Dictionary:
|
|
|
62
63
|
var device: int = Read.as_int(params.get("device", 0), 0)
|
|
63
64
|
var held: bool = Read.as_bool(params.get("pressed", true), true)
|
|
64
65
|
var whole: bool = not params.has("pressed")
|
|
66
|
+
var holding: Variant = Values.held_for(params)
|
|
67
|
+
if holding is String:
|
|
68
|
+
return {"type": "error", "message": holding}
|
|
69
|
+
var frames: int = holding
|
|
65
70
|
|
|
66
71
|
Input.parse_input_event(_button(button, device, whole or held))
|
|
67
72
|
if whole:
|
|
68
|
-
# A frame between the halves, as a key has, so a game that acts on the press and one
|
|
69
|
-
# acts on the release each get a frame to do it in.
|
|
70
|
-
|
|
73
|
+
# A frame between the halves at least, as a key has, so a game that acts on the press and one
|
|
74
|
+
# that acts on the release each get a frame to do it in.
|
|
75
|
+
for _frame: int in frames:
|
|
76
|
+
await _host.get_tree().process_frame
|
|
71
77
|
Input.parse_input_event(_button(button, device, false))
|
|
72
78
|
await _host.get_tree().process_frame
|
|
73
79
|
|
|
74
80
|
return {
|
|
75
81
|
"type": "input_injected",
|
|
76
82
|
"input_type": "joypad_button",
|
|
83
|
+
"held_frames": Values.held_answer(whole, frames),
|
|
77
84
|
"button": button,
|
|
78
85
|
"button_name": _name_of(button, JOY_BUTTONS),
|
|
79
86
|
"device": device,
|
|
@@ -93,23 +93,37 @@ const READS_ONLY: String = ", and a path, which a wait reads every frame, only c
|
|
|
93
93
|
## "get_theme_stylebox(\"panel\"):content_margin_top", since what a control's theme gives it is only
|
|
94
94
|
## reachable by naming what is asked for; see [method arguments_of].
|
|
95
95
|
##
|
|
96
|
+
## A step into a vector, a colour, a rectangle or a transform reads one of its components,
|
|
97
|
+
## "size:x" or "global_transform:origin:y", by the names GDScript gives them; see
|
|
98
|
+
## [method Values.components_of].
|
|
99
|
+
##
|
|
96
100
|
## Answers with a `message` instead when a step along the way is not there or holds something that
|
|
97
101
|
## is not an object, naming the step rather than the whole path: "/root/Main:_game has no property
|
|
98
|
-
## clocks" is a typo found, and "no property _game:clocks:speed" is a puzzle.
|
|
99
|
-
|
|
102
|
+
## clocks" is a typo found, and "no property _game:clocks:speed" is a puzzle. Answers `chain` as
|
|
103
|
+
## well, each holder walked through and the step taken off it, for [method write_back].
|
|
104
|
+
static func walk_to(node: Object, node_path: String, reaching: String) -> Dictionary:
|
|
100
105
|
var parts: PackedStringArray = steps_of(reaching)
|
|
101
106
|
var holder: Variant = node
|
|
102
107
|
var called: String = node_path
|
|
108
|
+
var chain: Array[Dictionary] = []
|
|
103
109
|
for step: int in parts.size() - 1:
|
|
104
110
|
var named: String = parts[step]
|
|
105
111
|
if not can_read(holder, named):
|
|
106
112
|
return {"type": "error", "message": nothing_there(holder, named, called)}
|
|
113
|
+
chain.append({"holder": holder, "named": named, "called": called})
|
|
107
114
|
holder = read_under(holder, named)
|
|
108
115
|
called = "%s:%s" % [called, named]
|
|
109
|
-
if not
|
|
116
|
+
if not can_hold(holder):
|
|
110
117
|
var wanted: String = parts[step + 1]
|
|
111
|
-
return {
|
|
112
|
-
|
|
118
|
+
return {
|
|
119
|
+
"type": "error",
|
|
120
|
+
"message":
|
|
121
|
+
(
|
|
122
|
+
"%s holds %s, which has nothing to read %s off"
|
|
123
|
+
% [called, type_string(typeof(holder)), wanted]
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
return {"holder": holder, "name": parts[parts.size() - 1], "called": called, "chain": chain}
|
|
113
127
|
|
|
114
128
|
|
|
115
129
|
## The object [param given] names for a method's argument, as `{"object": ...}`, or as
|
|
@@ -160,10 +174,17 @@ static func object_at(root: Node, node: Node, node_path: String, given: Variant)
|
|
|
160
174
|
return {"object": value}
|
|
161
175
|
|
|
162
176
|
|
|
163
|
-
## Whether a step can be taken into [param value] at all: an object, a list
|
|
164
|
-
## into, and a number or a string is where a path ends whether the caller
|
|
165
|
-
|
|
166
|
-
|
|
177
|
+
## Whether a step can be taken into [param value] at all: an object, a list, a map or a value with
|
|
178
|
+
## components can be walked into, and a number or a string is where a path ends whether the caller
|
|
179
|
+
## meant it to or not.
|
|
180
|
+
static func can_hold(value: Variant) -> bool:
|
|
181
|
+
return value is Object or value is Array or value is Dictionary or packed(value) or composite(value)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
## Whether [param value] is read by its components, a vector or a colour or a transform, and is a
|
|
185
|
+
## copy wherever it is read from.
|
|
186
|
+
static func composite(value: Variant) -> bool:
|
|
187
|
+
return not Values.components_of(value).is_empty()
|
|
167
188
|
|
|
168
189
|
|
|
169
190
|
## Whether [param value] is a packed list. Stepped into the way a list is, by index, and a write into
|
|
@@ -212,6 +233,8 @@ static func can_read(holder: Variant, named: String) -> bool:
|
|
|
212
233
|
if not method.is_empty():
|
|
213
234
|
return args.is_empty() and method in MAP_CALLS
|
|
214
235
|
return not _key_in(map, named).is_empty()
|
|
236
|
+
if composite(holder):
|
|
237
|
+
return method.is_empty() and named in Values.components_of(holder)
|
|
215
238
|
if holder is Object:
|
|
216
239
|
var object: Object = holder
|
|
217
240
|
if not method.is_empty():
|
|
@@ -420,25 +443,62 @@ static func _native_flags(object: Object, method: String) -> int:
|
|
|
420
443
|
return 0
|
|
421
444
|
|
|
422
445
|
|
|
423
|
-
## Put [param value] where [param named] points
|
|
424
|
-
##
|
|
425
|
-
|
|
446
|
+
## Put [param value] where [param named] points, and answer the holder written into. Only ever
|
|
447
|
+
## called once [method can_read] agrees. Lists and maps are reference types, so writing into one
|
|
448
|
+
## reaches the game's own copy; a vector or a transform is a copy, and the one answered is the
|
|
449
|
+
## changed copy, for [method write_back] to put where it came from.
|
|
450
|
+
static func write(holder: Variant, named: String, value: Variant) -> Variant:
|
|
426
451
|
if holder is Array:
|
|
427
452
|
var items: Array = holder
|
|
428
453
|
items[_index_in(named, items.size())] = value
|
|
429
|
-
return
|
|
454
|
+
return holder
|
|
430
455
|
if packed(holder):
|
|
431
456
|
var list: Variant = holder
|
|
432
457
|
list[_index_in(named, len(holder))] = value
|
|
433
|
-
return
|
|
458
|
+
return holder
|
|
434
459
|
if holder is Dictionary:
|
|
435
460
|
var map: Dictionary = holder
|
|
436
461
|
# Written back under the key it was found under, since a map keyed by StringName and one
|
|
437
462
|
# keyed by String both read the same way and a write to the wrong one adds a second entry.
|
|
438
463
|
map[_key_in(map, named)[0]] = value
|
|
439
|
-
return
|
|
464
|
+
return holder
|
|
465
|
+
if composite(holder):
|
|
466
|
+
var changed: Variant = holder
|
|
467
|
+
changed[named] = value
|
|
468
|
+
return changed
|
|
440
469
|
var object: Object = holder
|
|
441
470
|
object.set(named, value)
|
|
471
|
+
return holder
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
## Why a write into [param chain]'s last holder, a copy, cannot reach the game, or "" when it can:
|
|
475
|
+
## each copy is put back where it was read from on the way up, and a step that was a call answered
|
|
476
|
+
## a copy the game does not keep anywhere. [param chain] is the one [method walk_to] answered.
|
|
477
|
+
static func unwritable_copy(chain: Array[Dictionary]) -> String:
|
|
478
|
+
for index: int in range(chain.size() - 1, -1, -1):
|
|
479
|
+
var link: Dictionary = chain[index]
|
|
480
|
+
var named: String = link["named"]
|
|
481
|
+
if not method_of(named).is_empty():
|
|
482
|
+
return (
|
|
483
|
+
"%s:%s is what a call answered, a copy the game keeps nowhere, so a part of it is not a place to write"
|
|
484
|
+
% [link["called"], named]
|
|
485
|
+
)
|
|
486
|
+
if not composite(link["holder"]):
|
|
487
|
+
return ""
|
|
488
|
+
return ""
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
## Puts [param changed], the copy a write into a vector or a transform changed, back where
|
|
492
|
+
## [param chain] read it from, and every copy it was read out of back where that came from, up to
|
|
493
|
+
## the first holder keeping its own: an object, whose setter runs, a list or a map.
|
|
494
|
+
static func write_back(chain: Array[Dictionary], changed: Variant) -> void:
|
|
495
|
+
var value: Variant = changed
|
|
496
|
+
for index: int in range(chain.size() - 1, -1, -1):
|
|
497
|
+
var link: Dictionary = chain[index]
|
|
498
|
+
var holder: Variant = link["holder"]
|
|
499
|
+
value = write(holder, str(link["named"]), value)
|
|
500
|
+
if not composite(holder):
|
|
501
|
+
return
|
|
442
502
|
|
|
443
503
|
|
|
444
504
|
## What [param holder] holds under [param named], or answers to it when [param named] is a call.
|
|
@@ -455,6 +515,8 @@ static func read_under(holder: Variant, named: String) -> Variant:
|
|
|
455
515
|
if holder is Dictionary:
|
|
456
516
|
var map: Dictionary = holder
|
|
457
517
|
return map[_key_in(map, named)[0]]
|
|
518
|
+
if composite(holder):
|
|
519
|
+
return holder[named]
|
|
458
520
|
var object: Object = holder
|
|
459
521
|
var method: String = method_of(named)
|
|
460
522
|
if not method.is_empty():
|
|
@@ -521,6 +583,18 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
521
583
|
if keys.size() > 8:
|
|
522
584
|
rest = " and %d more" % [keys.size() - 8]
|
|
523
585
|
return "%s has no key %s; it is keyed by %s%s" % [called_as, named, ", ".join(some), rest]
|
|
586
|
+
if composite(holder):
|
|
587
|
+
var kind: String = type_string(typeof(holder))
|
|
588
|
+
var sort: String = ("an " if kind.left(1) in ["A", "E", "I", "O", "U"] else "a ") + kind
|
|
589
|
+
var has: String = ", ".join(Values.components_of(holder))
|
|
590
|
+
if not container_call.is_empty():
|
|
591
|
+
return (
|
|
592
|
+
"%s is %s, which a path reads by its components, %s, and makes no call on"
|
|
593
|
+
% [called_as, sort, has]
|
|
594
|
+
)
|
|
595
|
+
return "%s is %s, so it has no %s; it has %s" % [called_as, sort, named, has]
|
|
596
|
+
if not holder is Object:
|
|
597
|
+
return "%s is %s, which has nothing to read %s off" % [called_as, type_string(typeof(holder)), named]
|
|
524
598
|
var object: Object = holder
|
|
525
599
|
var method: String = method_of(named)
|
|
526
600
|
if not method.is_empty():
|
|
@@ -634,12 +634,12 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
634
634
|
if node_path.is_empty() or property.is_empty():
|
|
635
635
|
return {"type": "error", "message": "Node path and property required"}
|
|
636
636
|
|
|
637
|
-
var standing: Dictionary = Values.
|
|
637
|
+
var standing: Dictionary = Values.target_at(_host.get_tree().root, node_path)
|
|
638
638
|
if standing.has("message"):
|
|
639
639
|
return standing
|
|
640
|
-
var
|
|
640
|
+
var target: Object = standing["object"]
|
|
641
641
|
|
|
642
|
-
var reached: Dictionary = Paths.walk_to(
|
|
642
|
+
var reached: Dictionary = Paths.walk_to(target, node_path, property)
|
|
643
643
|
if reached.has("message"):
|
|
644
644
|
return reached
|
|
645
645
|
|