gdharness 1.3.47 → 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 +589 -229
- package/build/godot/addons/gdharness_editor/animation_reads.gd +20 -0
- package/build/godot/addons/gdharness_editor/declared_values.gd +37 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +6 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +8 -11
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +19 -0
- 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_hover.gd +9 -7
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +72 -11
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +21 -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/runtime_words.gd +1 -1
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +9 -0
- package/build/godot/operations/animation_reads.gd +20 -0
- package/build/godot/operations/declared_values.gd +37 -0
- package/build/godot/operations/dependencies.gd +65 -9
- package/build/godot/operations/file_reads.gd +6 -0
- package/build/godot/operations/import_pipeline.gd +10 -2
- package/build/godot/operations/project_config.gd +35 -0
- package/build/godot/operations/property_values.gd +8 -11
- package/build/godot/operations/resource_files.gd +1 -2
- package/build/godot/operations/value_shapes.gd +9 -0
- package/build/godot/tests/listed.gd +25 -0
- package/build/keeper.js +1312 -1026
- package/package.json +1 -1
|
@@ -16,6 +16,22 @@ const UPDATE_MODE_NAMES: Dictionary[int, String] = {
|
|
|
16
16
|
Animation.UPDATE_DISCRETE: "discrete",
|
|
17
17
|
Animation.UPDATE_CAPTURE: "capture",
|
|
18
18
|
}
|
|
19
|
+
const INTERPOLATION_NAMES: Dictionary[int, String] = {
|
|
20
|
+
Animation.INTERPOLATION_NEAREST: "nearest",
|
|
21
|
+
Animation.INTERPOLATION_LINEAR: "linear",
|
|
22
|
+
Animation.INTERPOLATION_CUBIC: "cubic",
|
|
23
|
+
Animation.INTERPOLATION_LINEAR_ANGLE: "linear_angle",
|
|
24
|
+
Animation.INTERPOLATION_CUBIC_ANGLE: "cubic_angle",
|
|
25
|
+
}
|
|
26
|
+
## The track types whose keys are blended between, so how they are and whether a looping animation
|
|
27
|
+
## blends its last key into its first say how the track moves.
|
|
28
|
+
const INTERPOLATED_TYPES: Array[int] = [
|
|
29
|
+
Animation.TYPE_VALUE,
|
|
30
|
+
Animation.TYPE_POSITION_3D,
|
|
31
|
+
Animation.TYPE_ROTATION_3D,
|
|
32
|
+
Animation.TYPE_SCALE_3D,
|
|
33
|
+
Animation.TYPE_BLEND_SHAPE,
|
|
34
|
+
]
|
|
19
35
|
const TRACK_TYPE_NAMES: Dictionary[int, String] = {
|
|
20
36
|
Animation.TYPE_VALUE: "property",
|
|
21
37
|
Animation.TYPE_POSITION_3D: "position_3d",
|
|
@@ -174,6 +190,10 @@ static func _track(anim: Animation, track: int, values: GdhSerialisation) -> Dic
|
|
|
174
190
|
if kind == Animation.TYPE_VALUE:
|
|
175
191
|
var mode: int = anim.value_track_get_update_mode(track)
|
|
176
192
|
shaped["update"] = UPDATE_MODE_NAMES.get(mode, str(mode))
|
|
193
|
+
if kind in INTERPOLATED_TYPES:
|
|
194
|
+
var interpolation: int = anim.track_get_interpolation_type(track)
|
|
195
|
+
shaped["interpolation"] = INTERPOLATION_NAMES.get(interpolation, str(interpolation))
|
|
196
|
+
shaped["loopWrap"] = anim.track_get_interpolation_loop_wrap(track)
|
|
177
197
|
var keyframes: Array[Dictionary] = []
|
|
178
198
|
var methods: Dictionary = {}
|
|
179
199
|
for key: int in anim.track_get_key_count(track):
|
|
@@ -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():
|
|
@@ -356,6 +356,12 @@ static func _entry(
|
|
|
356
356
|
var data: Dictionary = {
|
|
357
357
|
"name": node["name"], "type": node["type"], "path": path, "children": children,
|
|
358
358
|
}
|
|
359
|
+
var given_values: Dictionary = node["properties"]
|
|
360
|
+
var attached: Variant = given_values.get("script")
|
|
361
|
+
if attached is Script:
|
|
362
|
+
var script: Script = attached
|
|
363
|
+
if not str(script.get_global_name()).is_empty():
|
|
364
|
+
data["scriptClass"] = str(script.get_global_name())
|
|
359
365
|
# The root's file is the scene being read, so only another node's names a second scene.
|
|
360
366
|
if path != "." and node.has("instanceOf"):
|
|
361
367
|
data["instanceOf"] = node["instanceOf"]
|
|
@@ -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
|
##
|
|
@@ -109,6 +109,9 @@ func add_animation_track(args: Dictionary) -> Dictionary:
|
|
|
109
109
|
if anim.track_get_type(track_idx) == Animation.TYPE_VALUE:
|
|
110
110
|
var mode: int = anim.value_track_get_update_mode(track_idx)
|
|
111
111
|
answer["update"] = GdhAnimationReads.UPDATE_MODE_NAMES.get(mode, str(mode))
|
|
112
|
+
var blended: int = anim.track_get_interpolation_type(track_idx)
|
|
113
|
+
answer["interpolation"] = GdhAnimationReads.INTERPOLATION_NAMES.get(blended, str(blended))
|
|
114
|
+
answer["loopWrap"] = anim.track_get_interpolation_loop_wrap(track_idx)
|
|
112
115
|
if built[3]:
|
|
113
116
|
answer["existingTrack"] = true
|
|
114
117
|
var replaced: Array = built[5]
|
|
@@ -165,6 +168,7 @@ func _add_track(
|
|
|
165
168
|
var kind: Animation.TrackType = Animation.TYPE_VALUE
|
|
166
169
|
var path: NodePath = NodePath(target_path)
|
|
167
170
|
var update: int = -1
|
|
171
|
+
var interpolation: int = -1
|
|
168
172
|
if track_type == "property":
|
|
169
173
|
var prop_name: String = str(track.get("property", ""))
|
|
170
174
|
if prop_name.is_empty():
|
|
@@ -175,6 +179,11 @@ func _add_track(
|
|
|
175
179
|
)
|
|
176
180
|
if named != null:
|
|
177
181
|
update = GdhRead.as_int(named)
|
|
182
|
+
var blended: Variant = GdhAnimationReads.INTERPOLATION_NAMES.find_key(
|
|
183
|
+
str(track.get("interpolation", ""))
|
|
184
|
+
)
|
|
185
|
+
if blended != null:
|
|
186
|
+
interpolation = GdhRead.as_int(blended)
|
|
178
187
|
var values: Array = _key_values(target, prop_name, keyframes)
|
|
179
188
|
if not values[0]:
|
|
180
189
|
return _not_added(str(values[1]))
|
|
@@ -196,6 +205,12 @@ func _add_track(
|
|
|
196
205
|
misfit = _key_misfit(target, method_name, arguments, index)
|
|
197
206
|
method_found = target.has_method(method_name)
|
|
198
207
|
kind = Animation.TYPE_METHOD
|
|
208
|
+
# A method key is called at its time and never blended, so neither has anything to set.
|
|
209
|
+
for blending: String in ["interpolation", "loopWrap"]:
|
|
210
|
+
if track.has(blending):
|
|
211
|
+
return _not_added(
|
|
212
|
+
"track.%s is for property tracks; a method key is not blended." % blending
|
|
213
|
+
)
|
|
199
214
|
else:
|
|
200
215
|
return _not_added(
|
|
201
216
|
(
|
|
@@ -250,6 +265,10 @@ func _add_track(
|
|
|
250
265
|
_as_the_editor_makes(anim, track_idx, player, target)
|
|
251
266
|
if update >= 0:
|
|
252
267
|
anim.value_track_set_update_mode(track_idx, update as Animation.UpdateMode)
|
|
268
|
+
if interpolation >= 0:
|
|
269
|
+
anim.track_set_interpolation_type(track_idx, interpolation as Animation.InterpolationType)
|
|
270
|
+
if track.has("loopWrap"):
|
|
271
|
+
anim.track_set_interpolation_loop_wrap(track_idx, GdhRead.as_bool(track["loopWrap"]))
|
|
253
272
|
for index: int in keyframes.size():
|
|
254
273
|
var keyframe: Dictionary = keyframes[index]
|
|
255
274
|
if anim.track_insert_key(track_idx, GdhRead.as_float(keyframe["time"]), keys[index]) < 0:
|
|
@@ -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
|
),
|
|
@@ -160,7 +160,7 @@ func _hover(viewport: Viewport, point: Vector2) -> void:
|
|
|
160
160
|
var inside: Vector2 = container.get_global_transform_with_canvas().affine_inverse() * point
|
|
161
161
|
if container.stretch:
|
|
162
162
|
inside /= container.stretch_shrink
|
|
163
|
-
for child: Node in container.get_children():
|
|
163
|
+
for child: Node in container.get_children(true):
|
|
164
164
|
var shown: SubViewport = child as SubViewport
|
|
165
165
|
if shown != null and not shown.gui_disable_input:
|
|
166
166
|
_hover(shown, shown.get_final_transform().affine_inverse() * inside)
|
|
@@ -179,7 +179,7 @@ func _drop(viewport: Viewport, until: int) -> void:
|
|
|
179
179
|
var over: Control = instance_from_id(top) as Control
|
|
180
180
|
var container: SubViewportContainer = over as SubViewportContainer
|
|
181
181
|
if container != null:
|
|
182
|
-
for child: Node in container.get_children():
|
|
182
|
+
for child: Node in container.get_children(true):
|
|
183
183
|
var shown: SubViewport = child as SubViewport
|
|
184
184
|
if shown != null:
|
|
185
185
|
_drop(shown, 0)
|
|
@@ -264,7 +264,7 @@ func _pressed_at(viewport: Viewport, point: Vector2) -> Node:
|
|
|
264
264
|
var inside: Vector2 = container.get_global_transform_with_canvas().affine_inverse() * point
|
|
265
265
|
if container.stretch:
|
|
266
266
|
inside /= container.stretch_shrink
|
|
267
|
-
for child: Node in container.get_children():
|
|
267
|
+
for child: Node in container.get_children(true):
|
|
268
268
|
var shown: SubViewport = child as SubViewport
|
|
269
269
|
if shown != null and not shown.gui_disable_input:
|
|
270
270
|
var within: Node = _pressed_at(
|
|
@@ -277,9 +277,11 @@ func _pressed_at(viewport: Viewport, point: Vector2) -> Node:
|
|
|
277
277
|
|
|
278
278
|
## The controls [param node] holds that the engine keeps as roots of [param node]'s viewport: drawn
|
|
279
279
|
## on their own, or with no control above them. Not into another viewport, a window or a
|
|
280
|
-
## SubViewport, whose controls are that one's.
|
|
280
|
+
## SubViewport, whose controls are that one's. Internal children count, as they do for the engine,
|
|
281
|
+
## though get_children leaves them out unless asked: a layer added with INTERNAL_MODE_BACK was
|
|
282
|
+
## passed over, and the press predicted for the control under it.
|
|
281
283
|
static func _gather_roots(node: Node, roots: Array[Control]) -> void:
|
|
282
|
-
for child: Node in node.get_children():
|
|
284
|
+
for child: Node in node.get_children(true):
|
|
283
285
|
if child is Viewport:
|
|
284
286
|
continue
|
|
285
287
|
var control: Control = child as Control
|
|
@@ -331,8 +333,8 @@ func _at(item: CanvasItem, point: Vector2, holder: Transform2D) -> Control:
|
|
|
331
333
|
var control: Control = item as Control
|
|
332
334
|
var local: Vector2 = matrix.affine_inverse() * point
|
|
333
335
|
if control == null or not control.is_clipping_contents() or _has_point(control, local):
|
|
334
|
-
for index: int in range(item.get_child_count() - 1, -1, -1):
|
|
335
|
-
var child: CanvasItem = item.get_child(index) as CanvasItem
|
|
336
|
+
for index: int in range(item.get_child_count(true) - 1, -1, -1):
|
|
337
|
+
var child: CanvasItem = item.get_child(index, true) as CanvasItem
|
|
336
338
|
if child == null or child.top_level:
|
|
337
339
|
continue
|
|
338
340
|
var found: Control = _at(child, point, matrix)
|
|
@@ -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.
|