gdharness 1.1.21 → 1.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +1036 -525
- package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
- package/build/godot/addons/gdharness_editor/reading.gd +17 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
- package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
- package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +8 -4
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +28 -8
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +7 -299
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +20 -9
- package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
- package/build/godot/operations/audio_buses.gd +47 -11
- package/build/godot/operations/class_cache.gd +18 -2
- package/build/godot/operations/dependencies.gd +128 -17
- package/build/godot/operations/file_walk.gd +20 -0
- package/build/godot/operations/gdscript_analysis.gd +59 -20
- package/build/godot/operations/gdscript_authoring.gd +127 -60
- package/build/godot/operations/godot_operations.gd +24 -5
- package/build/godot/operations/import_pipeline.gd +179 -19
- package/build/godot/operations/logger.gd +2 -2
- package/build/godot/operations/plugins.gd +88 -70
- package/build/godot/operations/project_config.gd +41 -6
- package/build/godot/operations/project_diagnostics.gd +25 -10
- package/build/godot/operations/reading.gd +17 -0
- package/build/godot/operations/resource_files.gd +60 -17
- package/build/godot/operations/serialisation.gd +116 -7
- package/build/index.js +949 -435
- package/package.json +1 -1
|
@@ -0,0 +1,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
|
|
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
|
-
|
|
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
|
|
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
|
|
228
|
-
## answers the way an object argument does. Anything that is not a typed
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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],
|
|
251
|
-
##
|
|
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
|
|
268
|
+
if not Paths.method_of(named).is_empty():
|
|
254
269
|
return TYPE_NIL
|
|
255
|
-
|
|
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.
|