gdharness 1.1.22 → 1.1.23
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 +452 -219
- package/build/godot/addons/gdharness_editor/bridge_client.gd +12 -2
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +7 -1
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +51 -5
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +61 -8
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +105 -49
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +41 -19
- package/build/godot/operations/classdb_queries.gd +28 -17
- package/build/godot/operations/project_diagnostics.gd +2 -1
- package/build/index.js +442 -124
- package/build/keeper.js +4 -0
- package/package.json +1 -1
|
@@ -98,12 +98,21 @@ var _tried_announced: bool = false
|
|
|
98
98
|
var _told_about_url: String = ""
|
|
99
99
|
var _connecting_for: float = 0.0
|
|
100
100
|
|
|
101
|
+
## Whether this editor was started by another editor, which is Godot's own restart and the project
|
|
102
|
+
## manager opening a project: the new editor is the old one's child and has its environment, with
|
|
103
|
+
## the variable this addon sets as it loads still naming the old one. Its arguments are gone, since
|
|
104
|
+
## the engine consumes `--lsp-port`, `--dap-port` and `--log-file` and hands none of them back, so the
|
|
105
|
+
## ports and the marker a server put in that environment describe the editor before this one.
|
|
106
|
+
var _started_by_an_editor: bool = false
|
|
107
|
+
|
|
101
108
|
|
|
102
109
|
func _ready() -> void:
|
|
103
110
|
_project_path = ProjectSettings.globalize_path("res://")
|
|
104
111
|
version_at_load = _loaded_version()
|
|
105
112
|
digest_at_load = _loaded_digest()
|
|
106
113
|
_keep_breakpoints_through_sessions()
|
|
114
|
+
var inherited: String = OS.get_environment(EDITOR_PID_VARIABLE)
|
|
115
|
+
_started_by_an_editor = not inherited.is_empty() and inherited != str(OS.get_process_id())
|
|
107
116
|
OS.set_environment(EDITOR_PID_VARIABLE, str(OS.get_process_id()))
|
|
108
117
|
|
|
109
118
|
_reconnect_timer = Timer.new()
|
|
@@ -306,7 +315,8 @@ func _handle_connect() -> void:
|
|
|
306
315
|
# Whether a server started this editor, which is the only kind that can be started again
|
|
307
316
|
# as itself: the engine hands back none of the arguments it was given, so the arguments
|
|
308
317
|
# have to come from whoever wrote them. See `restart_editor` in play_tools.gd.
|
|
309
|
-
"opened_by_a_server": _opened_by_a_server()
|
|
318
|
+
"opened_by_a_server": _opened_by_a_server() and not _started_by_an_editor,
|
|
319
|
+
"started_by_an_editor": _started_by_an_editor
|
|
310
320
|
}
|
|
311
321
|
)
|
|
312
322
|
|
|
@@ -338,7 +348,7 @@ static func _opened_by_a_server() -> bool:
|
|
|
338
348
|
## collided with the editor it was moved away from. Restarting is done by starting the editor again
|
|
339
349
|
## with the same arguments now, which is `editor_launch restart`, so there is nothing to persist.
|
|
340
350
|
func _serves(variable: String, setting: String) -> int:
|
|
341
|
-
var asked: int = _asked_for(variable)
|
|
351
|
+
var asked: int = 0 if _started_by_an_editor else _asked_for(variable)
|
|
342
352
|
return asked if asked > 0 else _serving(setting)
|
|
343
353
|
|
|
344
354
|
|
|
@@ -416,8 +416,14 @@ func _send_error(client: StreamPeerTCP, request_id: Variant, message: String) ->
|
|
|
416
416
|
_send_response(client, {"type": "error", "message": message, "id": request_id})
|
|
417
417
|
|
|
418
418
|
|
|
419
|
+
## Down with the window only when the close is taken. A game that asks before quitting turns
|
|
420
|
+
## auto_accept_quit off and decides for itself, and one whose player answered No went on running
|
|
421
|
+
## with no runtime and no announcement, so every later call said no game was running. A quit it
|
|
422
|
+
## does take ends the tree, which [method _exit_tree] cleans up after.
|
|
419
423
|
func _notification(what: int) -> void:
|
|
420
|
-
if what
|
|
424
|
+
if what != NOTIFICATION_WM_CLOSE_REQUEST:
|
|
425
|
+
return
|
|
426
|
+
if not is_inside_tree() or get_tree().auto_accept_quit:
|
|
421
427
|
_cleanup()
|
|
422
428
|
|
|
423
429
|
|
|
@@ -13,7 +13,7 @@ func _init(host: Node) -> void:
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
func capture_screenshot(params: Dictionary) -> Dictionary:
|
|
16
|
-
return await _capture(_host.get_tree().root, params)
|
|
16
|
+
return await _capture(_host.get_tree().root, params, true)
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
func capture_viewport(params: Dictionary) -> Dictionary:
|
|
@@ -28,11 +28,53 @@ func capture_viewport(params: Dictionary) -> Dictionary:
|
|
|
28
28
|
if not node is Viewport:
|
|
29
29
|
return {"type": "error", "message": "Node is not a Viewport: " + viewport_path}
|
|
30
30
|
var viewport: Viewport = node
|
|
31
|
-
|
|
31
|
+
if not viewport is SubViewport:
|
|
32
|
+
return await _capture(viewport, params)
|
|
33
|
+
# Drawn now rather than taken as it stands. A SubViewport the game does not redraw every frame
|
|
34
|
+
# (disabled, a spent update-once, or update-when-visible behind a hidden container) keeps the
|
|
35
|
+
# last frame it drew, and that frame was answered as the viewport now. One update is asked for
|
|
36
|
+
# and the game's own mode put back after it.
|
|
37
|
+
var sub: SubViewport = viewport
|
|
38
|
+
var mode: SubViewport.UpdateMode = sub.render_target_update_mode
|
|
39
|
+
# Nothing is drawn without a window, so the frame waited for below may never come; the capture
|
|
40
|
+
# refuses that state itself.
|
|
41
|
+
if mode == SubViewport.UPDATE_ALWAYS or not _host.get_tree().root.can_draw():
|
|
42
|
+
return await _capture(viewport, params)
|
|
43
|
+
sub.render_target_update_mode = SubViewport.UPDATE_ONCE
|
|
44
|
+
await RenderingServer.frame_post_draw
|
|
45
|
+
var answer: Dictionary = await _capture(viewport, params)
|
|
46
|
+
sub.render_target_update_mode = mode
|
|
47
|
+
return answer
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Lays the game's own windows that the root viewport does not draw over [param image], where
|
|
51
|
+
## they sit on screen.
|
|
52
|
+
##
|
|
53
|
+
## With subwindows not embedded, a dialog or a menu's popup is a window of the operating system's
|
|
54
|
+
## with a render target of its own, so a screenshot of the root viewport was the screen without the
|
|
55
|
+
## dialog the game was showing. In tree order, so a window added later lies over an earlier one.
|
|
56
|
+
func _lay_native_windows(image: Image) -> void:
|
|
57
|
+
var root: Window = _host.get_tree().root
|
|
58
|
+
if root.gui_embed_subwindows:
|
|
59
|
+
return
|
|
60
|
+
var pending: Array[Node] = [root]
|
|
61
|
+
while not pending.is_empty():
|
|
62
|
+
var node: Node = pending.pop_back()
|
|
63
|
+
var children: Array[Node] = node.get_children(true)
|
|
64
|
+
for index: int in range(children.size() - 1, -1, -1):
|
|
65
|
+
pending.append(children[index])
|
|
66
|
+
if node == root or not node is Window:
|
|
67
|
+
continue
|
|
68
|
+
var window: Window = node
|
|
69
|
+
if not window.visible or window.is_embedded():
|
|
70
|
+
continue
|
|
71
|
+
var drawn: Image = window.get_texture().get_image()
|
|
72
|
+
if drawn == null or drawn.is_empty():
|
|
73
|
+
continue
|
|
74
|
+
drawn.convert(image.get_format())
|
|
75
|
+
image.blend_rect(drawn, Rect2i(Vector2i.ZERO, drawn.get_size()), window.position - root.position)
|
|
32
76
|
|
|
33
77
|
|
|
34
|
-
## The server names the file, so a game cannot point it at a path of its own choosing; a call
|
|
35
|
-
## with no path is a call the server did not make.
|
|
36
78
|
## The size a capture drawn at [param drawn] is scaled to, from the width and height asked for, 0
|
|
37
79
|
## being not asked. One side alone keeps the picture's proportions, since that is what asking for a
|
|
38
80
|
## smaller picture means: it was ignored unless both came, so the full-size picture was sent back.
|
|
@@ -46,7 +88,9 @@ static func scaled_to(drawn: Vector2i, width: int, height: int) -> Vector2i:
|
|
|
46
88
|
return drawn
|
|
47
89
|
|
|
48
90
|
|
|
49
|
-
|
|
91
|
+
## The server names the file, so a game cannot point it at a path of its own choosing; a call
|
|
92
|
+
## with no path is a call the server did not make.
|
|
93
|
+
func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false) -> Dictionary:
|
|
50
94
|
var requested_path: String = str(params.get("output_path", ""))
|
|
51
95
|
if requested_path.is_empty():
|
|
52
96
|
return {"type": "error", "message": "output_path required"}
|
|
@@ -80,6 +124,8 @@ func _capture(viewport: Viewport, params: Dictionary) -> Dictionary:
|
|
|
80
124
|
var image: Image = viewport_texture.get_image()
|
|
81
125
|
if image == null:
|
|
82
126
|
return {"type": "error", "message": "Failed to capture viewport image"}
|
|
127
|
+
if with_windows:
|
|
128
|
+
_lay_native_windows(image)
|
|
83
129
|
|
|
84
130
|
var drawn: Vector2i = Vector2i(image.get_width(), image.get_height())
|
|
85
131
|
var target: Vector2i = scaled_to(
|
|
@@ -132,7 +132,7 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
132
132
|
#
|
|
133
133
|
# Judged by what reads back alone, so a value of another kind than the one held, which cannot be
|
|
134
134
|
# compared with it, is still caught when the engine drops it.
|
|
135
|
-
var already: bool =
|
|
135
|
+
var already: bool = held_already(given, old_value)
|
|
136
136
|
var unmoved: bool = Values.comparable(now, old_value) and now == old_value
|
|
137
137
|
if unmoved and not already:
|
|
138
138
|
var why: String = (
|
|
@@ -187,7 +187,7 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
187
187
|
## property whose setter ignored a value differing from the held one below 32 bits: it is answered
|
|
188
188
|
## unchanged rather than refused. Vectors and colours hold 32-bit parts in the value itself, so they
|
|
189
189
|
## already compare at the property's precision.
|
|
190
|
-
static func
|
|
190
|
+
static func held_already(given: Variant, held: Variant) -> bool:
|
|
191
191
|
if not Values.comparable(given, held):
|
|
192
192
|
return false
|
|
193
193
|
if given == held:
|
|
@@ -67,6 +67,9 @@ const PACKED: Array[int] = [
|
|
|
67
67
|
## The calls an empty list has nothing to answer, which the engine reports as an error when asked.
|
|
68
68
|
const NEEDS_AN_ELEMENT: Array[String] = ["front", "back", "pick_random"]
|
|
69
69
|
|
|
70
|
+
## Said after every call a path refuses for doing something rather than reading.
|
|
71
|
+
const READS_ONLY: String = ", and a path, which a wait reads every frame, only calls methods that read"
|
|
72
|
+
|
|
70
73
|
|
|
71
74
|
## What [param reaching] names something on, which is the node itself until the name has a colon
|
|
72
75
|
## in it, and the last name along that path.
|
|
@@ -203,7 +206,7 @@ static func can_read(holder: Variant, named: String) -> bool:
|
|
|
203
206
|
var object: Object = holder
|
|
204
207
|
var method: String = method_of(named)
|
|
205
208
|
if not method.is_empty():
|
|
206
|
-
return
|
|
209
|
+
return call_refused(object, method, "").is_empty()
|
|
207
210
|
return _has_property(object, named)
|
|
208
211
|
return false
|
|
209
212
|
|
|
@@ -245,6 +248,62 @@ static func _arguments_required(object: Object, method: String) -> int:
|
|
|
245
248
|
return -1
|
|
246
249
|
|
|
247
250
|
|
|
251
|
+
## Why a path may not call [param method] on [param object], said about [param called_as], or ""
|
|
252
|
+
## when it may.
|
|
253
|
+
##
|
|
254
|
+
## A path is read, and a wait reads it every frame, so a call in one has to be a read. The engine
|
|
255
|
+
## marks its own reading methods const and `queue_free()`, `free()` and `set_name()` not, measured on
|
|
256
|
+
## 4.7.2. A game's methods carry no such mark, so one declared `-> void` is taken at its word, as
|
|
257
|
+
## doing something and answering nothing, and one left untyped is trusted. Without this a find
|
|
258
|
+
## with property "queue_free()" freed every node it matched, and a wait on "advance_day()" called
|
|
259
|
+
## it once a frame and then reported the state it had driven the game to.
|
|
260
|
+
static func call_refused(object: Object, method: String, called_as: String) -> String:
|
|
261
|
+
var required: int = _arguments_required(object, method)
|
|
262
|
+
if required == -1:
|
|
263
|
+
return "%s has no method %s" % [called_as, method]
|
|
264
|
+
if required > 0:
|
|
265
|
+
return (
|
|
266
|
+
"%s.%s takes %d argument%s, and a path can only call a method that takes none"
|
|
267
|
+
% [called_as, method, required, "" if required == 1 else "s"]
|
|
268
|
+
)
|
|
269
|
+
var scripted: Dictionary = _script_method(object, method)
|
|
270
|
+
if not scripted.is_empty():
|
|
271
|
+
var returned: Dictionary = scripted.get("return", {})
|
|
272
|
+
var usage: int = returned.get("usage", 0)
|
|
273
|
+
var type: int = returned.get("type", TYPE_NIL)
|
|
274
|
+
if type == TYPE_NIL and usage & PROPERTY_USAGE_NIL_IS_VARIANT == 0:
|
|
275
|
+
return (
|
|
276
|
+
"%s.%s is declared -> void, so it does something rather than answering%s"
|
|
277
|
+
% [called_as, method, READS_ONLY]
|
|
278
|
+
)
|
|
279
|
+
return ""
|
|
280
|
+
if _native_flags(object, method) & METHOD_FLAG_CONST == 0:
|
|
281
|
+
return "%s.%s changes what it is called on rather than reading it%s" % [called_as, method, READS_ONLY]
|
|
282
|
+
return ""
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
## The entry a script attached to [param object] declares for [param method], or an empty one when
|
|
286
|
+
## the method is the engine's.
|
|
287
|
+
static func _script_method(object: Object, method: String) -> Dictionary:
|
|
288
|
+
var attached: Variant = object.get_script()
|
|
289
|
+
if not attached is Script:
|
|
290
|
+
return {}
|
|
291
|
+
var script: Script = attached
|
|
292
|
+
for entry: Dictionary in script.get_script_method_list():
|
|
293
|
+
if str(entry.get("name", "")) == method:
|
|
294
|
+
return entry
|
|
295
|
+
return {}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
## The flags the engine declares [param method] with on [param object].
|
|
299
|
+
static func _native_flags(object: Object, method: String) -> int:
|
|
300
|
+
for entry: Dictionary in object.get_method_list():
|
|
301
|
+
if str(entry.get("name", "")) == method:
|
|
302
|
+
var flags: int = entry.get("flags", 0)
|
|
303
|
+
return flags
|
|
304
|
+
return 0
|
|
305
|
+
|
|
306
|
+
|
|
248
307
|
## Put [param value] where [param named] points. Only ever called once [method can_read] agrees,
|
|
249
308
|
## and lists and maps are reference types, so writing into one reaches the game's own copy.
|
|
250
309
|
static func write(holder: Variant, named: String, value: Variant) -> void:
|
|
@@ -343,13 +402,7 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
343
402
|
var object: Object = holder
|
|
344
403
|
var method: String = method_of(named)
|
|
345
404
|
if not method.is_empty():
|
|
346
|
-
|
|
347
|
-
if required == -1:
|
|
348
|
-
return "%s has no method %s" % [called_as, method]
|
|
349
|
-
return (
|
|
350
|
-
"%s.%s takes %d argument%s, and a path can only call a method that takes none"
|
|
351
|
-
% [called_as, method, required, "" if required == 1 else "s"]
|
|
352
|
-
)
|
|
405
|
+
return call_refused(object, method, called_as)
|
|
353
406
|
# A method spelled as a property is the likeliest thing behind a name the object has no property
|
|
354
407
|
# for, and the caller is one pair of brackets away from what they meant.
|
|
355
408
|
if object.has_method(named):
|
|
@@ -14,7 +14,6 @@ const Words = preload("runtime_words.gd")
|
|
|
14
14
|
## The most nodes one find answers with, unless asked for fewer: enough for any real query and
|
|
15
15
|
## far short of the tree dump a query exists to avoid.
|
|
16
16
|
const FIND_LIMIT: int = 100
|
|
17
|
-
const FIND_LIMIT_CEILING: int = 1000
|
|
18
17
|
|
|
19
18
|
## What a find can be narrowed by, which is also what it refuses to answer without. Named once so
|
|
20
19
|
## the refusal lists the same set the walk reads.
|
|
@@ -24,6 +23,11 @@ const FILTERS: PackedStringArray = ["class", "script", "name", "group", "says"]
|
|
|
24
23
|
## thousand is a tree somebody pointed this at by mistake.
|
|
25
24
|
const READ_LIMIT: int = 500
|
|
26
25
|
|
|
26
|
+
## The most either can be asked for. A limit above it is refused rather than cut to it: the ceiling
|
|
27
|
+
## was the default for a read, so the remedy its own answer gave, raising the limit, changed nothing
|
|
28
|
+
## and said nothing, and a caller told to raise a number that stays put concludes the screen moved.
|
|
29
|
+
const LIMIT_CEILING: int = 5000
|
|
30
|
+
|
|
27
31
|
var _host: Node
|
|
28
32
|
var _values: Values
|
|
29
33
|
|
|
@@ -88,7 +92,11 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
88
92
|
wanted[filter] = str(params.get(filter, ""))
|
|
89
93
|
var wanted_property: String = str(params.get("property", ""))
|
|
90
94
|
var include_hidden: bool = Read.as_bool(params.get("include_hidden", true), true)
|
|
91
|
-
var limit: int =
|
|
95
|
+
var limit: int = Read.as_int(params.get("limit", FIND_LIMIT), FIND_LIMIT)
|
|
96
|
+
if limit < 1 or limit > LIMIT_CEILING:
|
|
97
|
+
return {
|
|
98
|
+
"type": "error", "message": "limit is from 1 to %d, and %d was asked for" % [LIMIT_CEILING, limit]
|
|
99
|
+
}
|
|
92
100
|
|
|
93
101
|
if not _anything_asked(wanted):
|
|
94
102
|
return {"type": "error", "message": "find_nodes needs at least one of " + ", ".join(FILTERS)}
|
|
@@ -116,6 +124,10 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
116
124
|
for filter: String in wanted:
|
|
117
125
|
widened[filter] = widened_says if filter == "says" else wanted[filter]
|
|
118
126
|
var nearly_said: int = 0
|
|
127
|
+
# The near misses the same find would leave out as hidden, kept apart: the note says what a
|
|
128
|
+
# query would find, and counting these in it named a query that, asked, found none of them.
|
|
129
|
+
var nearly_hidden: int = 0
|
|
130
|
+
var nearly_said_hidden: int = 0
|
|
119
131
|
var hidden: int = 0
|
|
120
132
|
while not pending.is_empty():
|
|
121
133
|
var node: Node = pending.pop_front()
|
|
@@ -129,13 +141,19 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
129
141
|
else:
|
|
130
142
|
found.append(_found(node, wanted_property))
|
|
131
143
|
elif rest and literal and str(node.name).containsn(wanted["name"]):
|
|
132
|
-
|
|
144
|
+
if include_hidden or shown(node):
|
|
145
|
+
nearly += 1
|
|
146
|
+
else:
|
|
147
|
+
nearly_hidden += 1
|
|
133
148
|
elif (
|
|
134
149
|
not widened_says.is_empty()
|
|
135
150
|
and _named(node, wanted["name"])
|
|
136
151
|
and _matches_apart_from_name(node, widened)
|
|
137
152
|
):
|
|
138
|
-
|
|
153
|
+
if include_hidden or shown(node):
|
|
154
|
+
nearly_said += 1
|
|
155
|
+
else:
|
|
156
|
+
nearly_said_hidden += 1
|
|
139
157
|
# Internal children included, which they were not. A ConfirmationDialog builds its Yes and
|
|
140
158
|
# its No as internal nodes, and a ScrollContainer its bars, so a find over a screen for
|
|
141
159
|
# every Button came back without the two buttons the player is being asked to press:
|
|
@@ -151,21 +169,39 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
151
169
|
# Nothing found is the one answer that cannot be told apart from having asked the wrong
|
|
152
170
|
# question, and a name written without a wildcard is the way an agent writes "contains".
|
|
153
171
|
# Said only when it changes the answer, so a genuine nothing stays a plain nothing.
|
|
154
|
-
if found.is_empty() and nearly > 0:
|
|
155
|
-
var
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
172
|
+
if found.is_empty() and nearly + nearly_hidden > 0:
|
|
173
|
+
var named_near: int = nearly if nearly > 0 else nearly_hidden
|
|
174
|
+
var holding: String = "names contain" if named_near > 1 else "name contains"
|
|
175
|
+
(
|
|
176
|
+
notes
|
|
177
|
+
. append(
|
|
178
|
+
(
|
|
179
|
+
'name is matched as a glob against the whole name; %d %snode %s "%s", which "*%s*"%s would find'
|
|
180
|
+
% [
|
|
181
|
+
named_near,
|
|
182
|
+
"" if nearly > 0 else "hidden ",
|
|
183
|
+
holding,
|
|
184
|
+
wanted["name"],
|
|
185
|
+
wanted["name"],
|
|
186
|
+
"" if nearly > 0 else " with includeHidden true",
|
|
187
|
+
]
|
|
188
|
+
)
|
|
160
189
|
)
|
|
161
190
|
)
|
|
162
|
-
if found.is_empty() and nearly_said > 0:
|
|
191
|
+
if found.is_empty() and nearly_said + nearly_said_hidden > 0:
|
|
192
|
+
var said_near: int = nearly_said if nearly_said > 0 else nearly_said_hidden
|
|
163
193
|
(
|
|
164
194
|
notes
|
|
165
195
|
. append(
|
|
166
196
|
(
|
|
167
|
-
'says with a wildcard is matched as a glob against a node\'s whole text; "%s" would find %d
|
|
168
|
-
% [
|
|
197
|
+
'says with a wildcard is matched as a glob against a node\'s whole text; "%s"%s would find %d %snode%s'
|
|
198
|
+
% [
|
|
199
|
+
widened_says,
|
|
200
|
+
"" if nearly_said > 0 else " with includeHidden true",
|
|
201
|
+
said_near,
|
|
202
|
+
"" if nearly_said > 0 else "hidden ",
|
|
203
|
+
"" if said_near == 1 else "s",
|
|
204
|
+
]
|
|
169
205
|
)
|
|
170
206
|
)
|
|
171
207
|
)
|
|
@@ -184,17 +220,52 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
184
220
|
return answer
|
|
185
221
|
|
|
186
222
|
|
|
187
|
-
## Whether the player can see [param node],
|
|
223
|
+
## Whether the player can see [param node], by the rule the engine draws by.
|
|
188
224
|
##
|
|
189
|
-
##
|
|
190
|
-
## a
|
|
191
|
-
##
|
|
192
|
-
##
|
|
193
|
-
##
|
|
225
|
+
## A CanvasItem's visibility comes down through CanvasItem parents only, and one whose parent is not
|
|
226
|
+
## a CanvasItem starts again from its canvas: measured on 4.7.2, a Label under a plain Node under a
|
|
227
|
+
## hidden panel, and one under a hidden Node3D, both answer visible in tree, while one straight under
|
|
228
|
+
## the panel does not. Walking every ancestor called those two hidden, so their words were left off
|
|
229
|
+
## a screen that showed them and a wait on them ran out. What a CanvasItem's rule leaves out is its
|
|
230
|
+
## canvas: a hidden layer hides everything drawn on it while each item keeps its own flag, and a
|
|
231
|
+
## hidden window everything in it. A wait on words asks the same, so it reads this too.
|
|
194
232
|
static func shown(node: Node) -> bool:
|
|
233
|
+
var item: CanvasItem = node as CanvasItem
|
|
234
|
+
if item != null:
|
|
235
|
+
return item.is_visible_in_tree() and _on_a_shown_canvas(item)
|
|
236
|
+
var spatial: Node3D = node as Node3D
|
|
237
|
+
if spatial != null:
|
|
238
|
+
return spatial.is_visible_in_tree() and _in_shown_windows(spatial)
|
|
239
|
+
var layer: CanvasLayer = node as CanvasLayer
|
|
240
|
+
if layer != null:
|
|
241
|
+
return layer.visible and _in_shown_windows(layer)
|
|
242
|
+
var window: Window = node as Window
|
|
243
|
+
if window != null:
|
|
244
|
+
return window.visible and _in_shown_windows(window.get_parent())
|
|
245
|
+
# A node that draws nothing is shown when what it sits in is.
|
|
246
|
+
var parent: Node = node.get_parent()
|
|
247
|
+
return parent == null or shown(parent)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
## Whether the canvas [param item] is drawn on is showing: its layer, and the windows around it.
|
|
251
|
+
static func _on_a_shown_canvas(item: CanvasItem) -> bool:
|
|
252
|
+
var walk: Node = item.get_parent()
|
|
253
|
+
while walk != null:
|
|
254
|
+
var layer: CanvasLayer = walk as CanvasLayer
|
|
255
|
+
if layer != null:
|
|
256
|
+
return layer.visible and _in_shown_windows(layer)
|
|
257
|
+
if walk is Viewport:
|
|
258
|
+
return _in_shown_windows(walk)
|
|
259
|
+
walk = walk.get_parent()
|
|
260
|
+
return true
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
## Whether every window [param node] sits in is showing, itself included when it is one.
|
|
264
|
+
static func _in_shown_windows(node: Node) -> bool:
|
|
195
265
|
var walk: Node = node
|
|
196
266
|
while walk != null:
|
|
197
|
-
|
|
267
|
+
var window: Window = walk as Window
|
|
268
|
+
if window != null and not window.visible:
|
|
198
269
|
return false
|
|
199
270
|
walk = walk.get_parent()
|
|
200
271
|
return true
|
|
@@ -304,7 +375,11 @@ static func _script_is(attached: Script, wanted: String) -> bool:
|
|
|
304
375
|
func read_text(params: Dictionary) -> Dictionary:
|
|
305
376
|
var root_path: String = str(params.get("root", "/root"))
|
|
306
377
|
var include_hidden: bool = Read.as_bool(params.get("include_hidden", false))
|
|
307
|
-
var limit: int =
|
|
378
|
+
var limit: int = Read.as_int(params.get("limit", READ_LIMIT), READ_LIMIT)
|
|
379
|
+
if limit < 1 or limit > LIMIT_CEILING:
|
|
380
|
+
return {
|
|
381
|
+
"type": "error", "message": "limit is from 1 to %d, and %d was asked for" % [LIMIT_CEILING, limit]
|
|
382
|
+
}
|
|
308
383
|
|
|
309
384
|
var reached: Dictionary = Values.node_at(_host.get_tree().root, root_path)
|
|
310
385
|
if reached.has("message"):
|
|
@@ -338,41 +413,22 @@ func read_text(params: Dictionary) -> Dictionary:
|
|
|
338
413
|
## until it is opened, and reading a closed menu would put every item on the screen.
|
|
339
414
|
## Fills [param into] up to [param most] lines and answers with how many further lines the rest of
|
|
340
415
|
## the subtree says, which is what the caller is told rather than left to infer.
|
|
416
|
+
##
|
|
417
|
+
## Asked of each node rather than cut off at the first hidden one, since a hidden panel does not
|
|
418
|
+
## hide a label reached through a plain Node: see [method shown].
|
|
341
419
|
func _read_into(node: Node, include_hidden: bool, most: int, into: Array[String]) -> int:
|
|
342
|
-
if not include_hidden and not _drawn(node):
|
|
343
|
-
return 0
|
|
344
420
|
var left_out: int = 0
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
into.
|
|
348
|
-
|
|
349
|
-
|
|
421
|
+
if include_hidden or shown(node):
|
|
422
|
+
for said: String in Words.lines_said_by(node):
|
|
423
|
+
if into.size() < most:
|
|
424
|
+
into.append(said)
|
|
425
|
+
else:
|
|
426
|
+
left_out += 1
|
|
350
427
|
for child: Node in node.get_children(true):
|
|
351
428
|
left_out += _read_into(child, include_hidden, most, into)
|
|
352
429
|
return left_out
|
|
353
430
|
|
|
354
431
|
|
|
355
|
-
## Whether [param node] is on the screen at all, for the three kinds of thing that can be hidden.
|
|
356
|
-
##
|
|
357
|
-
## A Node3D among them because a hidden one draws nothing, Label3D included: reading a screen or
|
|
358
|
-
## finding what is on it counted a hidden 3D subtree as showing, which is the one answer neither
|
|
359
|
-
## question wants.
|
|
360
|
-
static func _drawn(node: Node) -> bool:
|
|
361
|
-
var control: CanvasItem = node as CanvasItem
|
|
362
|
-
if control != null:
|
|
363
|
-
return control.visible
|
|
364
|
-
# A hidden layer hides everything drawn on it, a pause menu's for one, while each of those keeps
|
|
365
|
-
# its own visible flag, so read as shown it matched the words of a menu nobody could see.
|
|
366
|
-
var layer: CanvasLayer = node as CanvasLayer
|
|
367
|
-
if layer != null:
|
|
368
|
-
return layer.visible
|
|
369
|
-
var spatial: Node3D = node as Node3D
|
|
370
|
-
if spatial != null:
|
|
371
|
-
return spatial.visible
|
|
372
|
-
var window: Window = node as Window
|
|
373
|
-
return window == null or window.visible
|
|
374
|
-
|
|
375
|
-
|
|
376
432
|
## Where a node is on screen: a Control's rectangle, a Node2D's position, or the place a 3D node
|
|
377
433
|
## is drawn in, in both the canvas coordinates the node reports and the window pixels input
|
|
378
434
|
## arrives in. The two differ whenever the project stretches its viewport, which is what made a
|
|
@@ -69,7 +69,6 @@ func wait_signal(params: Dictionary) -> Dictionary:
|
|
|
69
69
|
return {"type": "error", "message": "%s has no signal %s" % [node_path, signal_name]}
|
|
70
70
|
|
|
71
71
|
var catcher: SignalCatcher = SignalCatcher.new()
|
|
72
|
-
catcher.arity = _signal_arity(node, signal_name)
|
|
73
72
|
var callable: Callable = catcher._on_fired
|
|
74
73
|
var listening: Error = node.connect(signal_name, callable, CONNECT_ONE_SHOT)
|
|
75
74
|
if listening != OK:
|
|
@@ -153,6 +152,8 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
153
152
|
if holds_objects and Changes.names_no_object(wanted):
|
|
154
153
|
wanted = null
|
|
155
154
|
var refused: String = _not_comparable(current, wanted, node_path, property)
|
|
155
|
+
if refused.is_empty():
|
|
156
|
+
refused = _cut_to_fit(params["value"], typeof(current), node_path, property)
|
|
156
157
|
var never_held: bool = typeof(wanted) != TYPE_NIL and typeof(wanted) != TYPE_OBJECT
|
|
157
158
|
if refused.is_empty() and declared == TYPE_OBJECT and never_held:
|
|
158
159
|
refused = (
|
|
@@ -169,9 +170,12 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
169
170
|
var frames: int = 0
|
|
170
171
|
# The type is checked every time round, not only at the top: a property that holds an object a
|
|
171
172
|
# frame later is the same error arriving late.
|
|
173
|
+
# At the precision the property keeps, as set compares: a float arrives as 64 bits and most
|
|
174
|
+
# engine properties keep 32, so h_offset waited on as -1.1 read -1.10000002384186 every frame and
|
|
175
|
+
# the wait ran out on a value the game had reached.
|
|
172
176
|
while (
|
|
173
177
|
Values.comparable(current, wanted)
|
|
174
|
-
and
|
|
178
|
+
and not Changes.held_already(wanted, current)
|
|
175
179
|
and Time.get_ticks_msec() - started < timeout_ms
|
|
176
180
|
):
|
|
177
181
|
await _host.get_tree().process_frame
|
|
@@ -187,13 +191,42 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
187
191
|
"type": "condition",
|
|
188
192
|
"path": node_path,
|
|
189
193
|
"property": property,
|
|
190
|
-
"met":
|
|
194
|
+
"met": Changes.held_already(wanted, current),
|
|
191
195
|
"value": _values.serialize(current),
|
|
192
196
|
"elapsed_ms": Time.get_ticks_msec() - started,
|
|
193
197
|
"frames": frames,
|
|
194
198
|
}
|
|
195
199
|
|
|
196
200
|
|
|
201
|
+
## Why [param given] would have to be cut to be waited on a whole number or a truth, or "" when not.
|
|
202
|
+
##
|
|
203
|
+
## A number arrives from JSON as a float, so one is converted to what the property holds, and the
|
|
204
|
+
## conversion truncates: 3.5 waited on an int became 3 and 0.2 on a bool became true, and a property
|
|
205
|
+
## already holding that answered met at once, about a state nobody asked for.
|
|
206
|
+
static func _cut_to_fit(given: Variant, type: int, node_path: String, property: String) -> String:
|
|
207
|
+
if type != TYPE_INT and type != TYPE_BOOL:
|
|
208
|
+
return ""
|
|
209
|
+
var number: Variant = given
|
|
210
|
+
if given is String:
|
|
211
|
+
var text: String = given
|
|
212
|
+
number = Read.json_or_null(text)
|
|
213
|
+
if typeof(number) != TYPE_FLOAT and typeof(number) != TYPE_INT:
|
|
214
|
+
return ""
|
|
215
|
+
var value: float = Read.as_float(number)
|
|
216
|
+
var whole: bool = value == floorf(value)
|
|
217
|
+
if type == TYPE_INT and whole:
|
|
218
|
+
return ""
|
|
219
|
+
if type == TYPE_BOOL and whole and (value == 0.0 or value == 1.0):
|
|
220
|
+
return ""
|
|
221
|
+
return (
|
|
222
|
+
(
|
|
223
|
+
"%s.%s holds %s and the value to wait for is %s, which it would have to be cut to fit:"
|
|
224
|
+
+ " waiting on it would answer about a state nobody asked for."
|
|
225
|
+
)
|
|
226
|
+
% [node_path, property, type_string(type), str(given)]
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
197
230
|
## Why [param wanted] cannot be waited for against [param current], or "" when it can.
|
|
198
231
|
##
|
|
199
232
|
## Refused before anything is evaluated, because the evaluation is what does the damage: an object
|
|
@@ -342,26 +375,15 @@ func _anything_says(node_path: String, said: String, include_hidden: bool) -> bo
|
|
|
342
375
|
return false
|
|
343
376
|
|
|
344
377
|
|
|
345
|
-
func _signal_arity(node: Node, signal_name: String) -> int:
|
|
346
|
-
for entry: Dictionary in node.get_signal_list():
|
|
347
|
-
if entry.get("name", "") == signal_name:
|
|
348
|
-
var declared: Array = entry.get("args", [])
|
|
349
|
-
return declared.size()
|
|
350
|
-
return 0
|
|
351
|
-
|
|
352
|
-
|
|
353
378
|
## Remembers that a signal fired and what it carried, for a wait that polls rather than
|
|
354
|
-
## awaits the signal directly, so the wait can also give up.
|
|
355
|
-
##
|
|
356
|
-
##
|
|
379
|
+
## awaits the signal directly, so the wait can also give up. Takes any number of arguments: a
|
|
380
|
+
## handler of five fixed ones covered the engine's signals, and a game's own signal with six was
|
|
381
|
+
## never delivered to it, so the wait ran out and said the signal had not fired.
|
|
357
382
|
class SignalCatcher:
|
|
358
383
|
extends RefCounted
|
|
359
384
|
var fired: bool = false
|
|
360
|
-
var arity: int = 0
|
|
361
385
|
var args: Array = []
|
|
362
386
|
|
|
363
|
-
func _on_fired(
|
|
364
|
-
a: Variant = null, b: Variant = null, c: Variant = null, d: Variant = null, e: Variant = null
|
|
365
|
-
) -> void:
|
|
387
|
+
func _on_fired(...carried: Array) -> void:
|
|
366
388
|
fired = true
|
|
367
|
-
args =
|
|
389
|
+
args = carried
|