gdharness 1.1.21 → 1.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/build/cli.js +1036 -525
  2. package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
  3. package/build/godot/addons/gdharness_editor/reading.gd +17 -0
  4. package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
  5. package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
  6. package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
  7. package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
  8. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
  9. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
  10. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
  11. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
  13. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
  14. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
  15. package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
  16. package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
  17. package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +8 -4
  19. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
  20. package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
  22. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +28 -8
  23. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +7 -299
  24. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
  25. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
  26. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
  27. package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
  28. package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
  29. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +20 -9
  30. package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
  31. package/build/godot/operations/audio_buses.gd +47 -11
  32. package/build/godot/operations/class_cache.gd +18 -2
  33. package/build/godot/operations/dependencies.gd +128 -17
  34. package/build/godot/operations/file_walk.gd +20 -0
  35. package/build/godot/operations/gdscript_analysis.gd +59 -20
  36. package/build/godot/operations/gdscript_authoring.gd +127 -60
  37. package/build/godot/operations/godot_operations.gd +24 -5
  38. package/build/godot/operations/import_pipeline.gd +179 -19
  39. package/build/godot/operations/logger.gd +2 -2
  40. package/build/godot/operations/plugins.gd +88 -70
  41. package/build/godot/operations/project_config.gd +41 -6
  42. package/build/godot/operations/project_diagnostics.gd +25 -10
  43. package/build/godot/operations/reading.gd +17 -0
  44. package/build/godot/operations/resource_files.gd +60 -17
  45. package/build/godot/operations/serialisation.gd +116 -7
  46. package/build/index.js +949 -435
  47. package/package.json +1 -1
@@ -0,0 +1,351 @@
1
+ extends RefCounted
2
+
3
+ ## What reaches the running game the way a keyboard reaches it: text typed into the field that has
4
+ ## the focus, and an item chosen out of a menu the way a keyboard chooses one.
5
+
6
+ const Menus = preload("runtime_menus.gd")
7
+ const Read = preload("reading.gd")
8
+ const Values = preload("runtime_values.gd")
9
+ const Words = preload("runtime_words.gd")
10
+
11
+ ## The two characters a field reads as keys rather than as text.
12
+ const NEWLINE: int = 10
13
+ const TAB: int = 9
14
+
15
+ var _host: Node
16
+
17
+
18
+ func _init(host: Node) -> void:
19
+ _host = host
20
+
21
+
22
+ ## Types [param text] wherever the focus is, one key event per character.
23
+ ##
24
+ ## A key on its own cannot do this and should not try: which character a key produces is the
25
+ ## keyboard layout's business, and shift over a digit is an exclamation mark on one layout and
26
+ ## something else on the next. Given the character instead there is nothing to guess, so a field
27
+ ## can be filled with anything a player could type, this project's own two typefaces included.
28
+ ##
29
+ ## A newline and a tab are the two characters a field reads as keys rather than as text, so they
30
+ ## are sent as those keys and carry no character of their own: typing a name and submitting it is
31
+ ## one call rather than two.
32
+ ##
33
+ ## Pushed into the viewport for the reason a click is, and it matters more here: the focus is what
34
+ ## decides where a character lands, so a caller that clicked a field and then typed would otherwise
35
+ ## have both waiting in the same queue with nothing said about the order.
36
+ ##
37
+ ## [code]replace[/code] is for a field that already says something, which is most of them: typing
38
+ ## lands at the caret, so a spin box reading 2.1 typed "0.3" at reads 2.10.3 and parses back to
39
+ ## 2.1, and the answer said four characters had gone in. What the field holds afterwards is read
40
+ ## back now rather than echoed from the request, so a call that typed somewhere unhelpful says so.
41
+ func inject_text(params: Dictionary) -> Dictionary:
42
+ var text: String = str(params.get("text", ""))
43
+ var over: bool = Read.as_bool(params.get("replace", false))
44
+ if text.is_empty() and not over:
45
+ return {"type": "error", "message": "text needs something to type, or replace to empty a field"}
46
+
47
+ var viewport: Viewport = _host.get_tree().root
48
+ var focused: Control = focus_owner(viewport)
49
+ var shut: String = _shut_to_typing(focused)
50
+ if not shut.is_empty():
51
+ return {"type": "error", "message": shut}
52
+
53
+ # Where it goes, which is the one thing a caller cannot see from here, read before the keys: a
54
+ # name field that swaps its panel out on Enter has left the tree by the time the keys are done,
55
+ # and a node out of the tree has no path to give. Null is a game reading keys for itself with
56
+ # nothing focused, which is a real thing to be typing at.
57
+ var into: Variant = null
58
+ if focused != null:
59
+ into = str(focused.get_path())
60
+
61
+ var replaced: bool = over and _select_everything(focused)
62
+ # Emptying a field is a real thing to ask for and the one shape of filling one in that types no
63
+ # characters. The selection is standing, so the key a player presses over one is what clears it.
64
+ if replaced and text.is_empty():
65
+ _press(viewport, _held_down(KEY_DELETE))
66
+ for index: int in text.length():
67
+ _press(viewport, _typed(text.unicode_at(index)))
68
+
69
+ var holds: Variant = null
70
+ if is_instance_valid(focused):
71
+ holds = _what_it_holds(focused)
72
+ return {
73
+ "type": "input_injected",
74
+ "input_type": "text",
75
+ "text": text,
76
+ "characters": text.length(),
77
+ "into": into,
78
+ "replaced": replaced,
79
+ "holds": holds,
80
+ }
81
+
82
+
83
+ ## The control keys pushed into [param viewport] go to: its focus, or the focus of the window
84
+ ## embedded in it that has the focus, followed down.
85
+ ##
86
+ ## The game's own viewport keeps the control it had focused when a dialog took the focus, and keys
87
+ ## pushed at it go on to the dialog, measured on 4.7.2: a root field was reported as typed into and
88
+ ## stayed empty while the characters landed in the dialog's field.
89
+ static func focus_owner(viewport: Viewport) -> Control:
90
+ var at: Viewport = viewport
91
+ var deeper: bool = true
92
+ while deeper:
93
+ deeper = false
94
+ for window: Window in at.get_embedded_subwindows():
95
+ if window.has_focus():
96
+ at = window
97
+ deeper = true
98
+ break
99
+ return at.gui_get_focus_owner()
100
+
101
+
102
+ ## Selects everything in [param focused] so the next character typed writes over it, and answers
103
+ ## whether there was a field to select in.
104
+ ##
105
+ ## Through the control rather than through a Ctrl+A, which is the one part of filling a field that
106
+ ## cannot honestly be sent as a key: the shortcut is Cmd+A on macOS and is an [InputMap] action a
107
+ ## project is free to unbind, and a caller asking for the field to be replaced would then be typing
108
+ ## on the end of what was there. The replacement itself is still every character a player types.
109
+ static func _select_everything(focused: Control) -> bool:
110
+ var field: LineEdit = focused as LineEdit
111
+ if field != null:
112
+ field.select_all()
113
+ return true
114
+ var box: TextEdit = focused as TextEdit
115
+ if box == null:
116
+ return false
117
+ box.select_all()
118
+ return true
119
+
120
+
121
+ ## What the field says now, or null where the focus is not a field at all.
122
+ ##
123
+ ## A spin box rewrites its own text out of the number it parsed, so this is the only thing that
124
+ ## says whether what was typed became what the field means. A secret field answers with its mask,
125
+ ## which still says how many characters it took.
126
+ static func _what_it_holds(focused: Control) -> Variant:
127
+ var field: LineEdit = focused as LineEdit
128
+ if field != null:
129
+ return Words.masked(field) if field.secret else field.text
130
+ var box: TextEdit = focused as TextEdit
131
+ if box == null:
132
+ return null
133
+ return box.text
134
+
135
+
136
+ ## Why nothing typed would reach [param focused], or "" when it would.
137
+ ##
138
+ ## A field that is not editable takes no characters however it is reached, and a click does not
139
+ ## open a read-only [LineEdit] for editing, measured on 4.7.2, so sending the caller to click it
140
+ ## would not help.
141
+ ##
142
+ ## Having the focus is not the same as being edited. Since Godot 4.4 a field is focused and shut
143
+ ## until something opens it, which is what a click does and what submitting undoes: press Enter in
144
+ ## a box and it keeps the focus and drops every key that arrives afterwards. Typing into one
145
+ ## answered that the characters had gone in and put nothing anywhere, which is the shape a refusal
146
+ ## exists to prevent. Measured on a spin box in a real game: `has_focus` true, `is_editing` false,
147
+ ## three characters reported and the field unchanged. Only a [LineEdit] is refused for that: it is
148
+ ## the one control the engine will say it about, since [TextEdit] has no editing state of its own.
149
+ ## A game reading keys for itself is typed at with nothing focused at all, and that is not this
150
+ ## tool's business to judge.
151
+ static func _shut_to_typing(focused: Control) -> String:
152
+ var field: LineEdit = focused as LineEdit
153
+ var box: TextEdit = focused as TextEdit
154
+ if (field != null and not field.editable) or (box != null and not box.editable):
155
+ return "%s has the focus and is read-only, so nothing typed lands in it." % focused.get_path()
156
+ if field == null or field.is_editing():
157
+ return ""
158
+ return (
159
+ "%s has the focus and is not being edited, so nothing typed lands in it. Click it first."
160
+ % focused.get_path()
161
+ )
162
+
163
+
164
+ ## A whole key press into [param viewport]: down, then the release, so nothing is left held down
165
+ ## behind the caller.
166
+ static func _press(viewport: Viewport, down: InputEventKey) -> void:
167
+ viewport.push_input(down)
168
+ var up: InputEventKey = down.duplicate()
169
+ up.pressed = false
170
+ viewport.push_input(up)
171
+
172
+
173
+ ## A key held down by its keycode, for the ones that stand for an edit rather than a character.
174
+ static func _held_down(code: Key) -> InputEventKey:
175
+ var event: InputEventKey = InputEventKey.new()
176
+ event.pressed = true
177
+ event.keycode = code
178
+ event.physical_keycode = code
179
+ event.key_label = code
180
+ return event
181
+
182
+
183
+ ## The key press that produces [param glyph], as a keyboard would send it.
184
+ static func _typed(glyph: int) -> InputEventKey:
185
+ var event: InputEventKey = InputEventKey.new()
186
+ event.pressed = true
187
+ if glyph == NEWLINE:
188
+ event.keycode = KEY_ENTER
189
+ elif glyph == TAB:
190
+ event.keycode = KEY_TAB
191
+ else:
192
+ var capital: int = String.chr(glyph).to_upper().unicode_at(0)
193
+ event.keycode = capital as Key
194
+ event.shift_pressed = capital != glyph
195
+ event.unicode = glyph
196
+ event.physical_keycode = event.keycode
197
+ event.key_label = event.keycode
198
+ return event
199
+
200
+
201
+ ## Chooses an item out of a menu, by what it says or by where it is in the list.
202
+ ##
203
+ ## A menu's items are drawn rather than built, so there is no node under the pointer to aim at and
204
+ ## no rectangle to ask for: [PopupMenu] exposes their text, their ids and which one has the focus,
205
+ ## and nothing about where any of them is. So a click cannot reach one, and a whole click on the
206
+ ## [OptionButton] in front of it opens the menu on the press and closes it again on the release.
207
+ ## Every language picker, every filter and every dropdown in a game was unreachable, and the way
208
+ ## past it was to call `select` and emit `item_selected`, which sets a number and runs none of the
209
+ ## engine's own path.
210
+ ##
211
+ ## Chosen the way a keyboard chooses: the item takes the focus and then `ui_accept` presses it,
212
+ ## which is the same route through [PopupMenu] a pointer takes and which needs no geometry, so it
213
+ ## works in a game with no window as well.
214
+ ##
215
+ ## [param path] may be the menu or the button in front of it. Naming the button is what a caller
216
+ ## has, since the menu is an internal child with a generated name that changes between runs.
217
+ func choose(params: Dictionary) -> Dictionary:
218
+ var node_path: String = str(params.get("path", ""))
219
+ if node_path.is_empty():
220
+ return {"type": "error", "message": "Node path required"}
221
+ var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
222
+ if standing.has("message"):
223
+ return standing
224
+ var node: Node = standing["node"]
225
+
226
+ var menu: PopupMenu = Menus.menu_of(node)
227
+ if menu == null:
228
+ return {
229
+ "type": "error",
230
+ "message":
231
+ (
232
+ "%s is a %s, which is neither a PopupMenu nor something holding one"
233
+ % [node_path, node.get_class()]
234
+ )
235
+ }
236
+
237
+ if not params.has("index") and str(params.get("text", "")).is_empty():
238
+ return {
239
+ "type": "error",
240
+ "message":
241
+ (
242
+ "%s needs the item named, by text or index. It holds: %s"
243
+ % [node_path, ", ".join(Menus.items_of(menu))]
244
+ )
245
+ }
246
+
247
+ # A button a player cannot use is refused, as a click refuses one: the engine opens the menu of a
248
+ # disabled or hidden OptionButton when asked to, measured on 4.7.2, and the pick then changed a
249
+ # selection no player could have changed.
250
+ var holder: BaseButton = node as BaseButton
251
+ if holder != null and holder.disabled:
252
+ return {"type": "error", "message": "%s is disabled, so its menu cannot be opened" % node_path}
253
+ if holder != null and not holder.is_visible_in_tree():
254
+ return {"type": "error", "message": "%s is not visible, so its menu cannot be opened" % node_path}
255
+
256
+ # Shown first, because a menu nobody has opened has no focus to move and the press would go to
257
+ # whatever is behind it, and before the item is looked for, because opening is when a game fills
258
+ # a menu: one rebuilt on about_to_popup was read as it stood before, measured on 4.7.2, and the
259
+ # index found in the old items chose another item of the new ones. An OptionButton opens its own;
260
+ # a bare PopupMenu is popped where it already sits, which leaves a menu already open where it is.
261
+ var opened: bool = Menus.open_the_menu(node, menu)
262
+ await _host.get_tree().process_frame
263
+
264
+ var index: int = Menus.wanted_item(menu, params)
265
+ var refused: String = _not_a_choice(node_path, menu, index)
266
+ if not refused.is_empty():
267
+ if opened and is_instance_valid(menu):
268
+ menu.hide()
269
+ return {"type": "error", "message": refused}
270
+
271
+ # Read before the press, because the press can take the menu away: a game that rebuilds its
272
+ # settings screen on item_selected frees the button and its menu inside the pick, and reading
273
+ # the item afterwards was an engine error in the game's log and an empty answer to a pick that
274
+ # had taken.
275
+ var answer: Dictionary = {
276
+ "type": "chosen",
277
+ "path": node_path,
278
+ "index": index,
279
+ "text": Words.item_says(menu, index),
280
+ "id": menu.get_item_id(index),
281
+ "opened": opened,
282
+ "menu": str(menu.get_path()),
283
+ }
284
+ # Heard rather than assumed: an item pressed is the only evidence the menu took it.
285
+ var fired: Array[int] = []
286
+ var hear: Callable = func(at: int) -> void: fired.append(at)
287
+ var _listening: int = menu.index_pressed.connect(hear)
288
+ menu.scroll_to_item(index)
289
+ menu.set_focused_item(index)
290
+ # Through Input rather than pushed at the menu, which is how a keyboard reaches an open one: a
291
+ # popup is a Window, it takes the focus when it opens, and Input delivers to whichever window
292
+ # has it. Pushed straight at the menu the event arrived and nothing happened.
293
+ Input.parse_input_event(_accept(true))
294
+ await _host.get_tree().process_frame
295
+ Input.parse_input_event(_accept(false))
296
+ await _host.get_tree().process_frame
297
+ if is_instance_valid(menu) and menu.index_pressed.is_connected(hear):
298
+ menu.index_pressed.disconnect(hear)
299
+
300
+ if not fired.has(index):
301
+ if opened and is_instance_valid(menu) and menu.visible:
302
+ menu.hide()
303
+ return {
304
+ "type": "error",
305
+ "message":
306
+ (
307
+ (
308
+ "%s item %d, %s, had the focus and ui_accept was pressed, and the menu did not take it,"
309
+ + " so nothing was chosen"
310
+ )
311
+ % [node_path, index, answer["text"]]
312
+ )
313
+ }
314
+
315
+ answer["control_afterwards"] = Values.afterwards(node)
316
+ # What the button in front of the menu reads now: a menu item that fired changes the thing
317
+ # holding it. A button the pick took away has nothing to read, and that it went is the answer.
318
+ if answer["control_afterwards"] == "in_tree" and node is OptionButton:
319
+ var chooser: OptionButton = node
320
+ answer["selected"] = chooser.get_selected()
321
+ answer["shows"] = Words.said_by(chooser)
322
+ return answer
323
+
324
+
325
+ ## Why item [param index] of [param menu] is not something to choose, or "" when it is.
326
+ static func _not_a_choice(node_path: String, menu: PopupMenu, index: int) -> String:
327
+ if index < 0:
328
+ return "%s has no such item. It holds: %s" % [node_path, ", ".join(Menus.items_of(menu))]
329
+ if menu.is_item_separator(index):
330
+ var heading: String = Words.item_says(menu, index).strip_edges()
331
+ if heading.is_empty():
332
+ return "%s item %d is a separator, not a choice" % [node_path, index]
333
+ return (
334
+ "%s item %d, %s, is a separator heading the items below it, not a choice"
335
+ % [node_path, index, heading]
336
+ )
337
+ if menu.is_item_disabled(index):
338
+ return "%s item %d, %s, is disabled" % [node_path, index, Words.item_says(menu, index)]
339
+ return ""
340
+
341
+
342
+ ## The press or release of `ui_accept`, which is what [PopupMenu] takes its focused item on.
343
+ ##
344
+ ## The action rather than the Enter key it is usually bound to: a project is free to bind it to
345
+ ## something else, and with Enter taken off it the key did nothing, measured on 4.7.2, while the
346
+ ## action chose the item.
347
+ static func _accept(pressed: bool) -> InputEventAction:
348
+ var event: InputEventAction = InputEventAction.new()
349
+ event.action = "ui_accept"
350
+ event.pressed = pressed
351
+ return event
@@ -0,0 +1 @@
1
+ uid://3a2opgsbkmu
@@ -94,12 +94,17 @@ func parameter_class(object: Object, method: String, index: int) -> String:
94
94
 
95
95
 
96
96
  ## What the slot [param named] on [param holder] declares it holds, as `{"type": int, "class":
97
- ## String}`: a property's declaration, or the element type of a typed list or the value type of a
98
- ## typed map. TYPE_NIL when nothing is declared.
97
+ ## String, "script": Script or null}`: a property's declaration, or the element type of a typed list
98
+ ## or the value type of a typed map. TYPE_NIL when nothing is declared, which is also what a
99
+ ## property declared as Variant or left untyped reads as: either way it takes anything.
99
100
  ##
100
101
  ## Asked rather than read off the value in the slot, because an object slot can be empty: a
101
102
  ## property typed `Gear` holding null reads as TYPE_NIL, and anything written into it passed the
102
103
  ## check and was then dropped by the engine, which answered with the slot still null.
104
+ ##
105
+ ## The script only for a typed container, the one place the engine hands it over. A property typed
106
+ ## as a script class with no `class_name` is declared under the engine class the script extends,
107
+ ## measured on 4.7.2: `var quarrel: Walker` with Walker a preloaded Node3D script lists as Node3D.
103
108
  static func slot_declared(holder: Variant, named: String) -> Dictionary:
104
109
  if holder is Array:
105
110
  var items: Array = holder
@@ -107,6 +112,7 @@ static func slot_declared(holder: Variant, named: String) -> Dictionary:
107
112
  return {
108
113
  "type": items.get_typed_builtin(),
109
114
  "class": _typed_class(items.get_typed_script(), items.get_typed_class_name()),
115
+ "script": _script_or_null(items.get_typed_script()),
110
116
  }
111
117
  elif holder is Dictionary:
112
118
  var map: Dictionary = holder
@@ -114,6 +120,7 @@ static func slot_declared(holder: Variant, named: String) -> Dictionary:
114
120
  return {
115
121
  "type": map.get_typed_value_builtin(),
116
122
  "class": _typed_class(map.get_typed_value_script(), map.get_typed_value_class_name()),
123
+ "script": _script_or_null(map.get_typed_value_script()),
117
124
  }
118
125
  elif holder is Object:
119
126
  var object: Object = holder
@@ -122,8 +129,13 @@ static func slot_declared(holder: Variant, named: String) -> Dictionary:
122
129
  return {
123
130
  "type": Read.as_int(entry.get("type", TYPE_NIL), TYPE_NIL),
124
131
  "class": str(entry.get("class_name", "")),
132
+ "script": null,
125
133
  }
126
- return {"type": TYPE_NIL, "class": ""}
134
+ return {"type": TYPE_NIL, "class": "", "script": null}
135
+
136
+
137
+ static func _script_or_null(typed: Variant) -> Script:
138
+ return typed if typed is Script else null
127
139
 
128
140
 
129
141
  ## The class a typed container names: its script's global name where it has one, since the engine
@@ -138,7 +150,20 @@ static func _typed_class(script: Variant, engine_class: StringName) -> String:
138
150
 
139
151
  ## Whether [param object] is a [param declared]: an engine class, or a script class by its global
140
152
  ## name anywhere along the script's inheritance.
141
- static func is_a(object: Object, declared: String) -> bool:
153
+ ##
154
+ ## Against [param declared_script] itself when there is one. A script with no global name is named by
155
+ ## the engine class it extends, so an object of that class with another script, or none, passes the
156
+ ## name, and a typed list built with it drops the element: measured on 4.7.2, an Array[Walker] given
157
+ ## a plain Node3D comes out empty, the engine's errors going to the game's output alone.
158
+ static func is_a(object: Object, declared: String, declared_script: Script = null) -> bool:
159
+ if declared_script != null:
160
+ var own: Variant = object.get_script()
161
+ while own is Script:
162
+ if own == declared_script:
163
+ return true
164
+ var along: Script = own
165
+ own = along.get_base_script()
166
+ return false
142
167
  if object.is_class(declared):
143
168
  return true
144
169
  var script: Variant = object.get_script()
@@ -150,16 +175,41 @@ static func is_a(object: Object, declared: String) -> bool:
150
175
  return false
151
176
 
152
177
 
153
- ## What [param object] is, by its script class where it has one.
178
+ ## What became of [param node] once an input it was given has run: still in the tree, taken out of
179
+ ## it, or freed. Untyped, because a freed reference handed to a typed parameter is itself the engine
180
+ ## error this is here to avoid.
181
+ static func afterwards(node: Variant) -> String:
182
+ if not is_instance_valid(node):
183
+ return "freed"
184
+ if not node is Node:
185
+ return "removed"
186
+ var placed: Node = node
187
+ return "in_tree" if placed.is_inside_tree() else "removed"
188
+
189
+
190
+ ## What [param object] is, by its script class where it has one, and by its script's file where
191
+ ## that has no global name, since the engine class alone is what a refusal about the script has
192
+ ## just said it matched.
154
193
  static func class_of(object: Object) -> String:
155
194
  var script: Variant = object.get_script()
156
195
  if script is Script:
157
196
  var current: Script = script
158
197
  if not current.get_global_name().is_empty():
159
198
  return str(current.get_global_name())
199
+ return "%s with the script %s" % [object.get_class(), script_named(current)]
160
200
  return object.get_class()
161
201
 
162
202
 
203
+ ## How a refusal names [param script]: its global name, or its file. A class declared inside another
204
+ ## script has neither, measured on 4.7.2, so it is called an inner class and no more.
205
+ static func script_named(script: Script) -> String:
206
+ if not script.get_global_name().is_empty():
207
+ return str(script.get_global_name())
208
+ if not script.resource_path.is_empty():
209
+ return script.resource_path
210
+ return "an inner class"
211
+
212
+
163
213
  ## A value from the wire fitted to the type a property or parameter declares.
164
214
  func fitted(value: Variant, type: int) -> Variant:
165
215
  return _shared.fitted(value, type)
@@ -224,9 +274,13 @@ static func _element_type(named: String) -> Dictionary:
224
274
  ## A typed container refuses a plain one, and the engine says nothing about it: an Array[int]
225
275
  ## property written with the list JSON carries kept what it held, and the answer showed the write
226
276
  ## as done. Each element is fitted the way a single value is, and an object element is named by
227
- ## its path through [param resolve], which is called with the path and the class declared and
228
- ## answers the way an object argument does. Anything that is not a typed container comes back as
229
- ## it was given.
277
+ ## its path through [param resolve], which is called with the path, the class declared and the
278
+ ## script declared, and answers the way an object argument does. Anything that is not a typed
279
+ ## container comes back as it was given.
280
+ ##
281
+ ## The container built is counted against what went into it. The engine drops an element it will
282
+ ## not take and says so only in the game's output, so a list that came out shorter is refused here
283
+ ## rather than written, which would empty the game's own.
230
284
  func typed_like(given: Variant, like: Variant, resolve: Callable) -> Dictionary:
231
285
  if like is Array and given is Array:
232
286
  var template: Array = like
@@ -239,20 +293,18 @@ func typed_like(given: Variant, like: Variant, resolve: Callable) -> Dictionary:
239
293
  items[index],
240
294
  template.get_typed_builtin(),
241
295
  _typed_class(template.get_typed_script(), template.get_typed_class_name()),
296
+ _script_or_null(template.get_typed_script()),
242
297
  resolve
243
298
  )
244
299
  if element.has("message"):
245
300
  return {"message": "element %d %s" % [index, element["message"]]}
246
301
  built.append(element["value"])
247
- return {
248
- "value":
249
- Array(
250
- built,
251
- template.get_typed_builtin(),
252
- template.get_typed_class_name(),
253
- template.get_typed_script()
254
- )
255
- }
302
+ var typed: Array = Array(
303
+ built, template.get_typed_builtin(), template.get_typed_class_name(), template.get_typed_script()
304
+ )
305
+ if typed.size() != built.size():
306
+ return {"message": "the engine would not take its elements as what the list holds"}
307
+ return {"value": typed}
256
308
  if like is Dictionary and given is Dictionary:
257
309
  var map_like: Dictionary = like
258
310
  if not map_like.is_typed():
@@ -264,6 +316,7 @@ func typed_like(given: Variant, like: Variant, resolve: Callable) -> Dictionary:
264
316
  key,
265
317
  map_like.get_typed_key_builtin(),
266
318
  _typed_class(map_like.get_typed_key_script(), map_like.get_typed_key_class_name()),
319
+ _script_or_null(map_like.get_typed_key_script()),
267
320
  resolve
268
321
  )
269
322
  if key_fitted.has("message"):
@@ -272,33 +325,37 @@ func typed_like(given: Variant, like: Variant, resolve: Callable) -> Dictionary:
272
325
  entries[key],
273
326
  map_like.get_typed_value_builtin(),
274
327
  _typed_class(map_like.get_typed_value_script(), map_like.get_typed_value_class_name()),
328
+ _script_or_null(map_like.get_typed_value_script()),
275
329
  resolve
276
330
  )
277
331
  if value_fitted.has("message"):
278
332
  return {"message": "the value under %s %s" % [str(key), value_fitted["message"]]}
279
333
  rebuilt[key_fitted["value"]] = value_fitted["value"]
280
- return {
281
- "value":
282
- Dictionary(
283
- rebuilt,
284
- map_like.get_typed_key_builtin(),
285
- map_like.get_typed_key_class_name(),
286
- map_like.get_typed_key_script(),
287
- map_like.get_typed_value_builtin(),
288
- map_like.get_typed_value_class_name(),
289
- map_like.get_typed_value_script()
290
- )
291
- }
334
+ var typed_map: Dictionary = Dictionary(
335
+ rebuilt,
336
+ map_like.get_typed_key_builtin(),
337
+ map_like.get_typed_key_class_name(),
338
+ map_like.get_typed_key_script(),
339
+ map_like.get_typed_value_builtin(),
340
+ map_like.get_typed_value_class_name(),
341
+ map_like.get_typed_value_script()
342
+ )
343
+ if typed_map.size() != rebuilt.size():
344
+ return {"message": "the engine would not take its entries as what the map holds"}
345
+ return {"value": typed_map}
292
346
  return {"value": given}
293
347
 
294
348
 
295
349
  ## One element fitted to a container's element type, as `{"value"}` or `{"message"}` saying what it
296
350
  ## was and what goes there. The message is the part after "element N".
297
- func _element(item: Variant, builtin: int, declared: String, resolve: Callable) -> Dictionary:
351
+ ##
352
+ ## An object element given as a record goes to [param resolve] as a path would, which says how an
353
+ ## object is named, rather than being refused as a map that cannot become the class.
354
+ func _element(item: Variant, builtin: int, declared: String, script: Script, resolve: Callable) -> Dictionary:
298
355
  if builtin == TYPE_NIL:
299
356
  return {"value": item}
300
- if builtin == TYPE_OBJECT and item is String:
301
- var named: Dictionary = resolve.call(item, declared)
357
+ if builtin == TYPE_OBJECT and (item is String or item is Dictionary):
358
+ var named: Dictionary = resolve.call(item, declared, script)
302
359
  if named.has("message"):
303
360
  return {"message": str(named["message"])}
304
361
  return {"value": named["object"]}
@@ -306,6 +363,8 @@ func _element(item: Variant, builtin: int, declared: String, resolve: Callable)
306
363
  if not acceptable(fitted_item, builtin):
307
364
  # The value as it was sent rather than its engine type: JSON numbers arrive as floats, so a 7
308
365
  # the caller wrote was named "float".
309
- var wanted: String = declared if builtin == TYPE_OBJECT else type_string(builtin)
366
+ var wanted: String = type_string(builtin)
367
+ if builtin == TYPE_OBJECT:
368
+ wanted = declared if script == null else script_named(script)
310
369
  return {"message": "is %s, which cannot become %s" % [JSON.stringify(serialize(item)), wanted]}
311
370
  return {"value": fitted_item}
@@ -3,6 +3,7 @@ extends RefCounted
3
3
  ## The commands that take time: they let the game run and answer when what was waited for has
4
4
  ## happened, or when the time ran out, so the caller never sleeps for a guessed length.
5
5
 
6
+ const Changes = preload("runtime_changes.gd")
6
7
  const Paths = preload("runtime_paths.gd")
7
8
  const Queries = preload("runtime_queries.gd")
8
9
  const Read = preload("reading.gd")
@@ -149,7 +150,7 @@ func wait_until(params: Dictionary) -> Dictionary:
149
150
  # two ops would disagree about one property, and the wait would run out on the text null.
150
151
  var untyped_object: bool = declared == TYPE_NIL and (current == null or typeof(current) == TYPE_OBJECT)
151
152
  var holds_objects: bool = declared == TYPE_OBJECT or untyped_object
152
- if holds_objects and wanted is String and str(wanted).strip_edges() == "null":
153
+ if holds_objects and Changes.names_no_object(wanted):
153
154
  wanted = null
154
155
  var refused: String = _not_comparable(current, wanted, node_path, property)
155
156
  var never_held: bool = typeof(wanted) != TYPE_NIL and typeof(wanted) != TYPE_OBJECT
@@ -208,6 +209,17 @@ static func _not_comparable(current: Variant, wanted: Variant, node_path: String
208
209
  var types: Array[String] = [
209
210
  node_path, property, type_string(typeof(current)), type_string(typeof(wanted))
210
211
  ]
212
+ # A value that cannot become what the property holds is said as that, which names the remedy,
213
+ # before the comparison it would also fail; an object on either side is said as the comparison.
214
+ var objects: bool = typeof(current) == TYPE_OBJECT or typeof(wanted) == TYPE_OBJECT
215
+ if not objects and not Values.acceptable(wanted, typeof(current)):
216
+ return (
217
+ (
218
+ "%s.%s holds %s and the value to wait for is %s, which cannot become one:"
219
+ + " waiting on it would answer about a state nobody asked for."
220
+ )
221
+ % types
222
+ )
211
223
  if not Values.comparable(current, wanted):
212
224
  return (
213
225
  (
@@ -247,16 +259,15 @@ static func _watched(node: Node, node_path: String, property: String) -> Diction
247
259
  return {"value": Paths.read_under(holder, named), "declared": _declared_type(holder, named)}
248
260
 
249
261
 
250
- ## The type [param named] is declared with on [param holder], or TYPE_NIL for anything that declares
251
- ## none: an untyped property, a step into a list or map, or a call.
262
+ ## The type [param named] is declared with on [param holder], read the way a set reads it, so the two
263
+ ## refuse the same values: a property's declaration, or a typed list's or map's element type. Read
264
+ ## off properties alone, an element of an Array[Node] holding null declares nothing, and a wait for 5
265
+ ## on it runs out the timeout while a set of 5 on it is refused at once. TYPE_NIL for anything
266
+ ## declaring none, and for a call, whose answer nothing declares.
252
267
  static func _declared_type(holder: Variant, named: String) -> int:
253
- if typeof(holder) != TYPE_OBJECT or not Paths.method_of(named).is_empty():
268
+ if not Paths.method_of(named).is_empty():
254
269
  return TYPE_NIL
255
- var object: Object = holder
256
- for entry: Dictionary in object.get_property_list():
257
- if str(entry.get("name", "")) == named:
258
- return Read.as_int(entry.get("type", TYPE_NIL), TYPE_NIL)
259
- return TYPE_NIL
270
+ return Values.slot_declared(holder, named)["type"]
260
271
 
261
272
 
262
273
  ## Waits until something under [param node_path] has [param said] written on it.