gdharness 1.3.3 → 1.3.5
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 +532 -456
- package/build/godot/addons/gdharness_editor/animation_reads.gd +3 -2
- package/build/godot/addons/gdharness_editor/declared_values.gd +181 -4
- package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
- package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
- package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
- package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +192 -231
- package/build/godot/addons/gdharness_editor/reading.gd +15 -0
- package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
- package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +97 -14
- package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
- package/build/godot/addons/gdharness_editor/tool_executor.gd +9 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +38 -35
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +20 -12
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +68 -2
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +94 -333
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/declared_values.gd +293 -0
- package/build/godot/addons/gdharness_runtime/declared_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +4 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +29 -1
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +26 -39
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +18 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +62 -5
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +48 -15
- package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
- package/build/godot/operations/animation_reads.gd +3 -2
- package/build/godot/operations/classdb_queries.gd +112 -46
- package/build/godot/operations/declared_values.gd +181 -4
- package/build/godot/operations/dependencies.gd +14 -39
- package/build/godot/operations/editor_tools.gd +4 -3
- package/build/godot/operations/file_reads.gd +15 -3
- package/build/godot/operations/gdscript_analysis.gd +229 -75
- package/build/godot/operations/gdscript_authoring.gd +157 -36
- package/build/godot/operations/import_pipeline.gd +17 -6
- package/build/godot/operations/logger.gd +7 -0
- package/build/godot/operations/property_paths.gd +225 -0
- package/build/godot/operations/property_values.gd +192 -231
- package/build/godot/operations/reading.gd +15 -0
- package/build/godot/operations/same_values.gd +177 -0
- package/build/godot/operations/serialisation.gd +75 -3
- package/build/keeper.js +83 -69
- package/package.json +1 -1
|
@@ -360,6 +360,10 @@ func _process(_delta: float) -> void:
|
|
|
360
360
|
break
|
|
361
361
|
_clients.append(client)
|
|
362
362
|
_send_welcome(client)
|
|
363
|
+
# Out before the request that came with it is read: a request can hold the game in a
|
|
364
|
+
# breakpoint before the frame's drain, and the server, never welcomed, took a game held
|
|
365
|
+
# inside its call for one that had not taken it. A client gone already is dropped below.
|
|
366
|
+
var _welcomed: bool = _drain(client)
|
|
363
367
|
|
|
364
368
|
var gone: Array[StreamPeerTCP] = []
|
|
365
369
|
for client: StreamPeerTCP in _clients:
|
|
@@ -5,6 +5,7 @@ extends RefCounted
|
|
|
5
5
|
## back what the engine did rather than trusting that it did it.
|
|
6
6
|
|
|
7
7
|
const GdhCallErrors = preload("call_errors.gd")
|
|
8
|
+
const GdhDeclared = preload("declared_values.gd")
|
|
8
9
|
const GdhPaths = preload("runtime_paths.gd")
|
|
9
10
|
const GdhRead = preload("reading.gd")
|
|
10
11
|
const GdhValues = preload("runtime_values.gd")
|
|
@@ -146,6 +147,15 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
146
147
|
}
|
|
147
148
|
if already and unmoved:
|
|
148
149
|
answer["unchanged"] = true
|
|
150
|
+
# The engine stores a number past an export range, or one no enum value has, as given; only the
|
|
151
|
+
# inspector holds a property to them, so the game now holds a value its own script never expected.
|
|
152
|
+
if holder is Object:
|
|
153
|
+
var owner: Object = holder
|
|
154
|
+
var shown: String = JSON.stringify(_values.serialize(now), "", false)
|
|
155
|
+
var declaration: Dictionary = slot["declared"]
|
|
156
|
+
var outside: String = GdhDeclared.outside(owner, declaration, now, shown)
|
|
157
|
+
if not outside.is_empty():
|
|
158
|
+
answer["outside_declared"] = outside
|
|
149
159
|
return answer
|
|
150
160
|
|
|
151
161
|
|
|
@@ -240,6 +250,15 @@ func _value_for(
|
|
|
240
250
|
var declared: int = slot["type"]
|
|
241
251
|
var declared_script: Script = slot["script"]
|
|
242
252
|
var wanted: int = declared if declared != TYPE_NIL else typeof(old_value)
|
|
253
|
+
var declaration: Dictionary = slot["declared"]
|
|
254
|
+
var enum_hint: bool = GdhRead.as_int(declaration.get("hint", PROPERTY_HINT_NONE)) == PROPERTY_HINT_ENUM
|
|
255
|
+
if declared == TYPE_INT and enum_hint and value is String and reached["holder"] is Object:
|
|
256
|
+
var owner: Object = reached["holder"]
|
|
257
|
+
var by_name: Array = GdhDeclared.enum_by_name(owner, declaration, str(value), called)
|
|
258
|
+
if by_name[0]:
|
|
259
|
+
return {"value": by_name[1]}
|
|
260
|
+
if not str(by_name[1]).is_empty():
|
|
261
|
+
return {"type": "error", "message": by_name[1]}
|
|
243
262
|
var holds_objects: bool = (
|
|
244
263
|
declared == TYPE_OBJECT or (declared == TYPE_NIL and (old_value == null or old_value is Object))
|
|
245
264
|
)
|
|
@@ -433,8 +452,15 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
433
452
|
OS.add_logger(caught)
|
|
434
453
|
var began: int = Time.get_ticks_usec()
|
|
435
454
|
var result: Variant = holder.callv(named, deserialized_args)
|
|
436
|
-
var elapsed: int = Time.get_ticks_usec() - began
|
|
437
455
|
OS.remove_logger(caught)
|
|
456
|
+
# A method that awaits hands back its state at the first await and its value only when it ends,
|
|
457
|
+
# so the answer waits for the value; one that takes longer than the server waits is collected by
|
|
458
|
+
# its requestId like any slow call.
|
|
459
|
+
var state: Object = result if result is Object else null
|
|
460
|
+
var awaited: bool = state != null and state.get_class() == "GDScriptFunctionState"
|
|
461
|
+
if awaited:
|
|
462
|
+
result = await Signal(state, &"completed")
|
|
463
|
+
var elapsed: int = Time.get_ticks_usec() - began
|
|
438
464
|
|
|
439
465
|
if not caught.refused.is_empty():
|
|
440
466
|
var message: String = (
|
|
@@ -463,6 +489,8 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
463
489
|
"result": _values.serialize(result),
|
|
464
490
|
"elapsed_usec": elapsed
|
|
465
491
|
}
|
|
492
|
+
if awaited:
|
|
493
|
+
answer["awaited"] = true
|
|
466
494
|
_keep_reported(answer, "errors", caught.raised)
|
|
467
495
|
_keep_reported(answer, "warnings", caught.warned)
|
|
468
496
|
var asked: Variant = params.get("properties")
|
|
@@ -17,9 +17,6 @@ const GdhScreen = preload("runtime_screen.gd")
|
|
|
17
17
|
const GdhTargets = preload("runtime_targets.gd")
|
|
18
18
|
const GdhMouse = preload("runtime_mouse.gd")
|
|
19
19
|
|
|
20
|
-
## The distance from a capital letter to its small one in Unicode. A keycode holds the capital.
|
|
21
|
-
const TO_SMALL: int = 32
|
|
22
|
-
|
|
23
20
|
## Set, to the desktop's name, for a process gdharness started on a desktop of its own and for
|
|
24
21
|
## everything that process starts. Kept in step with `DESKTOP_VARIABLE` in src/desktop.ts.
|
|
25
22
|
const DESKTOP_VARIABLE: String = "GDHARNESS_DESKTOP"
|
|
@@ -161,10 +158,11 @@ func inject_action(params: Dictionary) -> Dictionary:
|
|
|
161
158
|
var frames: int = holding
|
|
162
159
|
|
|
163
160
|
_say_action(action, whole or held, strength)
|
|
161
|
+
var lasted: Variant = null
|
|
164
162
|
if whole:
|
|
165
163
|
# A frame between the halves at least, as a click has: a listener that acts on the press and
|
|
166
164
|
# a listener that acts on the release both get their own frame to do it in.
|
|
167
|
-
await
|
|
165
|
+
lasted = await GdhValues.hold(_host.get_tree(), frames)
|
|
168
166
|
_say_action(action, false, strength)
|
|
169
167
|
await _host.get_tree().process_frame
|
|
170
168
|
|
|
@@ -175,15 +173,10 @@ func inject_action(params: Dictionary) -> Dictionary:
|
|
|
175
173
|
"pressed": held and not whole,
|
|
176
174
|
"whole": whole,
|
|
177
175
|
"held_frames": GdhValues.held_answer(whole, frames),
|
|
176
|
+
"held_for": lasted,
|
|
178
177
|
}
|
|
179
178
|
|
|
180
179
|
|
|
181
|
-
## Waits out [param frames] process frames, which is how long a whole press is held down.
|
|
182
|
-
func _held(frames: int) -> void:
|
|
183
|
-
for _frame: int in frames:
|
|
184
|
-
await _host.get_tree().process_frame
|
|
185
|
-
|
|
186
|
-
|
|
187
180
|
func _say_action(action: String, down: bool, strength: float) -> void:
|
|
188
181
|
var event: InputEventAction = InputEventAction.new()
|
|
189
182
|
event.action = action
|
|
@@ -197,12 +190,7 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
197
190
|
var keycode_raw: Variant = params.get("keycode", 0)
|
|
198
191
|
var held: bool = GdhRead.as_bool(params.get("pressed", true), true)
|
|
199
192
|
var whole: bool = not params.has("pressed")
|
|
200
|
-
var
|
|
201
|
-
|
|
202
|
-
if keycode_raw is String:
|
|
203
|
-
var named: String = keycode_raw
|
|
204
|
-
if not named.is_empty() and key_label.is_empty():
|
|
205
|
-
key_label = named
|
|
193
|
+
var named: String = keycode_raw if keycode_raw is String else ""
|
|
206
194
|
var keycode: int = 0 if keycode_raw is String else GdhRead.as_int(keycode_raw)
|
|
207
195
|
var holding: Variant = GdhValues.held_for(params)
|
|
208
196
|
if holding is String:
|
|
@@ -212,14 +200,17 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
212
200
|
var event: InputEventKey = InputEventKey.new()
|
|
213
201
|
event.pressed = whole or held
|
|
214
202
|
|
|
215
|
-
if not
|
|
216
|
-
event.keycode = OS.find_keycode_from_string(
|
|
203
|
+
if not named.is_empty():
|
|
204
|
+
event.keycode = OS.find_keycode_from_string(named)
|
|
217
205
|
if event.keycode == KEY_NONE:
|
|
218
|
-
return {"type": "error", "message":
|
|
206
|
+
return {"type": "error", "message": GdhValues.unknown_key(named)}
|
|
219
207
|
elif keycode > 0:
|
|
220
208
|
event.keycode = keycode as Key
|
|
221
209
|
else:
|
|
222
|
-
return {
|
|
210
|
+
return {
|
|
211
|
+
"type": "error",
|
|
212
|
+
"message": 'keycode is required: a key name, such as "Space" or "A", or its Godot keycode.'
|
|
213
|
+
}
|
|
223
214
|
|
|
224
215
|
# A key event from a real keyboard carries all three, and InputMap consults whichever one
|
|
225
216
|
# the bound event declares: keycode first, then physical_keycode, then key_label. An
|
|
@@ -237,11 +228,12 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
237
228
|
# TextEdit insert `unicode` and never consult the keycode, so an injected key could press any
|
|
238
229
|
# action in the map and still type nothing into a search box. Godot's keycodes for printable
|
|
239
230
|
# keys are the code points themselves, which is what makes this a mapping and not a table.
|
|
240
|
-
event.unicode =
|
|
231
|
+
event.unicode = GdhValues.glyph_of(event.keycode, event.shift_pressed)
|
|
241
232
|
|
|
242
233
|
Input.parse_input_event(event)
|
|
234
|
+
var lasted: Variant = null
|
|
243
235
|
if whole:
|
|
244
|
-
await
|
|
236
|
+
lasted = await GdhValues.hold(_host.get_tree(), frames)
|
|
245
237
|
var up: InputEventKey = event.duplicate()
|
|
246
238
|
up.pressed = false
|
|
247
239
|
Input.parse_input_event(up)
|
|
@@ -251,6 +243,7 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
251
243
|
"type": "input_injected",
|
|
252
244
|
"input_type": "key",
|
|
253
245
|
"held_frames": GdhValues.held_answer(whole, frames),
|
|
246
|
+
"held_for": lasted,
|
|
254
247
|
"keycode": event.keycode,
|
|
255
248
|
"physical_keycode": event.physical_keycode,
|
|
256
249
|
"shift": event.shift_pressed,
|
|
@@ -262,19 +255,6 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
262
255
|
}
|
|
263
256
|
|
|
264
257
|
|
|
265
|
-
## The character a key produces, or nothing for a key that produces none.
|
|
266
|
-
##
|
|
267
|
-
## Godot's keycodes below [constant KEY_SPECIAL] are the Unicode code points of the keys that
|
|
268
|
-
## print something, so the mapping is the value itself. Letters are held as their capitals, which
|
|
269
|
-
## is the one place the shift a caller asked for changes the answer rather than the key.
|
|
270
|
-
func _glyph_of(keycode: Key, shifted: bool) -> int:
|
|
271
|
-
if keycode >= KEY_SPECIAL:
|
|
272
|
-
return 0
|
|
273
|
-
if keycode >= KEY_A and keycode <= KEY_Z and not shifted:
|
|
274
|
-
return keycode + TO_SMALL
|
|
275
|
-
return keycode
|
|
276
|
-
|
|
277
|
-
|
|
278
258
|
## Clicks a mouse button at a position, and lets go of it again unless asked to hold it.
|
|
279
259
|
##
|
|
280
260
|
## The whole click by default, for the reason [method inject_action] is the whole press, and
|
|
@@ -314,21 +294,24 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
314
294
|
var _moved: Vector2 = _arrive(position)
|
|
315
295
|
var supplied: bool = _follow_in_the_window(position)
|
|
316
296
|
Input.parse_input_event(_button(position, button, whole or held, double))
|
|
297
|
+
var lasted: Variant = null
|
|
317
298
|
if notch:
|
|
318
299
|
Input.parse_input_event(_button(position, button, false, false))
|
|
319
300
|
elif whole:
|
|
320
301
|
# A frame between the halves at least, as inject_action has, so a control that acts on the
|
|
321
302
|
# press and one that acts on the release each get a frame to do it in.
|
|
322
|
-
await
|
|
303
|
+
lasted = await GdhValues.hold(_host.get_tree(), frames)
|
|
323
304
|
Input.parse_input_event(_button(position, button, false, false))
|
|
324
305
|
await _host.get_tree().process_frame
|
|
325
306
|
|
|
326
|
-
|
|
307
|
+
var at: Array[Vector2] = [position]
|
|
308
|
+
var said: Dictionary = _pointer_noted(
|
|
327
309
|
_hover_noted(
|
|
328
310
|
{
|
|
329
311
|
"type": "input_injected",
|
|
330
312
|
"input_type": "mouse_click",
|
|
331
313
|
"held_frames": GdhValues.held_answer(whole and not notch, frames),
|
|
314
|
+
"held_for": lasted,
|
|
332
315
|
"position": [position.x, position.y],
|
|
333
316
|
"button": button,
|
|
334
317
|
"pressed": held and not whole,
|
|
@@ -338,6 +321,7 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
338
321
|
supplied
|
|
339
322
|
)
|
|
340
323
|
)
|
|
324
|
+
return GdhMouse.outside_noted(said, _host.get_viewport(), at)
|
|
341
325
|
|
|
342
326
|
|
|
343
327
|
## Moves the pointer to a position, with the movement the event carries taken from where the
|
|
@@ -362,7 +346,8 @@ func inject_mouse_motion(params: Dictionary) -> Dictionary:
|
|
|
362
346
|
var supplied: bool = _follow_in_the_window(position)
|
|
363
347
|
Input.parse_input_event(_motion(position, relative))
|
|
364
348
|
|
|
365
|
-
|
|
349
|
+
var at: Array[Vector2] = [position]
|
|
350
|
+
var said: Dictionary = _pointer_noted(
|
|
366
351
|
_hover_noted(
|
|
367
352
|
{
|
|
368
353
|
"type": "input_injected",
|
|
@@ -373,6 +358,7 @@ func inject_mouse_motion(params: Dictionary) -> Dictionary:
|
|
|
373
358
|
supplied
|
|
374
359
|
)
|
|
375
360
|
)
|
|
361
|
+
return GdhMouse.outside_noted(said, _host.get_viewport(), at)
|
|
376
362
|
|
|
377
363
|
|
|
378
364
|
## Drags with a mouse button from one point to another, through any points between, as a hand does:
|
|
@@ -422,7 +408,7 @@ func inject_mouse_drag(params: Dictionary) -> Dictionary:
|
|
|
422
408
|
Input.parse_input_event(_button(end, button, false, false))
|
|
423
409
|
await _host.get_tree().process_frame
|
|
424
410
|
|
|
425
|
-
|
|
411
|
+
var said: Dictionary = _pointer_noted(
|
|
426
412
|
_hover_noted(
|
|
427
413
|
{
|
|
428
414
|
"type": "input_injected",
|
|
@@ -436,6 +422,7 @@ func inject_mouse_drag(params: Dictionary) -> Dictionary:
|
|
|
436
422
|
supplied
|
|
437
423
|
)
|
|
438
424
|
)
|
|
425
|
+
return GdhMouse.outside_noted(said, _host.get_viewport(), points)
|
|
439
426
|
|
|
440
427
|
|
|
441
428
|
## Whether [param centre] is somewhere a click can reach it: inside the viewport, and inside every
|
|
@@ -65,6 +65,24 @@ static func mask_of(button: int) -> int:
|
|
|
65
65
|
return 1 << (button - 1)
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
## [param answer], saying so when one of [param points] lies outside [param viewport]: the GUI picks
|
|
69
|
+
## what is under the pointer only inside it, so such a press or motion reaches no control, and its
|
|
70
|
+
## answer read the same as one that landed.
|
|
71
|
+
static func outside_noted(answer: Dictionary, viewport: Viewport, points: Array[Vector2]) -> Dictionary:
|
|
72
|
+
var shown: Rect2 = viewport.get_visible_rect()
|
|
73
|
+
for point: Vector2 in points:
|
|
74
|
+
if not shown.has_point(point):
|
|
75
|
+
answer["outside_viewport"] = (
|
|
76
|
+
(
|
|
77
|
+
"%s is outside the window's viewport, %s, so no control there receives it; the game's"
|
|
78
|
+
% [point, shown]
|
|
79
|
+
)
|
|
80
|
+
+ " _input and _unhandled_input still do."
|
|
81
|
+
)
|
|
82
|
+
break
|
|
83
|
+
return answer
|
|
84
|
+
|
|
85
|
+
|
|
68
86
|
## What to say about a name that is not a mouse button, with the ones that are.
|
|
69
87
|
static func no_such_button(named: Variant) -> Dictionary:
|
|
70
88
|
return {
|
|
@@ -69,11 +69,11 @@ func inject_joypad_button(params: Dictionary) -> Dictionary:
|
|
|
69
69
|
var frames: int = holding
|
|
70
70
|
|
|
71
71
|
Input.parse_input_event(_button(button, device, whole or held))
|
|
72
|
+
var lasted: Variant = null
|
|
72
73
|
if whole:
|
|
73
74
|
# A frame between the halves at least, as a key has, so a game that acts on the press and one
|
|
74
75
|
# that acts on the release each get a frame to do it in.
|
|
75
|
-
|
|
76
|
-
await _host.get_tree().process_frame
|
|
76
|
+
lasted = await GdhValues.hold(_host.get_tree(), frames)
|
|
77
77
|
Input.parse_input_event(_button(button, device, false))
|
|
78
78
|
await _host.get_tree().process_frame
|
|
79
79
|
|
|
@@ -81,6 +81,7 @@ func inject_joypad_button(params: Dictionary) -> Dictionary:
|
|
|
81
81
|
"type": "input_injected",
|
|
82
82
|
"input_type": "joypad_button",
|
|
83
83
|
"held_frames": GdhValues.held_answer(whole, frames),
|
|
84
|
+
"held_for": lasted,
|
|
84
85
|
"button": button,
|
|
85
86
|
"button_name": _name_of(button, JOY_BUTTONS),
|
|
86
87
|
"device": device,
|
|
@@ -4,6 +4,7 @@ extends RefCounted
|
|
|
4
4
|
## run off the game the node keeps. Every op that reads, writes, calls or waits on a game's state
|
|
5
5
|
## walks the same way, so the walk and what it says when a step is not there live in one place.
|
|
6
6
|
|
|
7
|
+
const GdhRead = preload("reading.gd")
|
|
7
8
|
const GdhSerialisation = preload("serialisation.gd")
|
|
8
9
|
const GdhValues = preload("runtime_values.gd")
|
|
9
10
|
|
|
@@ -640,7 +641,7 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
640
641
|
"%s has no property %s; it has a method of that name, which a path calls as %s()"
|
|
641
642
|
% [called_as, named, named]
|
|
642
643
|
)
|
|
643
|
-
return "%s has no property %s" % [called_as, named]
|
|
644
|
+
return GdhRead.near_property("%s has no property %s" % [called_as, named], object, named)
|
|
644
645
|
|
|
645
646
|
|
|
646
647
|
## Why [param method] is not a call a path makes on [param sort] ("a list", "a packed list" or "a
|
|
@@ -126,6 +126,58 @@ static func held_answer(whole: bool, frames: int) -> Variant:
|
|
|
126
126
|
return null
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
## Waits out [param frames] process frames of [param tree], how long a whole press is held down,
|
|
130
|
+
## and answers how long that was by the clock and in physics ticks. A process frame is no set
|
|
131
|
+
## time: a headless game draws nothing and runs them as fast as it can, so thirty held a movement
|
|
132
|
+
## read in _physics_process for twelve ticks, a fifth of a second, where a caller counting sixty
|
|
133
|
+
## frames a second expected half of one.
|
|
134
|
+
static func hold(tree: SceneTree, frames: int) -> Dictionary:
|
|
135
|
+
var began_usec: int = Time.get_ticks_usec()
|
|
136
|
+
var began_tick: int = Engine.get_physics_frames()
|
|
137
|
+
for _frame: int in frames:
|
|
138
|
+
await tree.process_frame
|
|
139
|
+
return {
|
|
140
|
+
"seconds": snappedf((Time.get_ticks_usec() - began_usec) / 1000000.0, 0.001),
|
|
141
|
+
"physics_frames": Engine.get_physics_frames() - began_tick,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
## The character a key produces, or nothing for a key that produces none.
|
|
146
|
+
##
|
|
147
|
+
## Godot's keycodes below [constant KEY_SPECIAL] are the Unicode code points of the keys that
|
|
148
|
+
## print something, so the mapping is the value itself. Letters are held as their capitals, which
|
|
149
|
+
## is the one place the shift a caller asked for changes the answer rather than the key: the small
|
|
150
|
+
## letter is 32 above its capital.
|
|
151
|
+
static func glyph_of(keycode: Key, shifted: bool) -> int:
|
|
152
|
+
if keycode >= KEY_SPECIAL:
|
|
153
|
+
return 0
|
|
154
|
+
if keycode >= KEY_A and keycode <= KEY_Z and not shifted:
|
|
155
|
+
return keycode + 32
|
|
156
|
+
return keycode
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## The refusal for [param name], which no key is called, with the key names nearest it. Read from the
|
|
160
|
+
## engine's own names, printable keys and the special ones above [constant KEY_SPECIAL], since the
|
|
161
|
+
## game takes any name OS.find_keycode_from_string does.
|
|
162
|
+
static func unknown_key(name: String) -> String:
|
|
163
|
+
var names: PackedStringArray = []
|
|
164
|
+
for code: int in range(32, 127) + range(KEY_SPECIAL + 1, KEY_SPECIAL + 0x100):
|
|
165
|
+
var spelled: String = OS.get_keycode_string(code as Key)
|
|
166
|
+
if not spelled.is_empty() and OS.find_keycode_from_string(spelled) == code:
|
|
167
|
+
var _named: bool = names.append(spelled)
|
|
168
|
+
var close: PackedStringArray = GdhRead.nearest(name, names)
|
|
169
|
+
var said: String = "No key is called %s." % name
|
|
170
|
+
if close.size() == 1:
|
|
171
|
+
said += " The nearest key name is %s." % close[0]
|
|
172
|
+
elif not close.is_empty():
|
|
173
|
+
said += " The nearest key names are %s." % ", ".join(close)
|
|
174
|
+
return (
|
|
175
|
+
said
|
|
176
|
+
+ ' A key is named as Godot names it, such as "Space", "Enter", "Escape", "A" or "F1",'
|
|
177
|
+
+ " or given by its keycode."
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
129
181
|
## What [param path] names: a node under [param root], as [method node_at] reads it, or an engine
|
|
130
182
|
## singleton by its name. Answers the object under "object", and under "node" the node that objects
|
|
131
183
|
## named in a value are found from, which for a singleton is [param root].
|
|
@@ -227,9 +279,11 @@ func parameter_class(object: Object, method: String, index: int) -> String:
|
|
|
227
279
|
|
|
228
280
|
|
|
229
281
|
## What the slot [param named] on [param holder] declares it holds, as `{"type": int, "class":
|
|
230
|
-
## String, "script": Script or null}`: a property's declaration, or the
|
|
231
|
-
## or the value type of a typed map. TYPE_NIL when nothing is declared,
|
|
232
|
-
## property declared as Variant or left untyped reads as: either way it takes
|
|
282
|
+
## String, "script": Script or null, "declared": Dictionary}`: a property's declaration, or the
|
|
283
|
+
## element type of a typed list or the value type of a typed map. TYPE_NIL when nothing is declared,
|
|
284
|
+
## which is also what a property declared as Variant or left untyped reads as: either way it takes
|
|
285
|
+
## anything. declared is the property's entry in the property list, which carries its enum or
|
|
286
|
+
## range, and empty for anything that is not a property.
|
|
233
287
|
##
|
|
234
288
|
## Asked rather than read off the value in the slot, because an object slot can be empty: a
|
|
235
289
|
## property typed `Gear` holding null reads as TYPE_NIL, and anything written into it passed the
|
|
@@ -243,7 +297,7 @@ static func slot_declared(holder: Variant, named: String) -> Dictionary:
|
|
|
243
297
|
# form's an integer, a rectangle's size a vector of the same form.
|
|
244
298
|
if named in GdhSerialisation.components_of(holder):
|
|
245
299
|
var component: Variant = holder[named]
|
|
246
|
-
return {"type": typeof(component), "class": "", "script": null}
|
|
300
|
+
return {"type": typeof(component), "class": "", "script": null, "declared": {}}
|
|
247
301
|
if holder is Array:
|
|
248
302
|
var items: Array = holder
|
|
249
303
|
if items.is_typed():
|
|
@@ -251,6 +305,7 @@ static func slot_declared(holder: Variant, named: String) -> Dictionary:
|
|
|
251
305
|
"type": items.get_typed_builtin(),
|
|
252
306
|
"class": _typed_class(items.get_typed_script(), items.get_typed_class_name()),
|
|
253
307
|
"script": _script_or_null(items.get_typed_script()),
|
|
308
|
+
"declared": {},
|
|
254
309
|
}
|
|
255
310
|
elif holder is Dictionary:
|
|
256
311
|
var map: Dictionary = holder
|
|
@@ -259,6 +314,7 @@ static func slot_declared(holder: Variant, named: String) -> Dictionary:
|
|
|
259
314
|
"type": map.get_typed_value_builtin(),
|
|
260
315
|
"class": _typed_class(map.get_typed_value_script(), map.get_typed_value_class_name()),
|
|
261
316
|
"script": _script_or_null(map.get_typed_value_script()),
|
|
317
|
+
"declared": {},
|
|
262
318
|
}
|
|
263
319
|
elif holder is Object:
|
|
264
320
|
var object: Object = holder
|
|
@@ -268,8 +324,9 @@ static func slot_declared(holder: Variant, named: String) -> Dictionary:
|
|
|
268
324
|
"type": GdhRead.as_int(entry.get("type", TYPE_NIL), TYPE_NIL),
|
|
269
325
|
"class": str(entry.get("class_name", "")),
|
|
270
326
|
"script": null,
|
|
327
|
+
"declared": entry,
|
|
271
328
|
}
|
|
272
|
-
return {"type": TYPE_NIL, "class": "", "script": null}
|
|
329
|
+
return {"type": TYPE_NIL, "class": "", "script": null, "declared": {}}
|
|
273
330
|
|
|
274
331
|
|
|
275
332
|
static func _script_or_null(typed: Variant) -> Script:
|
|
@@ -4,6 +4,7 @@ extends RefCounted
|
|
|
4
4
|
## happened, or when the time ran out, so the caller never sleeps for a guessed length.
|
|
5
5
|
|
|
6
6
|
const GdhChanges = preload("runtime_changes.gd")
|
|
7
|
+
const GdhDeclared = preload("declared_values.gd")
|
|
7
8
|
const GdhPaths = preload("runtime_paths.gd")
|
|
8
9
|
const GdhQueries = preload("runtime_queries.gd")
|
|
9
10
|
const GdhRead = preload("reading.gd")
|
|
@@ -51,6 +52,13 @@ static func _out_of_range(named: String, asked: int, least: int, most: int) -> D
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
## The refusal for [param signal_name], which [param node] lacks, naming the nearest signals it has,
|
|
56
|
+
## or every one when none is near: a name to send instead.
|
|
57
|
+
static func _no_signal(node: Node, node_path: String, signal_name: String) -> Dictionary:
|
|
58
|
+
var said: String = "%s has no signal %s" % [node_path, signal_name]
|
|
59
|
+
return {"type": "error", "message": GdhRead.offered(said, signal_name, GdhRead.signals_of(node)) + "."}
|
|
60
|
+
|
|
61
|
+
|
|
54
62
|
func wait_frames(params: Dictionary) -> Dictionary:
|
|
55
63
|
var frames: int = GdhRead.as_int(params.get("frames", 1), 1)
|
|
56
64
|
if frames < 1 or frames > MOST_FRAMES:
|
|
@@ -79,7 +87,7 @@ func wait_signal(params: Dictionary) -> Dictionary:
|
|
|
79
87
|
return standing
|
|
80
88
|
var node: Node = standing["node"]
|
|
81
89
|
if not node.has_signal(signal_name):
|
|
82
|
-
return
|
|
90
|
+
return _no_signal(node, node_path, signal_name)
|
|
83
91
|
|
|
84
92
|
var catcher: GdhSignalCatcher = GdhSignalCatcher.new()
|
|
85
93
|
var callable: Callable = catcher._on_fired
|
|
@@ -124,7 +132,7 @@ func watch_signal(params: Dictionary) -> Dictionary:
|
|
|
124
132
|
return standing
|
|
125
133
|
var node: Node = standing["node"]
|
|
126
134
|
if not node.has_signal(signal_name):
|
|
127
|
-
return
|
|
135
|
+
return _no_signal(node, node_path, signal_name)
|
|
128
136
|
var watch: GdhWatch = GdhWatch.new(node, node_path, signal_name)
|
|
129
137
|
var listening: Error = node.connect(signal_name, watch.catcher._on_fired)
|
|
130
138
|
if listening != OK:
|
|
@@ -240,7 +248,9 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
240
248
|
var current: Variant = watched["value"]
|
|
241
249
|
if compare in [">=", "<=", ">", "<"]:
|
|
242
250
|
return await _wait_in_order(node, params, current, timeout_ms)
|
|
243
|
-
var
|
|
251
|
+
var by_name: Array = _enum_by_name(watched, params["value"], node_path)
|
|
252
|
+
var sent: Variant = by_name[1] if not by_name.is_empty() and by_name[0] else params["value"]
|
|
253
|
+
var wanted: Variant = _values.fitted(sent, typeof(current))
|
|
244
254
|
# Read against what the property is declared as, not only against what it holds this frame: an
|
|
245
255
|
# object property holding null holds nothing typed, so any value was taken as it came and compared
|
|
246
256
|
# with null until the time ran out. #772 waited a minute for "null" that way and answered met
|
|
@@ -252,10 +262,13 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
252
262
|
var holds_objects: bool = declared == TYPE_OBJECT or untyped_object
|
|
253
263
|
if holds_objects and GdhChanges.names_no_object(wanted):
|
|
254
264
|
wanted = null
|
|
255
|
-
|
|
265
|
+
# A word no value of an enum is called, refused naming the values before it is compared as text.
|
|
266
|
+
var refused: String = "" if by_name.is_empty() or by_name[0] else str(by_name[1])
|
|
256
267
|
if refused.is_empty():
|
|
257
|
-
refused =
|
|
258
|
-
|
|
268
|
+
refused = _not_comparable(current, wanted, node_path, property)
|
|
269
|
+
if refused.is_empty():
|
|
270
|
+
refused = _cut_to_fit(sent, typeof(current), node_path, property)
|
|
271
|
+
var rounded: String = GdhValues.inexact(sent, typeof(current))
|
|
259
272
|
if refused.is_empty() and not rounded.is_empty():
|
|
260
273
|
refused = "%s.%s holds int: %s." % [node_path, property, rounded]
|
|
261
274
|
var never_held: bool = typeof(wanted) != TYPE_NIL and typeof(wanted) != TYPE_OBJECT
|
|
@@ -469,6 +482,20 @@ static func _not_comparable(current: Variant, wanted: Variant, node_path: String
|
|
|
469
482
|
return ""
|
|
470
483
|
|
|
471
484
|
|
|
485
|
+
## [param sent] as the number of the value an enum property names by it, as
|
|
486
|
+
## [method GdhDeclared.enum_by_name] answers, for a word waited for on what [param watched] read; []
|
|
487
|
+
## when the property is no enum or the value no word.
|
|
488
|
+
static func _enum_by_name(watched: Dictionary, sent: Variant, node_path: String) -> Array:
|
|
489
|
+
var declaration: Dictionary = watched["declaration"]
|
|
490
|
+
var enum_hint: bool = GdhRead.as_int(declaration.get("hint", PROPERTY_HINT_NONE)) == PROPERTY_HINT_ENUM
|
|
491
|
+
if watched["declared"] != TYPE_INT or not enum_hint or not sent is String:
|
|
492
|
+
return []
|
|
493
|
+
if not watched["holder"] is Object:
|
|
494
|
+
return []
|
|
495
|
+
var owner: Object = watched["holder"]
|
|
496
|
+
return GdhDeclared.enum_by_name(owner, declaration, str(sent), node_path)
|
|
497
|
+
|
|
498
|
+
|
|
472
499
|
## What [param property] reads as on [param node] right now, under `value`, or a `message` saying
|
|
473
500
|
## why it cannot be read.
|
|
474
501
|
##
|
|
@@ -486,18 +513,24 @@ static func _watched(node: Node, node_path: String, property: String) -> Diction
|
|
|
486
513
|
var missing: String = GdhPaths.nothing_under(holder, named, str(reached["called"]))
|
|
487
514
|
if not missing.is_empty():
|
|
488
515
|
return {"type": "error", "message": missing}
|
|
489
|
-
|
|
516
|
+
var slot: Dictionary = _declared_slot(holder, named)
|
|
517
|
+
return {
|
|
518
|
+
"value": GdhPaths.read_under(holder, named),
|
|
519
|
+
"declared": slot["type"],
|
|
520
|
+
"declaration": slot["declared"],
|
|
521
|
+
"holder": holder,
|
|
522
|
+
}
|
|
490
523
|
|
|
491
524
|
|
|
492
|
-
##
|
|
493
|
-
## refuse the same values: a property's declaration, or a
|
|
494
|
-
## off properties alone, an element of an Array[Node] holding
|
|
495
|
-
## on it runs out the timeout while a set of 5 on it is refused
|
|
496
|
-
## declaring none, and for a call, whose answer nothing declares.
|
|
497
|
-
static func
|
|
525
|
+
## What [param named] is declared with on [param holder], as [method GdhValues.slot_declared] answers,
|
|
526
|
+
## read the way a set reads it, so the two refuse the same values: a property's declaration, or a
|
|
527
|
+
## typed list's or map's element type. Read off properties alone, an element of an Array[Node] holding
|
|
528
|
+
## null declares nothing, and a wait for 5 on it runs out the timeout while a set of 5 on it is refused
|
|
529
|
+
## at once. TYPE_NIL for anything declaring none, and for a call, whose answer nothing declares.
|
|
530
|
+
static func _declared_slot(holder: Variant, named: String) -> Dictionary:
|
|
498
531
|
if not GdhPaths.method_of(named).is_empty():
|
|
499
|
-
return TYPE_NIL
|
|
500
|
-
return GdhValues.slot_declared(holder, named)
|
|
532
|
+
return {"type": TYPE_NIL, "class": "", "script": null, "declared": {}}
|
|
533
|
+
return GdhValues.slot_declared(holder, named)
|
|
501
534
|
|
|
502
535
|
|
|
503
536
|
## Waits until something under [param node_path] has [param said] written on it.
|
|
@@ -121,6 +121,10 @@ const BUILDERS: Dictionary = {
|
|
|
121
121
|
"Dictionary": "_build_dictionary",
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
## The built-in resources being described, outermost first, so one holding itself somewhere inside
|
|
125
|
+
## is answered by its class rather than without end.
|
|
126
|
+
var _describing: Array[Resource] = []
|
|
127
|
+
|
|
124
128
|
|
|
125
129
|
# Converts a Godot value into something JSON can carry. A type with no entry in the table
|
|
126
130
|
# passes through as itself, which is what the JSON-native ones want.
|
|
@@ -665,9 +669,21 @@ func _serialize_object(value: Object) -> Variant:
|
|
|
665
669
|
return _describe_input_event(event)
|
|
666
670
|
if value is Resource:
|
|
667
671
|
var resource: Resource = value
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
672
|
+
# A file's built-in resources load with a path of the file's own and "::", which no other
|
|
673
|
+
# file can refer to.
|
|
674
|
+
if resource.resource_path.is_empty() or resource.resource_path.contains("::"):
|
|
675
|
+
return _describe_built_in(resource)
|
|
676
|
+
var filed: Dictionary = {
|
|
677
|
+
"path": resource.resource_path, "class": resource.get_class(), "_type": "Resource"
|
|
678
|
+
}
|
|
679
|
+
# The engine class of a resource with a script is the one its script extends, so a Stats
|
|
680
|
+
# file read as a Resource and nothing said it was a Stats.
|
|
681
|
+
var attached: Variant = resource.get_script()
|
|
682
|
+
if attached is Script:
|
|
683
|
+
var script: Script = attached
|
|
684
|
+
if not script.get_global_name().is_empty():
|
|
685
|
+
filed["scriptClass"] = str(script.get_global_name())
|
|
686
|
+
return filed
|
|
671
687
|
if value is Node:
|
|
672
688
|
var node: Node = value
|
|
673
689
|
if node.is_inside_tree():
|
|
@@ -675,6 +691,62 @@ func _serialize_object(value: Object) -> Variant:
|
|
|
675
691
|
return _describe_object(value)
|
|
676
692
|
|
|
677
693
|
|
|
694
|
+
## A resource built into the file it was read from, in the form a write builds one from: its class
|
|
695
|
+
## under _type beside every property it stores away from its default. As its class alone it said
|
|
696
|
+
## nothing of what it held, so a CollisionShape2D's radius could not be read, and it could not be
|
|
697
|
+
## written back.
|
|
698
|
+
func _describe_built_in(resource: Resource) -> Dictionary:
|
|
699
|
+
if _describing.has(resource):
|
|
700
|
+
return {"class": resource.get_class(), "_type": "Resource"}
|
|
701
|
+
_describing.append(resource)
|
|
702
|
+
var described: Dictionary = {"_type": resource.get_class()}
|
|
703
|
+
var attached: Variant = resource.get_script()
|
|
704
|
+
var script: Script = null
|
|
705
|
+
if attached is Script:
|
|
706
|
+
script = attached
|
|
707
|
+
described["script"] = serialize_value(script)
|
|
708
|
+
var fresh: Object = _fresh(script)
|
|
709
|
+
for property: Dictionary in resource.get_property_list():
|
|
710
|
+
var usage: int = property.get("usage", 0)
|
|
711
|
+
var name: String = str(property.get("name", ""))
|
|
712
|
+
if (
|
|
713
|
+
not (usage & PROPERTY_USAGE_STORAGE)
|
|
714
|
+
or name == "script"
|
|
715
|
+
or (name.begins_with("resource_") and name != "resource_name")
|
|
716
|
+
):
|
|
717
|
+
continue
|
|
718
|
+
var held: Variant = resource.get(name)
|
|
719
|
+
var default: Variant = ClassDB.class_get_property_default_value(resource.get_class(), name)
|
|
720
|
+
if fresh != null:
|
|
721
|
+
default = fresh.get(name)
|
|
722
|
+
elif script != null and usage & PROPERTY_USAGE_SCRIPT_VARIABLE:
|
|
723
|
+
default = script.get_property_default_value(name)
|
|
724
|
+
if typeof(held) == typeof(default) and held == default:
|
|
725
|
+
continue
|
|
726
|
+
described[name] = serialize_value(held)
|
|
727
|
+
_describing.pop_back()
|
|
728
|
+
return described
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
## A new instance of [param script] to read defaults off, or null where none should be made. What
|
|
732
|
+
## the script's own code sets as it is made (a resource_name in _init, a list built by a
|
|
733
|
+
## constructor, which the engine reports no default for) is then a default rather than a value
|
|
734
|
+
## the file holds. None is made for a script whose _init needs arguments, nor in the editor for one
|
|
735
|
+
## that is not a tool: the editor never runs those, and can_instantiate says so.
|
|
736
|
+
static func _fresh(script: Script) -> Object:
|
|
737
|
+
if script == null or not script.can_instantiate():
|
|
738
|
+
return null
|
|
739
|
+
for method: Dictionary in script.get_script_method_list():
|
|
740
|
+
if str(method.get("name", "")) == "_init":
|
|
741
|
+
var arguments: Array = method.get("args", [])
|
|
742
|
+
var defaults: Array = method.get("default_args", [])
|
|
743
|
+
if arguments.size() > defaults.size():
|
|
744
|
+
return null
|
|
745
|
+
# Called by name: new() is declared on each kind of script rather than on Script.
|
|
746
|
+
var made: Variant = script.call("new")
|
|
747
|
+
return made if made is Object else null
|
|
748
|
+
|
|
749
|
+
|
|
678
750
|
## An input event, by what it is bound to: the engine's own words for it and every field it stores
|
|
679
751
|
## away from its default. As a bare resource it was its class and nothing else, so an input action
|
|
680
752
|
## read back from project.godot could not say whether it was bound to Space or to a joypad button.
|