gdharness 1.3.48 → 1.3.49
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 +430 -190
- package/build/godot/addons/gdharness_editor/declared_values.gd +37 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +8 -11
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +42 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +1 -1
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +8 -1
- package/build/godot/addons/gdharness_editor/value_shapes.gd +9 -0
- package/build/godot/addons/gdharness_runtime/declared_values.gd +37 -0
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +32 -9
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +3 -2
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +72 -11
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +14 -1
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +21 -8
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +3 -2
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +9 -0
- package/build/godot/operations/declared_values.gd +37 -0
- package/build/godot/operations/property_values.gd +8 -11
- package/build/godot/operations/value_shapes.gd +9 -0
- package/build/keeper.js +6 -2
- package/package.json +1 -1
|
@@ -113,6 +113,41 @@ static func engine_constants(target: Object, property: String) -> Dictionary:
|
|
|
113
113
|
return values
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
## The number [param word] names as a constant of the global enum the property [param declared] of
|
|
117
|
+
## [param target] holds, such as VERTICAL_ALIGNMENT_BOTTOM, or null when it names none. ClassDB
|
|
118
|
+
## lists no global enum and Expression reads no global constant, so a word spelt from the enum's
|
|
119
|
+
## name and a value the hint gives is compiled, and taken only when the engine reads it as that
|
|
120
|
+
## value: ORIENTATION_VERTICAL is spelt so and is not a constant, which is HORIZONTAL or VERTICAL.
|
|
121
|
+
static func _global_constant(target: Object, declared: Dictionary, word: String) -> Variant:
|
|
122
|
+
var engine_class: String = target.get_class()
|
|
123
|
+
var setter: String = str(
|
|
124
|
+
ClassDB.class_get_property_setter(engine_class, str(declared.get("name", "")))
|
|
125
|
+
)
|
|
126
|
+
var named: String = ""
|
|
127
|
+
for method: Dictionary in ClassDB.class_get_method_list(engine_class):
|
|
128
|
+
var arguments: Array = method.get("args", [])
|
|
129
|
+
if str(method.get("name", "")) == setter and not arguments.is_empty():
|
|
130
|
+
var value_argument: Dictionary = arguments[-1]
|
|
131
|
+
named = str(value_argument.get("class_name", ""))
|
|
132
|
+
break
|
|
133
|
+
if named.is_empty() or "." in named:
|
|
134
|
+
return null
|
|
135
|
+
var wanted: String = word.strip_edges().to_upper()
|
|
136
|
+
var hinted: Dictionary = enum_values(str(declared.get("hint_string", "")))
|
|
137
|
+
for value_word: String in hinted:
|
|
138
|
+
var constant: String = (
|
|
139
|
+
named.to_snake_case().to_upper() + "_" + value_word.to_upper().replace(" ", "_")
|
|
140
|
+
)
|
|
141
|
+
if constant != wanted:
|
|
142
|
+
continue
|
|
143
|
+
var reader: GDScript = GDScript.new()
|
|
144
|
+
reader.source_code = "extends RefCounted\nconst READ = %s\n" % constant
|
|
145
|
+
if (reader.reload() == OK
|
|
146
|
+
and reader.get_script_constant_map().get("READ") == hinted[value_word]):
|
|
147
|
+
return hinted[value_word]
|
|
148
|
+
return null
|
|
149
|
+
|
|
150
|
+
|
|
116
151
|
## [param word] as the number of the value the enum property [param declared] of [param target]
|
|
117
152
|
## names by it, as [true, number], or [false, why] naming the values, with [param holder] naming the
|
|
118
153
|
## target in the refusal. [false, ""] for a word that is a number, which converts as one.
|
|
@@ -132,6 +167,8 @@ static func enum_by_name(
|
|
|
132
167
|
if chosen == null:
|
|
133
168
|
constants = engine_constants(target, property)
|
|
134
169
|
chosen = enum_named(constants, word)
|
|
170
|
+
if chosen == null and constants.is_empty():
|
|
171
|
+
chosen = _global_constant(target, declared, word)
|
|
135
172
|
if chosen != null:
|
|
136
173
|
return [true, chosen]
|
|
137
174
|
if word.strip_edges().is_valid_int():
|
|
@@ -118,7 +118,7 @@ func cannot_hold(
|
|
|
118
118
|
if not GdhSerialisation.acceptable(value, expected_type):
|
|
119
119
|
var shape: String = "%s.%s is %s and the value given is %s, which cannot become one."
|
|
120
120
|
var remedy: String = GdhShapes.remedy(value, expected_type)
|
|
121
|
-
return shape % [holder, property, held,
|
|
121
|
+
return shape % [holder, property, held, GdhShapes.kind(value)] + remedy + cost
|
|
122
122
|
|
|
123
123
|
# And the same question of each element, because a list is accepted for a packed or typed array
|
|
124
124
|
# whatever is in it: a polygon written as [{"x": 1}] passes the check above and lands as one
|
|
@@ -132,7 +132,12 @@ func cannot_hold(
|
|
|
132
132
|
if not GdhSerialisation.acceptable(items[index], element):
|
|
133
133
|
var shape: String = "%s.%s is %s and item %d of the list given is %s, not %s."
|
|
134
134
|
var named: Array = [
|
|
135
|
-
holder,
|
|
135
|
+
holder,
|
|
136
|
+
property,
|
|
137
|
+
held,
|
|
138
|
+
index,
|
|
139
|
+
GdhShapes.kind(items[index]),
|
|
140
|
+
type_string(element),
|
|
136
141
|
]
|
|
137
142
|
return shape % named + GdhShapes.remedy(items[index], element) + cost
|
|
138
143
|
# A list of polygons is a list of lists of points, and each point is read as one too.
|
|
@@ -151,7 +156,7 @@ func cannot_hold(
|
|
|
151
156
|
type_string(inner),
|
|
152
157
|
at,
|
|
153
158
|
index,
|
|
154
|
-
|
|
159
|
+
GdhShapes.kind(points[at]),
|
|
155
160
|
type_string(inner),
|
|
156
161
|
]
|
|
157
162
|
return nested % said + GdhShapes.remedy(points[at], inner) + cost
|
|
@@ -173,14 +178,6 @@ func cannot_hold(
|
|
|
173
178
|
return ""
|
|
174
179
|
|
|
175
180
|
|
|
176
|
-
## What [param value] is, as a refusal names it. A number arrives from JSON as a float whether or
|
|
177
|
-
## not it has a fraction, so [1, 2] given for a list of words was called a list of floats.
|
|
178
|
-
static func _kind(value: Variant) -> String:
|
|
179
|
-
if value is int or value is float:
|
|
180
|
-
return "a number"
|
|
181
|
-
return type_string(typeof(value))
|
|
182
|
-
|
|
183
|
-
|
|
184
181
|
## Writes each of [param properties] onto [param target], stopping at the first it cannot write and
|
|
185
182
|
## answering with why, or "" when every one of them went on and holds what was given.
|
|
186
183
|
##
|
|
@@ -14,6 +14,12 @@ const HEADLESS_DISPLAY: String = "headless"
|
|
|
14
14
|
## for every editor on the machine, and bound only while a game runs.
|
|
15
15
|
const DEBUGGER_SETTING: String = "network/debug/remote_port"
|
|
16
16
|
const LOOPBACK: String = "127.0.0.1"
|
|
17
|
+
## Set in the environment of an editor the server started on its hidden desktop, and so in every
|
|
18
|
+
## game it plays. Kept in step with `DESKTOP_VARIABLE` in the server's desktop module.
|
|
19
|
+
const DESKTOP_VARIABLE: String = "GDHARNESS_DESKTOP"
|
|
20
|
+
const RUN_ARGS_SETTING: String = "editor/run/main_run_args"
|
|
21
|
+
## Kept in step with `HIDDEN_WINDOW_FPS` in the server's launch module.
|
|
22
|
+
const HIDDEN_WINDOW_FPS: int = 60
|
|
17
23
|
|
|
18
24
|
var _editor_plugin: EditorPlugin = null
|
|
19
25
|
## Which play this is, counted up for every one the editor starts, whoever starts it. What a server
|
|
@@ -80,10 +86,26 @@ func play_scene(args: Dictionary) -> Dictionary:
|
|
|
80
86
|
|
|
81
87
|
var debugger: int = _a_port_for_the_debugger()
|
|
82
88
|
|
|
89
|
+
var held: String = str(ProjectSettings.get_setting(RUN_ARGS_SETTING, ""))
|
|
90
|
+
var given: String = with_hidden_window_flags(
|
|
91
|
+
held,
|
|
92
|
+
OS.get_name() == "Windows" and not OS.get_environment(DESKTOP_VARIABLE).is_empty(),
|
|
93
|
+
GdhRead.as_int(ProjectSettings.get_setting("application/run/max_fps", 0)) > 0,
|
|
94
|
+
)
|
|
95
|
+
# The editor builds the game's command line from the text its run instances dialog holds, which
|
|
96
|
+
# it fetches from the setting on settings_changed. The project emits that a frame later, so it
|
|
97
|
+
# is emitted here on both sides of the play, and no frame passes in which anything could save
|
|
98
|
+
# the project with the flags in it.
|
|
99
|
+
if given != held:
|
|
100
|
+
ProjectSettings.set_setting(RUN_ARGS_SETTING, given)
|
|
101
|
+
ProjectSettings.settings_changed.emit()
|
|
83
102
|
if full_path.is_empty():
|
|
84
103
|
EditorInterface.play_main_scene()
|
|
85
104
|
else:
|
|
86
105
|
EditorInterface.play_custom_scene(full_path)
|
|
106
|
+
if given != held:
|
|
107
|
+
ProjectSettings.set_setting(RUN_ARGS_SETTING, held)
|
|
108
|
+
ProjectSettings.settings_changed.emit()
|
|
87
109
|
|
|
88
110
|
_play += 1
|
|
89
111
|
_was_playing = EditorInterface.is_playing_scene()
|
|
@@ -99,6 +121,26 @@ func play_scene(args: Dictionary) -> Dictionary:
|
|
|
99
121
|
}
|
|
100
122
|
|
|
101
123
|
|
|
124
|
+
## [param held], the project's run arguments, with the flags a window on the hidden desktop starts
|
|
125
|
+
## with put in front when [param hidden] says the editor is there: the server's `hiddenWindowFlags`
|
|
126
|
+
## gives the reason. Nothing is added for a game the arguments already play headless, and no cap
|
|
127
|
+
## where [param capped] says the project sets one or the arguments do. In front because a `--` in
|
|
128
|
+
## them makes everything after it the game's own; after `%command%` when they hold one, since what
|
|
129
|
+
## goes before it is the command that runs the engine.
|
|
130
|
+
static func with_hidden_window_flags(held: String, hidden: bool, capped: bool) -> String:
|
|
131
|
+
var words: PackedStringArray = held.split(" ", false)
|
|
132
|
+
if not hidden or words.has("--headless"):
|
|
133
|
+
return held
|
|
134
|
+
var flags: String = "--disable-vsync"
|
|
135
|
+
if not capped and not words.has("--max-fps"):
|
|
136
|
+
flags += " --max-fps %d" % HIDDEN_WINDOW_FPS
|
|
137
|
+
var placeholder: int = held.find("%command%")
|
|
138
|
+
if placeholder == -1:
|
|
139
|
+
return (flags + " " + held).strip_edges()
|
|
140
|
+
var after: int = placeholder + "%command%".length()
|
|
141
|
+
return held.substr(0, after) + " " + flags + held.substr(after)
|
|
142
|
+
|
|
143
|
+
|
|
102
144
|
## Gives this editor's debugger a port of its own, and answers the one the game will be sent to.
|
|
103
145
|
##
|
|
104
146
|
## The editor binds this while a game runs, out of a setting shared by every editor on the
|
|
@@ -212,7 +212,7 @@ func create_resource(args: Dictionary) -> Dictionary:
|
|
|
212
212
|
"ok": false,
|
|
213
213
|
"error":
|
|
214
214
|
(
|
|
215
|
-
"%s is neither an engine class nor a class_name
|
|
215
|
+
"%s is neither an engine class nor a class_name in the project's class list."
|
|
216
216
|
% resource_type
|
|
217
217
|
),
|
|
218
218
|
}
|
|
@@ -50,9 +50,16 @@ static func what_to_add(node_type: String, instance_path: String, scene_path: St
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
## The refusal for [param given], sent as [param argument], which names no node class, with the
|
|
53
|
-
## node classes nearest it, so "Node2DD" is offered Node2D.
|
|
53
|
+
## node classes nearest it, so "Node2DD" is offered Node2D. A name that is no engine class at all
|
|
54
|
+
## may
|
|
55
|
+
## have been meant as a class_name, which is taken too.
|
|
54
56
|
static func not_a_node_class(argument: String, given: String) -> String:
|
|
55
57
|
var said: String = "%s must be a node class, and %s is not one" % [argument, given]
|
|
58
|
+
if not ClassDB.class_exists(given):
|
|
59
|
+
said = (
|
|
60
|
+
"%s must be a node class or a class_name in the project's class list, and %s is neither"
|
|
61
|
+
% [argument, given]
|
|
62
|
+
)
|
|
56
63
|
var classes: PackedStringArray = ClassDB.get_inheriters_from_class("Node")
|
|
57
64
|
var _added: bool = classes.append("Node")
|
|
58
65
|
var close: PackedStringArray = GdhRead.nearest(given, classes)
|
|
@@ -234,6 +234,15 @@ static func packed_from(items: Array, type: int, fit: Callable) -> Variant:
|
|
|
234
234
|
return type_convert(built, type)
|
|
235
235
|
|
|
236
236
|
|
|
237
|
+
## What [param value] is, as a refusal names it. A number arrives from JSON as a float whether or
|
|
238
|
+
## not it has a fraction, so [1, 2] given for a list of words was called a list of floats, and 5
|
|
239
|
+
## given for a Vector2 was called a float.
|
|
240
|
+
static func kind(value: Variant) -> String:
|
|
241
|
+
if value is int or value is float:
|
|
242
|
+
return "a number"
|
|
243
|
+
return type_string(typeof(value))
|
|
244
|
+
|
|
245
|
+
|
|
237
246
|
## The sentences a refusal of [param value] where a [param type] goes ends with: which of its keys
|
|
238
247
|
## kept it from being read as one, and how one is written. Empty for a type not built from fields.
|
|
239
248
|
##
|
|
@@ -113,6 +113,41 @@ static func engine_constants(target: Object, property: String) -> Dictionary:
|
|
|
113
113
|
return values
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
## The number [param word] names as a constant of the global enum the property [param declared] of
|
|
117
|
+
## [param target] holds, such as VERTICAL_ALIGNMENT_BOTTOM, or null when it names none. ClassDB
|
|
118
|
+
## lists no global enum and Expression reads no global constant, so a word spelt from the enum's
|
|
119
|
+
## name and a value the hint gives is compiled, and taken only when the engine reads it as that
|
|
120
|
+
## value: ORIENTATION_VERTICAL is spelt so and is not a constant, which is HORIZONTAL or VERTICAL.
|
|
121
|
+
static func _global_constant(target: Object, declared: Dictionary, word: String) -> Variant:
|
|
122
|
+
var engine_class: String = target.get_class()
|
|
123
|
+
var setter: String = str(
|
|
124
|
+
ClassDB.class_get_property_setter(engine_class, str(declared.get("name", "")))
|
|
125
|
+
)
|
|
126
|
+
var named: String = ""
|
|
127
|
+
for method: Dictionary in ClassDB.class_get_method_list(engine_class):
|
|
128
|
+
var arguments: Array = method.get("args", [])
|
|
129
|
+
if str(method.get("name", "")) == setter and not arguments.is_empty():
|
|
130
|
+
var value_argument: Dictionary = arguments[-1]
|
|
131
|
+
named = str(value_argument.get("class_name", ""))
|
|
132
|
+
break
|
|
133
|
+
if named.is_empty() or "." in named:
|
|
134
|
+
return null
|
|
135
|
+
var wanted: String = word.strip_edges().to_upper()
|
|
136
|
+
var hinted: Dictionary = enum_values(str(declared.get("hint_string", "")))
|
|
137
|
+
for value_word: String in hinted:
|
|
138
|
+
var constant: String = (
|
|
139
|
+
named.to_snake_case().to_upper() + "_" + value_word.to_upper().replace(" ", "_")
|
|
140
|
+
)
|
|
141
|
+
if constant != wanted:
|
|
142
|
+
continue
|
|
143
|
+
var reader: GDScript = GDScript.new()
|
|
144
|
+
reader.source_code = "extends RefCounted\nconst READ = %s\n" % constant
|
|
145
|
+
if (reader.reload() == OK
|
|
146
|
+
and reader.get_script_constant_map().get("READ") == hinted[value_word]):
|
|
147
|
+
return hinted[value_word]
|
|
148
|
+
return null
|
|
149
|
+
|
|
150
|
+
|
|
116
151
|
## [param word] as the number of the value the enum property [param declared] of [param target]
|
|
117
152
|
## names by it, as [true, number], or [false, why] naming the values, with [param holder] naming the
|
|
118
153
|
## target in the refusal. [false, ""] for a word that is a number, which converts as one.
|
|
@@ -132,6 +167,8 @@ static func enum_by_name(
|
|
|
132
167
|
if chosen == null:
|
|
133
168
|
constants = engine_constants(target, property)
|
|
134
169
|
chosen = enum_named(constants, word)
|
|
170
|
+
if chosen == null and constants.is_empty():
|
|
171
|
+
chosen = _global_constant(target, declared, word)
|
|
135
172
|
if chosen != null:
|
|
136
173
|
return [true, chosen]
|
|
137
174
|
if word.strip_edges().is_valid_int():
|
|
@@ -40,6 +40,7 @@ const WALK: String = r"""
|
|
|
40
40
|
$ErrorActionPreference = 'Stop'
|
|
41
41
|
function Answer($value) {
|
|
42
42
|
[System.IO.File]::WriteAllText($Out, (ConvertTo-Json -InputObject $value -Depth 100 -Compress))
|
|
43
|
+
Mark 'answered'
|
|
43
44
|
}
|
|
44
45
|
function Mark($stage) {
|
|
45
46
|
[System.IO.File]::AppendAllText($Progress, "$stage $([DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds())`n")
|
|
@@ -280,14 +281,24 @@ func get_accessibility(params: Dictionary) -> Dictionary:
|
|
|
280
281
|
var looked_at: int = began_at
|
|
281
282
|
while OS.is_process_running(pid):
|
|
282
283
|
var now: int = Time.get_ticks_msec()
|
|
283
|
-
if
|
|
284
|
+
if now - looked_at >= LOOK_MS:
|
|
284
285
|
looked_at = now
|
|
285
|
-
|
|
286
|
+
var marked: String = FileAccess.get_file_as_string(progress)
|
|
287
|
+
if not walking and marked.contains("loaded "):
|
|
286
288
|
walking = true
|
|
287
289
|
deadline = _deadline(began_at, now, handles.size())
|
|
290
|
+
# The answer is whole once it is marked, so the read is done whatever the client does
|
|
291
|
+
# after it, and a client slow to exit costs the read nothing.
|
|
292
|
+
if marked.contains("answered "):
|
|
293
|
+
var _ended: Error = OS.kill(pid)
|
|
294
|
+
break
|
|
288
295
|
if now > deadline:
|
|
289
296
|
var _killed: Error = OS.kill(pid)
|
|
290
|
-
var reached: String = _reached(
|
|
297
|
+
var reached: String = _reached(
|
|
298
|
+
FileAccess.get_file_as_string(progress),
|
|
299
|
+
started_ms,
|
|
300
|
+
int(Time.get_unix_time_from_system() * 1000.0)
|
|
301
|
+
)
|
|
291
302
|
var _gone: Error = DirAccess.remove_absolute(progress)
|
|
292
303
|
return _refused(
|
|
293
304
|
(
|
|
@@ -743,19 +754,22 @@ static func _marks(progress: String) -> Array:
|
|
|
743
754
|
return marks
|
|
744
755
|
|
|
745
756
|
|
|
746
|
-
## How far a client started at [param started_ms]
|
|
747
|
-
## caller whose read it stopped: the time went to starting
|
|
748
|
-
##
|
|
749
|
-
|
|
757
|
+
## How far a client started at [param started_ms] and stopped at [param stopped_ms] got by the marks
|
|
758
|
+
## in [param progress], said for a caller whose read it stopped: the time went to starting
|
|
759
|
+
## PowerShell, loading UI Automation, the walks, or after the last walk, and which tells a loaded
|
|
760
|
+
## machine from a window that never answers.
|
|
761
|
+
static func _reached(progress: String, started_ms: int, stopped_ms: int) -> String:
|
|
750
762
|
var marks: Array = _marks(progress)
|
|
751
763
|
var at: Dictionary = {}
|
|
752
764
|
var walks: int = 0
|
|
753
765
|
var last: String = ""
|
|
766
|
+
var last_at: int = 0
|
|
754
767
|
for mark: Array in marks:
|
|
755
768
|
var stage: String = mark[0]
|
|
756
769
|
if stage.begins_with("walked "):
|
|
757
770
|
walks += 1
|
|
758
771
|
last = stage.trim_prefix("walked ")
|
|
772
|
+
last_at = mark[1]
|
|
759
773
|
elif not at.has(stage):
|
|
760
774
|
at[stage] = mark[1]
|
|
761
775
|
if not at.has("began"):
|
|
@@ -775,9 +789,18 @@ static func _reached(progress: String, started_ms: int) -> String:
|
|
|
775
789
|
return (
|
|
776
790
|
(
|
|
777
791
|
"PowerShell began the client after %d ms and loaded UI Automation in %d ms, and the "
|
|
778
|
-
+ "windows were walked %d time%s, the last finding %s element%s"
|
|
792
|
+
+ "windows were walked %d time%s, the last finding %s element%s and ending %d ms "
|
|
793
|
+
+ "before the client was stopped"
|
|
779
794
|
)
|
|
780
|
-
% [
|
|
795
|
+
% [
|
|
796
|
+
began,
|
|
797
|
+
loaded,
|
|
798
|
+
walks,
|
|
799
|
+
"" if walks == 1 else "s",
|
|
800
|
+
last,
|
|
801
|
+
"" if last == "1" else "s",
|
|
802
|
+
stopped_ms - last_at,
|
|
803
|
+
]
|
|
781
804
|
)
|
|
782
805
|
|
|
783
806
|
|
|
@@ -7,6 +7,7 @@ const GdhCallErrors = preload("call_errors.gd")
|
|
|
7
7
|
const GdhDeclared = preload("declared_values.gd")
|
|
8
8
|
const GdhPaths = preload("runtime_paths.gd")
|
|
9
9
|
const GdhRead = preload("reading.gd")
|
|
10
|
+
const GdhShapes = preload("value_shapes.gd")
|
|
10
11
|
const GdhValues = preload("runtime_values.gd")
|
|
11
12
|
## The most errors a call's answer carries, which is enough to see what the method ran into and
|
|
12
13
|
## short of a method reporting one per element of a list of thousands.
|
|
@@ -254,7 +255,7 @@ func _argument_for(
|
|
|
254
255
|
named,
|
|
255
256
|
type_string(wants),
|
|
256
257
|
index + 1,
|
|
257
|
-
|
|
258
|
+
GdhShapes.kind(sent),
|
|
258
259
|
GdhValues.remedy(sent, wants),
|
|
259
260
|
]
|
|
260
261
|
),
|
|
@@ -379,7 +380,7 @@ func _value_for(
|
|
|
379
380
|
called,
|
|
380
381
|
named,
|
|
381
382
|
type_string(wanted),
|
|
382
|
-
|
|
383
|
+
GdhShapes.kind(value),
|
|
383
384
|
GdhValues.remedy(value, wanted),
|
|
384
385
|
]
|
|
385
386
|
),
|
|
@@ -338,19 +338,23 @@ static func walk_to(node: Object, node_path: String, reaching: String) -> Dictio
|
|
|
338
338
|
##
|
|
339
339
|
## A colon path read from [param node], the node the call was made on, the way the method's own
|
|
340
340
|
## path is: "_ladder:shop:_wares:-15". One beginning with a slash starts at the node it names
|
|
341
|
-
## instead, "/root/Main/Hud" or "/root/Main:_game:run".
|
|
342
|
-
##
|
|
343
|
-
##
|
|
341
|
+
## instead, "/root/Main/Hud" or "/root/Main:_game:run". A res:// or uid:// path names a resource
|
|
342
|
+
## file, which is loaded the way the game loads one, so a resource the game already holds is handed
|
|
343
|
+
## over as that instance. The records a read answers a node and a filed resource with carry such a
|
|
344
|
+
## path and are taken as it. The record runtime_inspect answers any other object with is not
|
|
345
|
+
## accepted, because its id is a 64-bit number and does not survive being sent as JSON: a rounded id
|
|
346
|
+
## names another object or none, and the path names the one meant.
|
|
344
347
|
static func object_at(root: Node, node: Node, node_path: String, given: Variant) -> Dictionary:
|
|
348
|
+
var named: Variant = given
|
|
345
349
|
if given is Dictionary:
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
350
|
+
var record: Dictionary = given
|
|
351
|
+
var tag: String = str(record.get("_type", ""))
|
|
352
|
+
if not (tag in ["Node", "Resource"] and record.get("path") is String):
|
|
353
|
+
return { "message": _no_path_in(record) }
|
|
354
|
+
named = record["path"]
|
|
355
|
+
var path: String = named
|
|
356
|
+
if path.begins_with("res://") or path.begins_with("uid://"):
|
|
357
|
+
return _resource_at(path)
|
|
354
358
|
var start: Node = node
|
|
355
359
|
var start_path: String = node_path
|
|
356
360
|
var reaching: String = path
|
|
@@ -382,6 +386,63 @@ static func object_at(root: Node, node: Node, node_path: String, given: Variant)
|
|
|
382
386
|
return { "object": value }
|
|
383
387
|
|
|
384
388
|
|
|
389
|
+
## Why [param record], a dictionary given where an object goes, names none. A resource built into a
|
|
390
|
+
## scene is read as its class beside its properties and has no path; the runtime builds no resource
|
|
391
|
+
## from those, so the remedy is the file, not the path the other records are told to use.
|
|
392
|
+
static func _no_path_in(record: Dictionary) -> String:
|
|
393
|
+
if record.has("id"):
|
|
394
|
+
return (
|
|
395
|
+
"an object is named by its path, not by the record runtime_inspect answered with:"
|
|
396
|
+
+ " its id does not survive the trip as a number"
|
|
397
|
+
)
|
|
398
|
+
var sort: String = str(record.get("_type", record.get("class", "")))
|
|
399
|
+
if sort.is_empty():
|
|
400
|
+
return (
|
|
401
|
+
"an object is named by its path, or a resource by the res:// path of its file, and a"
|
|
402
|
+
+ " dictionary names neither"
|
|
403
|
+
)
|
|
404
|
+
return (
|
|
405
|
+
(
|
|
406
|
+
"a resource is named by the res:// path of its file, and the %s given has none: a"
|
|
407
|
+
+ " resource built into a scene has no path, and the game builds none from its"
|
|
408
|
+
+ " properties"
|
|
409
|
+
)
|
|
410
|
+
% sort
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
## The resource at [param path], a res:// or uid:// path, as `{"object": ...}`, or
|
|
415
|
+
## `{"message": ...}` saying why there is none to hand over.
|
|
416
|
+
static func _resource_at(path: String) -> Dictionary:
|
|
417
|
+
if path.contains("::"):
|
|
418
|
+
return {
|
|
419
|
+
"message":
|
|
420
|
+
(
|
|
421
|
+
"%s is a resource built into %s, and only that file can refer to it"
|
|
422
|
+
% [path, path.get_slice("::", 0)]
|
|
423
|
+
),
|
|
424
|
+
}
|
|
425
|
+
if not ResourceLoader.exists(path):
|
|
426
|
+
# An image or a sound nobody has imported is a file the engine has no loader for, and "No
|
|
427
|
+
# resource at" sent a caller looking for a file that was there.
|
|
428
|
+
if path.begins_with("res://") and FileAccess.file_exists(path):
|
|
429
|
+
return {
|
|
430
|
+
"message":
|
|
431
|
+
(
|
|
432
|
+
(
|
|
433
|
+
"%s is there, but it has never been imported, so the game has no resource "
|
|
434
|
+
+ "to load from it. project_import reimport with resourcePath %s imports it"
|
|
435
|
+
)
|
|
436
|
+
% [path, path]
|
|
437
|
+
),
|
|
438
|
+
}
|
|
439
|
+
return { "message": "No resource at %s" % path }
|
|
440
|
+
var loaded: Resource = load(path)
|
|
441
|
+
if loaded == null:
|
|
442
|
+
return { "message": "%s did not load; the game's output says why" % path }
|
|
443
|
+
return { "object": loaded }
|
|
444
|
+
|
|
445
|
+
|
|
385
446
|
## Whether a step can be taken into [param value] at all: an object, a list, a map or a value with
|
|
386
447
|
## components can be walked into, and a number or a string is where a path ends whether the caller
|
|
387
448
|
## meant it to or not.
|
|
@@ -607,7 +607,7 @@ func get_rect(params: Dictionary) -> Dictionary:
|
|
|
607
607
|
if node is Control:
|
|
608
608
|
var control: Control = node
|
|
609
609
|
var canvas_rect: Rect2 = control.get_global_rect()
|
|
610
|
-
|
|
610
|
+
var answer: Dictionary = {
|
|
611
611
|
"type": "rect",
|
|
612
612
|
"path": node_path,
|
|
613
613
|
"visible": control.is_visible_in_tree(),
|
|
@@ -621,6 +621,19 @@ func get_rect(params: Dictionary) -> Dictionary:
|
|
|
621
621
|
)
|
|
622
622
|
),
|
|
623
623
|
}
|
|
624
|
+
# Visible and in view read as clickable for a button under a dialog covering the screen,
|
|
625
|
+
# which a click by its words refused. Loaded here, since runtime_targets preloads this
|
|
626
|
+
# script.
|
|
627
|
+
if answer["visible"] and answer["in_view"]:
|
|
628
|
+
var own: Script = get_script()
|
|
629
|
+
var targets: GDScript = load(
|
|
630
|
+
own.resource_path.get_base_dir().path_join("runtime_targets.gd")
|
|
631
|
+
)
|
|
632
|
+
var over: Variant = targets.call("drawn_over", control)
|
|
633
|
+
if over is Node:
|
|
634
|
+
var cover: Node = over
|
|
635
|
+
answer["drawn_over"] = str(cover.get_path())
|
|
636
|
+
return answer
|
|
624
637
|
if node is Node2D:
|
|
625
638
|
var item: Node2D = node
|
|
626
639
|
var canvas_position: Vector2 = item.get_global_transform_with_canvas().origin
|
|
@@ -250,7 +250,7 @@ static func control_saying(
|
|
|
250
250
|
(
|
|
251
251
|
(
|
|
252
252
|
'no %s on screen%s says exactly "%s", so nothing was clicked: %s %s says it as '
|
|
253
|
-
+ 'part of "%s", while %s; pass index %d to press
|
|
253
|
+
+ 'part of "%s", while %s; pass index %d to press %s anyway'
|
|
254
254
|
)
|
|
255
255
|
% [
|
|
256
256
|
"button" if exact_elsewhere.size() > exact_out_of_reach.size() else "control",
|
|
@@ -261,6 +261,7 @@ static func control_saying(
|
|
|
261
261
|
_shortened(lines[index]),
|
|
262
262
|
_out_of_reach_exactly(exact_elsewhere, elsewhere_as),
|
|
263
263
|
index,
|
|
264
|
+
targets[index].get_path(),
|
|
264
265
|
]
|
|
265
266
|
),
|
|
266
267
|
}
|
|
@@ -272,7 +273,7 @@ static func control_saying(
|
|
|
272
273
|
(
|
|
273
274
|
'no control on screen%s says "%s" as a word of its own, so nothing was '
|
|
274
275
|
+ 'clicked: %s %s has it only inside a longer word, "%s"; pass index %d to '
|
|
275
|
-
+ "press
|
|
276
|
+
+ "press %s anyway"
|
|
276
277
|
)
|
|
277
278
|
% [
|
|
278
279
|
under,
|
|
@@ -281,6 +282,7 @@ static func control_saying(
|
|
|
281
282
|
targets[index].get_path(),
|
|
282
283
|
_shortened(lines[index]),
|
|
283
284
|
index,
|
|
285
|
+
targets[index].get_path(),
|
|
284
286
|
]
|
|
285
287
|
),
|
|
286
288
|
}
|
|
@@ -871,8 +873,8 @@ static func _where_it_lands(
|
|
|
871
873
|
var half: Vector2 = (drawn * Rect2(Vector2.ZERO, target.size)).size * 0.5
|
|
872
874
|
var moves: Dictionary[ScrollContainer, Vector2] = {}
|
|
873
875
|
var landed: Vector2 = point
|
|
874
|
-
var walk:
|
|
875
|
-
while walk != null
|
|
876
|
+
var walk: CanvasItem = _drawn_inside(target)
|
|
877
|
+
while walk != null:
|
|
876
878
|
var holder: ScrollContainer = walk as ScrollContainer
|
|
877
879
|
if holder != null:
|
|
878
880
|
var shown: Rect2 = (
|
|
@@ -887,7 +889,7 @@ static func _where_it_lands(
|
|
|
887
889
|
if next != landed:
|
|
888
890
|
moves[holder] = next - landed
|
|
889
891
|
landed = next
|
|
890
|
-
walk = walk
|
|
892
|
+
walk = _drawn_inside(walk)
|
|
891
893
|
return moves
|
|
892
894
|
|
|
893
895
|
|
|
@@ -919,8 +921,8 @@ static func _off_screen(target: Control, aim: Vector2) -> bool:
|
|
|
919
921
|
static func _clipped_away(
|
|
920
922
|
control: Control, point: Vector2, moves: Dictionary[ScrollContainer, Vector2]
|
|
921
923
|
) -> bool:
|
|
922
|
-
var walk:
|
|
923
|
-
while walk != null
|
|
924
|
+
var walk: CanvasItem = _drawn_inside(control)
|
|
925
|
+
while walk != null:
|
|
924
926
|
var holder: Control = walk as Control
|
|
925
927
|
if (
|
|
926
928
|
holder != null
|
|
@@ -928,10 +930,21 @@ static func _clipped_away(
|
|
|
928
930
|
and not _holds(holder, point - _moved_by(holder, moves))
|
|
929
931
|
):
|
|
930
932
|
return true
|
|
931
|
-
walk = walk
|
|
933
|
+
walk = _drawn_inside(walk)
|
|
932
934
|
return false
|
|
933
935
|
|
|
934
936
|
|
|
937
|
+
## The canvas item [param item] is drawn inside, which is what can clip it and what moves it, or
|
|
938
|
+
## null where its drawing starts afresh: under a CanvasLayer, a viewport or a node that is not a
|
|
939
|
+
## canvas item, or when it is top_level. A clipping Control above a CanvasLayer clips nothing on the
|
|
940
|
+
## layer, and judged against one, every button of a reader drawn on a layer under a clipping desk
|
|
941
|
+
## was refused as off the screen where it sat above the desk's top edge.
|
|
942
|
+
static func _drawn_inside(item: CanvasItem) -> CanvasItem:
|
|
943
|
+
if item.top_level:
|
|
944
|
+
return null
|
|
945
|
+
return item.get_parent() as CanvasItem
|
|
946
|
+
|
|
947
|
+
|
|
935
948
|
## The canvas layer [param control] is drawn on, 0 for the viewport's own canvas.
|
|
936
949
|
static func _layer_of(control: Control) -> int:
|
|
937
950
|
var layer: CanvasLayer = control.get_canvas_layer_node()
|
|
@@ -8,6 +8,7 @@ const GdhPaths = preload("runtime_paths.gd")
|
|
|
8
8
|
const GdhQueries = preload("runtime_queries.gd")
|
|
9
9
|
const GdhRead = preload("reading.gd")
|
|
10
10
|
const GdhSays = preload("runtime_says.gd")
|
|
11
|
+
const GdhShapes = preload("value_shapes.gd")
|
|
11
12
|
const GdhValues = preload("runtime_values.gd")
|
|
12
13
|
## The longest one wait may last, whatever the request says: past this the server has long
|
|
13
14
|
## since given up on the reply.
|
|
@@ -490,7 +491,7 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
490
491
|
"%s.%s is declared to hold an object and the value to wait for is %s, which it can"
|
|
491
492
|
+ " never hold: wait for null, or for the object's own value on a property of it."
|
|
492
493
|
)
|
|
493
|
-
% [node_path, property,
|
|
494
|
+
% [node_path, property, GdhShapes.kind(wanted)]
|
|
494
495
|
)
|
|
495
496
|
if not refused.is_empty():
|
|
496
497
|
return { "type": "error", "message": refused }
|
|
@@ -679,7 +680,7 @@ static func _not_comparable(
|
|
|
679
680
|
current: Variant, wanted: Variant, node_path: String, property: String
|
|
680
681
|
) -> String:
|
|
681
682
|
var types: Array[String] = [
|
|
682
|
-
node_path, property, type_string(typeof(current)),
|
|
683
|
+
node_path, property, type_string(typeof(current)), GdhShapes.kind(wanted),
|
|
683
684
|
]
|
|
684
685
|
# A value that cannot become what the property holds is said as that, which names the remedy,
|
|
685
686
|
# before the comparison it would also fail; an object on either side is said as the comparison.
|
|
@@ -234,6 +234,15 @@ static func packed_from(items: Array, type: int, fit: Callable) -> Variant:
|
|
|
234
234
|
return type_convert(built, type)
|
|
235
235
|
|
|
236
236
|
|
|
237
|
+
## What [param value] is, as a refusal names it. A number arrives from JSON as a float whether or
|
|
238
|
+
## not it has a fraction, so [1, 2] given for a list of words was called a list of floats, and 5
|
|
239
|
+
## given for a Vector2 was called a float.
|
|
240
|
+
static func kind(value: Variant) -> String:
|
|
241
|
+
if value is int or value is float:
|
|
242
|
+
return "a number"
|
|
243
|
+
return type_string(typeof(value))
|
|
244
|
+
|
|
245
|
+
|
|
237
246
|
## The sentences a refusal of [param value] where a [param type] goes ends with: which of its keys
|
|
238
247
|
## kept it from being read as one, and how one is written. Empty for a type not built from fields.
|
|
239
248
|
##
|