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.
Files changed (52) hide show
  1. package/build/cli.js +532 -456
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +3 -2
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +181 -4
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
  7. package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
  8. package/build/godot/addons/gdharness_editor/property_values.gd +192 -231
  9. package/build/godot/addons/gdharness_editor/reading.gd +15 -0
  10. package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
  11. package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/scene_file.gd +97 -14
  13. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
  14. package/build/godot/addons/gdharness_editor/tool_executor.gd +9 -3
  15. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +38 -35
  16. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +20 -12
  17. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
  18. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
  19. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
  20. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +68 -2
  21. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +94 -333
  22. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
  23. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_runtime/declared_values.gd +293 -0
  25. package/build/godot/addons/gdharness_runtime/declared_values.gd.uid +1 -0
  26. package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
  27. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +4 -0
  28. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +29 -1
  29. package/build/godot/addons/gdharness_runtime/runtime_input.gd +26 -39
  30. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +18 -0
  31. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
  32. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
  33. package/build/godot/addons/gdharness_runtime/runtime_values.gd +62 -5
  34. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +48 -15
  35. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
  36. package/build/godot/operations/animation_reads.gd +3 -2
  37. package/build/godot/operations/classdb_queries.gd +112 -46
  38. package/build/godot/operations/declared_values.gd +181 -4
  39. package/build/godot/operations/dependencies.gd +14 -39
  40. package/build/godot/operations/editor_tools.gd +4 -3
  41. package/build/godot/operations/file_reads.gd +15 -3
  42. package/build/godot/operations/gdscript_analysis.gd +229 -75
  43. package/build/godot/operations/gdscript_authoring.gd +157 -36
  44. package/build/godot/operations/import_pipeline.gd +17 -6
  45. package/build/godot/operations/logger.gd +7 -0
  46. package/build/godot/operations/property_paths.gd +225 -0
  47. package/build/godot/operations/property_values.gd +192 -231
  48. package/build/godot/operations/reading.gd +15 -0
  49. package/build/godot/operations/same_values.gd +177 -0
  50. package/build/godot/operations/serialisation.gd +75 -3
  51. package/build/keeper.js +83 -69
  52. 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 _held(frames)
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 key_label: String = str(params.get("key_label", ""))
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 key_label.is_empty():
216
- event.keycode = OS.find_keycode_from_string(key_label)
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": "Invalid key_label: " + key_label}
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 {"type": "error", "message": "keycode or key_label required"}
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 = _glyph_of(event.keycode, event.shift_pressed)
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 _held(frames)
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 _held(frames)
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
- return _pointer_noted(
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
- return _pointer_noted(
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
- return _pointer_noted(
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
- for _frame: int in frames:
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 element type of a typed list
231
- ## or the value type of a typed map. TYPE_NIL when nothing is declared, which is also what a
232
- ## property declared as Variant or left untyped reads as: either way it takes anything.
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 {"type": "error", "message": "%s has no signal %s" % [node_path, signal_name]}
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 {"type": "error", "message": "%s has no signal %s" % [node_path, signal_name]}
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 wanted: Variant = _values.fitted(params["value"], typeof(current))
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
- var refused: String = _not_comparable(current, wanted, node_path, property)
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 = _cut_to_fit(params["value"], typeof(current), node_path, property)
258
- var rounded: String = GdhValues.inexact(params["value"], typeof(current))
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
- return {"value": GdhPaths.read_under(holder, named), "declared": _declared_type(holder, named)}
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
- ## The type [param named] is declared with on [param holder], read the way a set reads it, so the two
493
- ## refuse the same values: a property's declaration, or a typed list's or map's element type. Read
494
- ## off properties alone, an element of an Array[Node] holding null declares nothing, and a wait for 5
495
- ## on it runs out the timeout while a set of 5 on it is refused at once. TYPE_NIL for anything
496
- ## declaring none, and for a call, whose answer nothing declares.
497
- static func _declared_type(holder: Variant, named: String) -> int:
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)["type"]
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
- if resource.resource_path.is_empty():
669
- return {"class": resource.get_class(), "_type": "Resource"}
670
- return {"path": resource.resource_path, "class": resource.get_class(), "_type": "Resource"}
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.