gdharness 1.3.8 → 1.3.9
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 +137 -66
- package/build/godot/addons/gdharness_editor/file_reads.gd +13 -0
- package/build/godot/addons/gdharness_editor/reading.gd +61 -5
- package/build/godot/addons/gdharness_editor/scene_file.gd +41 -35
- package/build/godot/addons/gdharness_editor/serialisation.gd +8 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -2
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +244 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +105 -157
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +25 -1
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -1
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +72 -3
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +7 -1
- package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
- package/build/godot/addons/gdharness_runtime/reading.gd +61 -5
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +3 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +27 -31
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +11 -5
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +2 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +26 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +105 -6
- package/build/godot/addons/gdharness_runtime/serialisation.gd +8 -7
- package/build/godot/operations/editor_tools.gd +8 -0
- package/build/godot/operations/file_reads.gd +13 -0
- package/build/godot/operations/gdscript_analysis.gd +14 -0
- package/build/godot/operations/gdscript_authoring.gd +1 -1
- package/build/godot/operations/godot_operations.gd +2 -0
- package/build/godot/operations/input_actions.gd +22 -7
- package/build/godot/operations/project_config.gd +31 -0
- package/build/godot/operations/reading.gd +61 -5
- package/build/godot/operations/serialisation.gd +8 -7
- package/build/keeper.js +43 -13
- package/package.json +1 -1
|
@@ -172,6 +172,11 @@ func _cut_asked(params: Dictionary, in_window: bool) -> Dictionary:
|
|
|
172
172
|
return _refused(shape % JSON.stringify(region_given))
|
|
173
173
|
asked["region"] = region
|
|
174
174
|
if not node_path.is_empty():
|
|
175
|
+
# Before any refusal of the window: a headless game refused a path that named nothing for
|
|
176
|
+
# having no window, and the wrong path was found only after a restart with one.
|
|
177
|
+
var standing: Dictionary = GdhValues.node_at(_host.get_tree().root, node_path)
|
|
178
|
+
if standing.has("message"):
|
|
179
|
+
return standing
|
|
175
180
|
asked["node_path"] = node_path
|
|
176
181
|
return asked
|
|
177
182
|
|
|
@@ -493,6 +493,9 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
493
493
|
answer["awaited"] = true
|
|
494
494
|
_keep_reported(answer, "errors", caught.raised)
|
|
495
495
|
_keep_reported(answer, "warnings", caught.warned)
|
|
496
|
+
# Main.explode, ended at a null it called into, answered result 0 as if it had counted something.
|
|
497
|
+
if caught.ended_at.has(named):
|
|
498
|
+
answer["ended_by_error_at"] = caught.ended_at[named]
|
|
496
499
|
var asked: Variant = params.get("properties")
|
|
497
500
|
if _reads_the_result(asked):
|
|
498
501
|
answer.merge(_read_off_result(result, asked))
|
|
@@ -21,15 +21,6 @@ const GdhMouse = preload("runtime_mouse.gd")
|
|
|
21
21
|
## everything that process starts. Kept in step with `DESKTOP_VARIABLE` in src/desktop.ts.
|
|
22
22
|
const DESKTOP_VARIABLE: String = "GDHARNESS_DESKTOP"
|
|
23
23
|
|
|
24
|
-
## Node.ProcessMode as its names, for a refusal to say which one is in force.
|
|
25
|
-
const PROCESS_MODES: Dictionary[int, String] = {
|
|
26
|
-
Node.PROCESS_MODE_INHERIT: "INHERIT",
|
|
27
|
-
Node.PROCESS_MODE_PAUSABLE: "PAUSABLE",
|
|
28
|
-
Node.PROCESS_MODE_WHEN_PAUSED: "WHEN_PAUSED",
|
|
29
|
-
Node.PROCESS_MODE_ALWAYS: "ALWAYS",
|
|
30
|
-
Node.PROCESS_MODE_DISABLED: "DISABLED",
|
|
31
|
-
}
|
|
32
|
-
|
|
33
24
|
## Whether this run has had the pointer note in full; the game process is the run.
|
|
34
25
|
static var _pointer_explained: bool = false
|
|
35
26
|
|
|
@@ -151,7 +142,7 @@ func inject_action(params: Dictionary) -> Dictionary:
|
|
|
151
142
|
return {"type": "error", "message": "Action name required"}
|
|
152
143
|
|
|
153
144
|
if not InputMap.has_action(action):
|
|
154
|
-
return {"type": "error", "message":
|
|
145
|
+
return {"type": "error", "message": GdhValues.no_action(action)}
|
|
155
146
|
var holding: Variant = GdhValues.held_for(params)
|
|
156
147
|
if holding is String:
|
|
157
148
|
return {"type": "error", "message": holding}
|
|
@@ -159,6 +150,9 @@ func inject_action(params: Dictionary) -> Dictionary:
|
|
|
159
150
|
|
|
160
151
|
var focused: String = _focus_owner()
|
|
161
152
|
_say_action(action, whole or held, strength)
|
|
153
|
+
var shortcuts: Dictionary = {"shortcuts": [], "shortcut_pressed": null}
|
|
154
|
+
if whole or held:
|
|
155
|
+
shortcuts = await GdhValues.press_shortcuts(_host.get_tree().root, action)
|
|
162
156
|
var lasted: Variant = null
|
|
163
157
|
if whole:
|
|
164
158
|
# A frame between the halves at least, as a click has: a listener that acts on the press and
|
|
@@ -172,6 +166,8 @@ func inject_action(params: Dictionary) -> Dictionary:
|
|
|
172
166
|
"type": "input_injected",
|
|
173
167
|
"input_type": "action",
|
|
174
168
|
"action": action,
|
|
169
|
+
"shortcuts": shortcuts["shortcuts"],
|
|
170
|
+
"shortcut_pressed": shortcuts["shortcut_pressed"],
|
|
175
171
|
"pressed": held and not whole,
|
|
176
172
|
"whole": whole,
|
|
177
173
|
"held_frames": GdhValues.held_answer(whole, frames),
|
|
@@ -272,6 +268,7 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
272
268
|
"ctrl": event.ctrl_pressed,
|
|
273
269
|
"alt": event.alt_pressed,
|
|
274
270
|
"unicode": event.unicode,
|
|
271
|
+
"actions": GdhValues.actions_of(event),
|
|
275
272
|
"pressed": held and not whole,
|
|
276
273
|
"whole": whole,
|
|
277
274
|
},
|
|
@@ -317,7 +314,8 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
317
314
|
# its drag from the press, which is where a real pointer would be.
|
|
318
315
|
var _moved: Vector2 = _arrive(position)
|
|
319
316
|
var supplied: bool = _follow_in_the_window(position)
|
|
320
|
-
|
|
317
|
+
var press: InputEventMouseButton = _button(position, button, whole or held, double)
|
|
318
|
+
Input.parse_input_event(press)
|
|
321
319
|
var lasted: Variant = null
|
|
322
320
|
if notch:
|
|
323
321
|
Input.parse_input_event(_button(position, button, false, false))
|
|
@@ -326,7 +324,12 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
326
324
|
# press and one that acts on the release each get a frame to do it in.
|
|
327
325
|
lasted = await GdhValues.hold(_host.get_tree(), frames)
|
|
328
326
|
Input.parse_input_event(_button(position, button, false, false))
|
|
329
|
-
|
|
327
|
+
# Input hands the events on at the next frame, so the control under the pointer is read after it.
|
|
328
|
+
await _host.get_tree().process_frame
|
|
329
|
+
var hovered: Control = _hover.hovered_in(_host.get_viewport())
|
|
330
|
+
var hovered_path: Variant = null
|
|
331
|
+
if hovered != null:
|
|
332
|
+
hovered_path = str(hovered.get_path())
|
|
330
333
|
|
|
331
334
|
var at: Array[Vector2] = [position]
|
|
332
335
|
var said: Dictionary = _pointer_noted(
|
|
@@ -338,6 +341,8 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
338
341
|
"held_for": lasted,
|
|
339
342
|
"position": [position.x, position.y],
|
|
340
343
|
"button": button,
|
|
344
|
+
"hovered": hovered_path,
|
|
345
|
+
"actions": GdhValues.actions_of(press),
|
|
341
346
|
"pressed": held and not whole,
|
|
342
347
|
"whole": whole,
|
|
343
348
|
"double": double
|
|
@@ -653,7 +658,8 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
653
658
|
var was_pressed: Variant = null
|
|
654
659
|
if toggle != null and toggle.toggle_mode:
|
|
655
660
|
was_pressed = toggle.button_pressed
|
|
656
|
-
|
|
661
|
+
var press: InputEventMouseButton = _button(position, button, true, double)
|
|
662
|
+
_press_button(viewport, press)
|
|
657
663
|
await _host.get_tree().process_frame
|
|
658
664
|
_press_button(viewport, _button(position, button, false, false))
|
|
659
665
|
# The release is what a button acts on, and a queue_free it causes lands at the end of
|
|
@@ -675,9 +681,16 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
675
681
|
"double": double,
|
|
676
682
|
"hovered": hovered_path,
|
|
677
683
|
"landed": landed,
|
|
684
|
+
"actions": GdhValues.actions_of(press),
|
|
678
685
|
"control_afterwards": afterwards,
|
|
679
686
|
"scrolled_into_view": scrolled,
|
|
680
687
|
}
|
|
688
|
+
# A Label and a TextureRect ignore the pointer unless told otherwise, so the click goes to what is
|
|
689
|
+
# under them, and landed false with that control as hovered said nothing of why.
|
|
690
|
+
if not landed and control.mouse_filter == Control.MOUSE_FILTER_IGNORE:
|
|
691
|
+
answer["why_not_landed"] = (
|
|
692
|
+
"%s's mouse_filter is ignore, so the pointer passes through it to what is under it" % node_path
|
|
693
|
+
)
|
|
681
694
|
if found != null:
|
|
682
695
|
answer["found"] = found
|
|
683
696
|
if was_pressed != null:
|
|
@@ -718,7 +731,7 @@ func _takes_no_click(control: Control, node_path: String) -> String:
|
|
|
718
731
|
% [node_path, on]
|
|
719
732
|
)
|
|
720
733
|
if not control.can_process():
|
|
721
|
-
var why: String =
|
|
734
|
+
var why: String = GdhTargets.why_it_does_not_process(control, node_path)
|
|
722
735
|
var over: Node = GdhTargets.drawn_over(control)
|
|
723
736
|
var cover: String = "" if over == null else "; %s is drawn over it" % over.get_path()
|
|
724
737
|
if _host.get_tree().paused:
|
|
@@ -730,23 +743,6 @@ func _takes_no_click(control: Control, node_path: String) -> String:
|
|
|
730
743
|
return ""
|
|
731
744
|
|
|
732
745
|
|
|
733
|
-
## Which node's process_mode keeps [param control] from processing, and what it is. A node set to
|
|
734
|
-
## inherit takes its parent's, so the one deciding can be any node above it: the game turning a
|
|
735
|
-
## whole screen off while a menu shows over it reads, on the button, as a button turned off.
|
|
736
|
-
static func _why_it_does_not_process(control: Control, node_path: String) -> String:
|
|
737
|
-
var deciding: Node = control
|
|
738
|
-
while deciding.process_mode == Node.PROCESS_MODE_INHERIT and deciding.get_parent() != null:
|
|
739
|
-
deciding = deciding.get_parent()
|
|
740
|
-
# The root inherits from nothing, which processes as pausable.
|
|
741
|
-
var value: int = deciding.process_mode
|
|
742
|
-
if value == Node.PROCESS_MODE_INHERIT:
|
|
743
|
-
value = Node.PROCESS_MODE_PAUSABLE
|
|
744
|
-
var mode: String = PROCESS_MODES.get(value, str(value))
|
|
745
|
-
if deciding == control:
|
|
746
|
-
return "its process_mode is %s" % mode
|
|
747
|
-
return "%s inherits process_mode %s from %s" % [node_path, mode, deciding.get_path()]
|
|
748
|
-
|
|
749
|
-
|
|
750
746
|
## Why a click at [param control]'s centre would reach nothing, or "" when it would reach it: the
|
|
751
747
|
## centre outside the viewport it is drawn in, at [param local]; outside the viewport showing that
|
|
752
748
|
## one on the screen, which [param reached] holds with the centre there; or clipped away by a
|
|
@@ -405,10 +405,13 @@ static func _arity(object: Object, method: String) -> Dictionary:
|
|
|
405
405
|
##
|
|
406
406
|
## A path is read, and a wait reads it every frame, so a call in one has to be a read. The engine
|
|
407
407
|
## marks its own reading methods const and `queue_free()`, `free()` and `set_name()` not, measured on
|
|
408
|
-
## 4.7.2
|
|
409
|
-
##
|
|
410
|
-
##
|
|
411
|
-
##
|
|
408
|
+
## 4.7.2, though it leaves some reads unmarked as well, `Engine.get_frames_drawn()` and
|
|
409
|
+
## `get_time_scale()` among them; so the refusal says the mark is missing, not that the method
|
|
410
|
+
## changes anything, and points at runtime_invoke call, which calls it. A game's methods carry no
|
|
411
|
+
## such mark, so one declared `-> void` is taken at its word, as doing something and answering
|
|
412
|
+
## nothing, and one left untyped is trusted. Without this a find with property "queue_free()" freed
|
|
413
|
+
## every node it matched, and a wait on "advance_day()" called it once a frame and then reported the
|
|
414
|
+
## state it had driven the game to.
|
|
412
415
|
##
|
|
413
416
|
## Given [param given] arguments, a call is the engine's and marked as reading, or it is refused. A
|
|
414
417
|
## game's method taking arguments is trusted on nothing at all: one answering a value can as well
|
|
@@ -450,7 +453,10 @@ static func call_refused(object: Object, method: String, called_as: String, give
|
|
|
450
453
|
)
|
|
451
454
|
return ""
|
|
452
455
|
if _native_flags(object, method) & METHOD_FLAG_CONST == 0:
|
|
453
|
-
return
|
|
456
|
+
return (
|
|
457
|
+
"%s.%s is not one the engine marks as only reading%s; runtime_invoke call calls it"
|
|
458
|
+
% [called_as, method, READS_ONLY]
|
|
459
|
+
)
|
|
454
460
|
return ""
|
|
455
461
|
|
|
456
462
|
|
|
@@ -723,6 +723,8 @@ func get_metrics(params: Dictionary) -> Dictionary:
|
|
|
723
723
|
"frame_time": frame_time,
|
|
724
724
|
"frame_time_max": frame_time_max,
|
|
725
725
|
"frames_timed": timed,
|
|
726
|
+
"process_frames": Engine.get_process_frames(),
|
|
727
|
+
"physics_frames": Engine.get_physics_frames(),
|
|
726
728
|
"physics_steps_per_second": physics_steps_per_second,
|
|
727
729
|
"physics_ticks_per_second": Engine.physics_ticks_per_second,
|
|
728
730
|
"max_physics_steps_per_frame": Engine.max_physics_steps_per_frame,
|
|
@@ -13,6 +13,15 @@ const GdhWords = preload("runtime_words.gd")
|
|
|
13
13
|
## How many of the controls saying the same words a refusal names, with the index for each.
|
|
14
14
|
const LISTED_CANDIDATES: int = 10
|
|
15
15
|
|
|
16
|
+
## Node.ProcessMode as its names, for a refusal to say which one is in force.
|
|
17
|
+
const PROCESS_MODES: Dictionary[int, String] = {
|
|
18
|
+
Node.PROCESS_MODE_INHERIT: "INHERIT",
|
|
19
|
+
Node.PROCESS_MODE_PAUSABLE: "PAUSABLE",
|
|
20
|
+
Node.PROCESS_MODE_WHEN_PAUSED: "WHEN_PAUSED",
|
|
21
|
+
Node.PROCESS_MODE_ALWAYS: "ALWAYS",
|
|
22
|
+
Node.PROCESS_MODE_DISABLED: "DISABLED",
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
## What a game started without a window gets whatever the project settings say, and the usual
|
|
17
26
|
## reason a control is out of reach. Only worth telling somebody when it is what they have.
|
|
18
27
|
const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
|
|
@@ -954,3 +963,20 @@ static func _out_of_reach_exactly(nodes: Array[Node], why: Array[String]) -> Str
|
|
|
954
963
|
if nodes.size() == 1:
|
|
955
964
|
return "1 control says exactly that and cannot be clicked: %s" % first
|
|
956
965
|
return "%d controls say exactly that and cannot be clicked, such as %s" % [nodes.size(), first]
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
## Which node's process_mode keeps [param control] from processing, and what it is. A node set to
|
|
969
|
+
## inherit takes its parent's, so the one deciding can be any node above it: the game turning a
|
|
970
|
+
## whole screen off while a menu shows over it reads, on the button, as a button turned off.
|
|
971
|
+
static func why_it_does_not_process(control: Control, node_path: String) -> String:
|
|
972
|
+
var deciding: Node = control
|
|
973
|
+
while deciding.process_mode == Node.PROCESS_MODE_INHERIT and deciding.get_parent() != null:
|
|
974
|
+
deciding = deciding.get_parent()
|
|
975
|
+
# The root inherits from nothing, which processes as pausable.
|
|
976
|
+
var value: int = deciding.process_mode
|
|
977
|
+
if value == Node.PROCESS_MODE_INHERIT:
|
|
978
|
+
value = Node.PROCESS_MODE_PAUSABLE
|
|
979
|
+
var mode: String = PROCESS_MODES.get(value, str(value))
|
|
980
|
+
if deciding == control:
|
|
981
|
+
return "its process_mode is %s" % mode
|
|
982
|
+
return "%s inherits process_mode %s from %s" % [node_path, mode, deciding.get_path()]
|
|
@@ -31,6 +31,9 @@ const SINGLETONS: Array[String] = [
|
|
|
31
31
|
## The most frames a press is held in one call, as many as a frames wait takes.
|
|
32
32
|
const MOST_HELD: int = 600
|
|
33
33
|
|
|
34
|
+
## Read by [method _key_names], once a game.
|
|
35
|
+
static var _named_keys: PackedStringArray = []
|
|
36
|
+
|
|
34
37
|
var _shared: GdhSerialisation = GdhSerialisation.new()
|
|
35
38
|
|
|
36
39
|
|
|
@@ -160,12 +163,7 @@ static func glyph_of(keycode: Key, shifted: bool) -> int:
|
|
|
160
163
|
## engine's own names, printable keys and the special ones above [constant KEY_SPECIAL], since the
|
|
161
164
|
## game takes any name OS.find_keycode_from_string does.
|
|
162
165
|
static func unknown_key(name: String) -> String:
|
|
163
|
-
var
|
|
164
|
-
for code: int in range(32, 127) + range(KEY_SPECIAL + 1, KEY_SPECIAL + 0x100):
|
|
165
|
-
var spelled: String = OS.get_keycode_string(code as Key)
|
|
166
|
-
if not spelled.is_empty() and OS.find_keycode_from_string(spelled) == code:
|
|
167
|
-
var _named: bool = names.append(spelled)
|
|
168
|
-
var close: PackedStringArray = GdhRead.nearest(name, names)
|
|
166
|
+
var close: PackedStringArray = GdhRead.nearest(name, _key_names())
|
|
169
167
|
var said: String = "No key is called %s." % name
|
|
170
168
|
if close.size() == 1:
|
|
171
169
|
said += " The nearest key name is %s." % close[0]
|
|
@@ -178,6 +176,107 @@ static func unknown_key(name: String) -> String:
|
|
|
178
176
|
)
|
|
179
177
|
|
|
180
178
|
|
|
179
|
+
## Every key name the engine takes, read once a game. The engine spells a code it has no name for as
|
|
180
|
+
## the character it would be, and past the last code point that is an error it prints: 136 of them
|
|
181
|
+
## for the special range, two seconds of console on a fast machine, more than a call's whole wait on
|
|
182
|
+
## a loaded one, and each in the game's log as an error the game never made. So they are asked with
|
|
183
|
+
## the engine's error printing off, which no other line of the main thread runs beside.
|
|
184
|
+
static func _key_names() -> PackedStringArray:
|
|
185
|
+
if not _named_keys.is_empty():
|
|
186
|
+
return _named_keys
|
|
187
|
+
var printing: bool = Engine.print_error_messages
|
|
188
|
+
Engine.print_error_messages = false
|
|
189
|
+
for code: int in range(32, 127) + range(KEY_SPECIAL + 1, KEY_SPECIAL + 0x100):
|
|
190
|
+
var spelled: String = OS.get_keycode_string(code as Key)
|
|
191
|
+
if not spelled.is_empty() and OS.find_keycode_from_string(spelled) == code:
|
|
192
|
+
var _named: bool = _named_keys.append(spelled)
|
|
193
|
+
Engine.print_error_messages = printing
|
|
194
|
+
return _named_keys
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
## The refusal for [param action], which the input map lacks, with the nearest of the project's own
|
|
198
|
+
## actions, or else all of them. The engine's ui_ ones are in every project, and offered for any name
|
|
199
|
+
## they only buried the project's ("dance" is near ui_cancel), so they are offered for a ui_ name only.
|
|
200
|
+
static func no_action(action: String) -> String:
|
|
201
|
+
var engine: PackedStringArray = []
|
|
202
|
+
var own: PackedStringArray = []
|
|
203
|
+
for named: StringName in InputMap.get_actions():
|
|
204
|
+
if str(named).begins_with("ui_"):
|
|
205
|
+
var _listed: bool = engine.append(str(named))
|
|
206
|
+
else:
|
|
207
|
+
var _owned: bool = own.append(str(named))
|
|
208
|
+
own.sort()
|
|
209
|
+
var said: String = "Action not found: %s" % action
|
|
210
|
+
var close: PackedStringArray = GdhRead.nearest(action, engine if action.begins_with("ui_") else own)
|
|
211
|
+
if not close.is_empty():
|
|
212
|
+
return "%s; the nearest %s %s." % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
213
|
+
if own.is_empty():
|
|
214
|
+
return "%s; the project defines no actions of its own, and the engine's are the ui_ ones." % said
|
|
215
|
+
return "%s; the project's own actions are %s, beside the engine's ui_ ones." % [said, ", ".join(own)]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
## The actions of the input map that [param event] stands for, as Input reads it for
|
|
219
|
+
## is_action_pressed: a key or a button bound to an action presses that action too, so a game
|
|
220
|
+
## counting the action counted the key, and the answer had said only that a key went in.
|
|
221
|
+
static func actions_of(event: InputEvent) -> Array[String]:
|
|
222
|
+
var named: Array[String] = []
|
|
223
|
+
for action: StringName in InputMap.get_actions():
|
|
224
|
+
if InputMap.event_is_action(event, action):
|
|
225
|
+
named.append(str(action))
|
|
226
|
+
named.sort()
|
|
227
|
+
return named
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
## Presses the buttons under [param root] whose Shortcut [param action] matches, as the player's key
|
|
231
|
+
## bound to it would, and answers their paths under "shortcuts" and the one that took the press
|
|
232
|
+
## under "shortcut_pressed". A Viewport hands a shortcut only keys, joypad buttons and
|
|
233
|
+
## InputEventShortcut, so an action pressed on its own never reached a Button whose Shortcut holds
|
|
234
|
+
## that action, which is how a game gives a button a binding the player can change.
|
|
235
|
+
##
|
|
236
|
+
## A key goes to the shortcuts last in the tree first and stops at the first button that takes it,
|
|
237
|
+
## while an InputEventShortcut reaches only the buttons holding that one Shortcut. So each Shortcut
|
|
238
|
+
## is sent in that order, a frame apart, until a button is pressed; a button still decides by its
|
|
239
|
+
## disabled, its visibility and its shortcut context, as it does for the key.
|
|
240
|
+
static func press_shortcuts(root: Node, action: String) -> Dictionary:
|
|
241
|
+
var pressing: InputEventAction = InputEventAction.new()
|
|
242
|
+
pressing.action = action
|
|
243
|
+
pressing.pressed = true
|
|
244
|
+
var buttons: Array[BaseButton] = []
|
|
245
|
+
for node: Node in root.find_children("*", "BaseButton", true, false):
|
|
246
|
+
var button: BaseButton = node
|
|
247
|
+
if button.shortcut != null and button.shortcut.matches_event(pressing):
|
|
248
|
+
buttons.append(button)
|
|
249
|
+
var paths: Array[String] = []
|
|
250
|
+
for button: BaseButton in buttons:
|
|
251
|
+
paths.append(str(button.get_path()))
|
|
252
|
+
buttons.reverse()
|
|
253
|
+
var taken: Array[String] = []
|
|
254
|
+
var heard: Array[Callable] = []
|
|
255
|
+
for button: BaseButton in buttons:
|
|
256
|
+
var path: String = str(button.get_path())
|
|
257
|
+
var hearing: Callable = func() -> void: taken.append(path)
|
|
258
|
+
heard.append(hearing)
|
|
259
|
+
var _listening: int = button.pressed.connect(hearing)
|
|
260
|
+
var sent: Dictionary = {}
|
|
261
|
+
for button: BaseButton in buttons:
|
|
262
|
+
if not taken.is_empty():
|
|
263
|
+
break
|
|
264
|
+
if sent.has(button.shortcut):
|
|
265
|
+
continue
|
|
266
|
+
sent[button.shortcut] = true
|
|
267
|
+
var through: InputEventShortcut = InputEventShortcut.new()
|
|
268
|
+
through.shortcut = button.shortcut
|
|
269
|
+
Input.parse_input_event(through)
|
|
270
|
+
await root.get_tree().process_frame
|
|
271
|
+
for at: int in buttons.size():
|
|
272
|
+
if is_instance_valid(buttons[at]):
|
|
273
|
+
buttons[at].pressed.disconnect(heard[at])
|
|
274
|
+
var pressed: Variant = null
|
|
275
|
+
if not taken.is_empty():
|
|
276
|
+
pressed = taken[0]
|
|
277
|
+
return {"shortcuts": paths, "shortcut_pressed": pressed}
|
|
278
|
+
|
|
279
|
+
|
|
181
280
|
## What [param path] names: a node under [param root], as [method node_at] reads it, or an engine
|
|
182
281
|
## singleton by its name. Answers the object under "object", and under "node" the node that objects
|
|
183
282
|
## named in a value are found from, which for a singleton is [param root].
|
|
@@ -142,28 +142,29 @@ func serialize_value(value: Variant) -> Variant:
|
|
|
142
142
|
return value
|
|
143
143
|
var carried: Variant = call(serialiser, value)
|
|
144
144
|
# The fixed shapes carry their components as bare floats, so an infinite one in a Vector2 is
|
|
145
|
-
# tagged here; containers reach their members through this
|
|
146
|
-
|
|
145
|
+
# tagged here, and a 32-bit one shortened; containers reach their members through this already.
|
|
146
|
+
var type: int = typeof(value)
|
|
147
|
+
return carried if type in [TYPE_ARRAY, TYPE_DICTIONARY] else _finite(carried, GdhRead.holds_singles(type))
|
|
147
148
|
|
|
148
149
|
|
|
149
150
|
# [param carried] with every non-finite float in it tagged. JSON has no spelling for infinity or
|
|
150
151
|
# NaN: the engine writes 1e99999 and null, and the server, reading 1e99999 as Infinity, writes it
|
|
151
152
|
# back out as null, so a property holding INF was answered as holding nothing.
|
|
152
|
-
func _finite(carried: Variant) -> Variant:
|
|
153
|
+
func _finite(carried: Variant, single: bool = false) -> Variant:
|
|
153
154
|
if carried is float:
|
|
154
155
|
var number: float = carried
|
|
155
|
-
return _serialize_float(number)
|
|
156
|
+
return _serialize_float(GdhRead.single(number) if single else number)
|
|
156
157
|
if carried is Dictionary:
|
|
157
158
|
var fields: Dictionary = carried
|
|
158
159
|
var tagged: Dictionary = {}
|
|
159
160
|
for key: Variant in fields:
|
|
160
|
-
tagged[key] = _finite(fields[key])
|
|
161
|
+
tagged[key] = _finite(fields[key], single)
|
|
161
162
|
return tagged
|
|
162
163
|
if carried is Array:
|
|
163
164
|
var items: Array = carried
|
|
164
165
|
var tagged_items: Array = []
|
|
165
166
|
for item: Variant in items:
|
|
166
|
-
tagged_items.append(_finite(item))
|
|
167
|
+
tagged_items.append(_finite(item, single))
|
|
167
168
|
return tagged_items
|
|
168
169
|
return carried
|
|
169
170
|
|
|
@@ -189,7 +190,7 @@ func _serialize_floats(value: Variant) -> Variant:
|
|
|
189
190
|
for each: float in numbers:
|
|
190
191
|
items.append(_serialize_float(each))
|
|
191
192
|
return items
|
|
192
|
-
return value
|
|
193
|
+
return numbers if value is PackedFloat32Array else value
|
|
193
194
|
|
|
194
195
|
|
|
195
196
|
# Rebuilds a Godot value from the shape serialize_value gave it.
|
|
@@ -7,6 +7,7 @@ extends RefCounted
|
|
|
7
7
|
## made here passes the same checks and answers in the same shape. What the engine does that an
|
|
8
8
|
## editor does not, running every script, is put back in scene_file.gd and headless_check.gd.
|
|
9
9
|
|
|
10
|
+
const GdhAnimationStates = preload("../addons/gdharness_editor/tools/animation_states.gd")
|
|
10
11
|
const GdhAnimationTools = preload("../addons/gdharness_editor/tools/animation_tools.gd")
|
|
11
12
|
const GdhResourceTools = preload("../addons/gdharness_editor/tools/resource_tools.gd")
|
|
12
13
|
const GdhSceneTools = preload("../addons/gdharness_editor/tools/scene_tools.gd")
|
|
@@ -36,11 +37,16 @@ const RESOURCE_COMMANDS: Array[String] = [
|
|
|
36
37
|
const ANIMATION_COMMANDS: Array[String] = [
|
|
37
38
|
"create_animation",
|
|
38
39
|
"add_animation_track",
|
|
40
|
+
"remove_animation_track",
|
|
39
41
|
"set_animation",
|
|
40
42
|
"rename_animation",
|
|
41
43
|
"remove_animation",
|
|
44
|
+
]
|
|
45
|
+
const ANIMATION_STATE_COMMANDS: Array[String] = [
|
|
42
46
|
"add_animation_state",
|
|
43
47
|
"connect_animation_states",
|
|
48
|
+
"remove_animation_state",
|
|
49
|
+
"disconnect_animation_states",
|
|
44
50
|
]
|
|
45
51
|
|
|
46
52
|
|
|
@@ -60,6 +66,8 @@ static func run(params: Dictionary) -> Dictionary:
|
|
|
60
66
|
tools = GdhResourceTools.new()
|
|
61
67
|
elif command in ANIMATION_COMMANDS:
|
|
62
68
|
tools = GdhAnimationTools.new()
|
|
69
|
+
elif command in ANIMATION_STATE_COMMANDS:
|
|
70
|
+
tools = GdhAnimationStates.new()
|
|
63
71
|
else:
|
|
64
72
|
return {"ok": false, "error": "No editor tool runs headless as " + command}
|
|
65
73
|
var answer: Variant = tools.call(command, arguments)
|
|
@@ -119,6 +119,9 @@ static func _laid(scene_path: String) -> Dictionary:
|
|
|
119
119
|
return {"ok": false, "error": refused}
|
|
120
120
|
if not nodes.has("."):
|
|
121
121
|
return {"ok": false, "error": "%s holds no nodes" % scene_path}
|
|
122
|
+
var base: SceneState = packed.get_state().get_base_scene_state()
|
|
123
|
+
if base != null:
|
|
124
|
+
nodes["."]["inherits"] = base.get_path()
|
|
122
125
|
return {"ok": true, "nodes": nodes}
|
|
123
126
|
|
|
124
127
|
|
|
@@ -175,6 +178,10 @@ static func _lay(
|
|
|
175
178
|
nodes[path] = _made(str(state.get_node_type(index)))
|
|
176
179
|
if ids[index] >= 0:
|
|
177
180
|
nodes[path]["uniqueId"] = ids[index]
|
|
181
|
+
# Laid at the root with no scene handed in is a scene the one being read inherits, and the
|
|
182
|
+
# deepest is laid first, so this is the file that makes the node.
|
|
183
|
+
if prefix == "." and loaded == null:
|
|
184
|
+
nodes[path]["inheritedFrom"] = state.get_path()
|
|
178
185
|
# The name is the state's even for an entry laid already, since an inherited scene names its
|
|
179
186
|
# root itself; a root instanced inside another scene is renamed by that scene once laid.
|
|
180
187
|
if not recovered:
|
|
@@ -324,6 +331,12 @@ static func _entry(
|
|
|
324
331
|
data["instanceOf"] = node["instanceOf"]
|
|
325
332
|
if node.has("editableChildren"):
|
|
326
333
|
data["editableChildren"] = node["editableChildren"]
|
|
334
|
+
# An inherited scene's file holds only what it changes, so the read says what it inherits and
|
|
335
|
+
# which nodes come from there, as the editor greys them.
|
|
336
|
+
if node.has("inherits"):
|
|
337
|
+
data["inherits"] = node["inherits"]
|
|
338
|
+
elif node.has("inheritedFrom"):
|
|
339
|
+
data["inheritedFrom"] = node["inheritedFrom"]
|
|
327
340
|
var groups: Array[String] = node["groups"]
|
|
328
341
|
if not groups.is_empty():
|
|
329
342
|
var sorted: Array[String] = groups.duplicate()
|
|
@@ -76,6 +76,9 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
76
76
|
if not full_script_path.begins_with("res://"):
|
|
77
77
|
full_script_path = "res://" + full_script_path
|
|
78
78
|
|
|
79
|
+
# Read as GDScript, a scene came back as a script extending RefCounted with nothing in it.
|
|
80
|
+
if full_script_path.get_extension().to_lower() != "gd":
|
|
81
|
+
return _log.failure(_not_a_script(full_script_path))
|
|
79
82
|
if not FileAccess.file_exists(full_script_path):
|
|
80
83
|
return _log.failure(
|
|
81
84
|
GdhFileWalk.new().with_nearest_files(
|
|
@@ -334,6 +337,17 @@ func _inner_class(
|
|
|
334
337
|
return body
|
|
335
338
|
|
|
336
339
|
|
|
340
|
+
## The refusal for [param path], which is not a .gd file, with the tool that reads what it is.
|
|
341
|
+
static func _not_a_script(path: String) -> String:
|
|
342
|
+
var said: String = "%s is not a GDScript file, and script_info reads .gd files only" % path
|
|
343
|
+
match path.get_extension().to_lower():
|
|
344
|
+
"tscn", "scn":
|
|
345
|
+
return said + ": scene_tree reads a scene's nodes, and with properties the script each carries."
|
|
346
|
+
"tres", "res":
|
|
347
|
+
return said + ": resource_edit read reads a resource."
|
|
348
|
+
return said + "."
|
|
349
|
+
|
|
350
|
+
|
|
337
351
|
static func _indentation(line: String) -> String:
|
|
338
352
|
return line.substr(0, line.length() - line.strip_edges(true, false).length())
|
|
339
353
|
|
|
@@ -259,7 +259,7 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
259
259
|
)
|
|
260
260
|
return _log.failure(
|
|
261
261
|
(
|
|
262
|
-
"Nothing was changed: %s parses
|
|
262
|
+
"Nothing was changed: %s parses now, and would not with these additions: %s"
|
|
263
263
|
% [script_path, _reasons(caught.raised, full_script_path)]
|
|
264
264
|
)
|
|
265
265
|
)
|
|
@@ -247,6 +247,8 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
|
|
|
247
247
|
payload = GdhProjectConfig.new(_log).get_project_setting(params)
|
|
248
248
|
"set_project_setting":
|
|
249
249
|
payload = GdhProjectConfig.new(_log).set_project_setting(params)
|
|
250
|
+
"unset_project_setting":
|
|
251
|
+
payload = GdhProjectConfig.new(_log).unset_project_setting(params)
|
|
250
252
|
"add_autoload":
|
|
251
253
|
payload = GdhProjectConfig.new(_log).add_autoload(params)
|
|
252
254
|
"remove_autoload":
|
|
@@ -3,6 +3,8 @@ extends RefCounted
|
|
|
3
3
|
const GdhRead = preload("reading.gd")
|
|
4
4
|
const GdhLog = preload("logger.gd")
|
|
5
5
|
const GdhProjectFile = preload("project_file.gd")
|
|
6
|
+
## The device an event matches every device with, InputMap's ALL_DEVICES, which GDScript is not given.
|
|
7
|
+
const ALL_DEVICES: int = -1
|
|
6
8
|
|
|
7
9
|
# The key names a caller may write, and what the engine calls them.
|
|
8
10
|
const KEY_NAMES: Dictionary = {
|
|
@@ -108,7 +110,13 @@ func _init(p_log: GdhLog) -> void:
|
|
|
108
110
|
func add_input_action(params: Dictionary) -> Dictionary:
|
|
109
111
|
var action_name: String = str(params.get("action_name", ""))
|
|
110
112
|
var events: Array = params.get("events", [])
|
|
111
|
-
var
|
|
113
|
+
var setting: String = "input/" + action_name
|
|
114
|
+
var held: Variant = ProjectSettings.get_setting(setting) if ProjectSettings.has_setting(setting) else null
|
|
115
|
+
var old: Dictionary = held if held is Dictionary else {}
|
|
116
|
+
var old_deadzone: float = GdhRead.as_float(old.get("deadzone", 0.5), 0.5)
|
|
117
|
+
# A rebind that names no deadzone keeps the one the action has, which a caller adding a key did
|
|
118
|
+
# not mean to change.
|
|
119
|
+
var deadzone: float = GdhRead.as_float(params.get("deadzone", old_deadzone), old_deadzone)
|
|
112
120
|
|
|
113
121
|
_log.info("Adding input action: " + action_name)
|
|
114
122
|
# The editor's Input Map refuses these, since project.godot keys an action by its name and
|
|
@@ -150,7 +158,9 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
150
158
|
if key_value == KEY_NONE:
|
|
151
159
|
return _log.failure(_unknown_key(keycode))
|
|
152
160
|
|
|
153
|
-
var event_config: Dictionary = {"class_name": "InputEventKey"
|
|
161
|
+
var event_config: Dictionary = {"class_name": "InputEventKey"}
|
|
162
|
+
var physical: bool = GdhRead.as_bool(fields.get("physical", false))
|
|
163
|
+
event_config["physical_keycode" if physical else "keycode"] = key_value
|
|
154
164
|
if fields.get("ctrl", false):
|
|
155
165
|
event_config["ctrl_pressed"] = true
|
|
156
166
|
if fields.get("alt", false):
|
|
@@ -190,7 +200,6 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
190
200
|
|
|
191
201
|
# An action that exists is rebound, which is how one of the engine's ui_ actions is changed, so
|
|
192
202
|
# what it held is answered rather than lost without a word.
|
|
193
|
-
var setting: String = "input/" + action_name
|
|
194
203
|
var answer: Dictionary = {
|
|
195
204
|
"action_name": action_name,
|
|
196
205
|
"events_count": events_config.size(),
|
|
@@ -199,8 +208,6 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
199
208
|
"was_new": not ProjectSettings.has_setting(setting),
|
|
200
209
|
}
|
|
201
210
|
if not answer["was_new"]:
|
|
202
|
-
var held: Variant = ProjectSettings.get_setting(setting)
|
|
203
|
-
var old: Dictionary = held if held is Dictionary else {}
|
|
204
211
|
var old_events: Array[Dictionary] = []
|
|
205
212
|
var listed: Variant = old.get("events", [])
|
|
206
213
|
if listed is Array:
|
|
@@ -210,7 +217,7 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
210
217
|
var bound: InputEvent = event
|
|
211
218
|
old_events.append(_shaped(bound))
|
|
212
219
|
answer["old_events"] = old_events
|
|
213
|
-
answer["old_deadzone"] =
|
|
220
|
+
answer["old_deadzone"] = old_deadzone
|
|
214
221
|
|
|
215
222
|
# Through ProjectSettings so the file is saved the way the editor saves it, header and
|
|
216
223
|
# every other line kept; a ConfigFile of project.godot drops the comments on the way out.
|
|
@@ -226,9 +233,12 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
226
233
|
## under text, which also covers a kind of event that form has no fields for.
|
|
227
234
|
static func _shaped(event: InputEvent) -> Dictionary:
|
|
228
235
|
var shaped: Dictionary = {"class_name": event.get_class(), "text": event.as_text()}
|
|
236
|
+
if event.device != ALL_DEVICES:
|
|
237
|
+
shaped["device"] = event.device
|
|
229
238
|
if event is InputEventKey:
|
|
230
239
|
var key: InputEventKey = event
|
|
231
|
-
|
|
240
|
+
if key.keycode != KEY_NONE or key.physical_keycode == KEY_NONE:
|
|
241
|
+
shaped["keycode"] = key.keycode
|
|
232
242
|
if key.physical_keycode != KEY_NONE:
|
|
233
243
|
shaped["physical_keycode"] = key.physical_keycode
|
|
234
244
|
for modifier: String in ["ctrl_pressed", "alt_pressed", "shift_pressed"]:
|
|
@@ -328,6 +338,7 @@ func build_input_action(deadzone: float, events: Array) -> Dictionary:
|
|
|
328
338
|
"InputEventKey":
|
|
329
339
|
var key: InputEventKey = InputEventKey.new()
|
|
330
340
|
key.keycode = GdhRead.as_int(event.get("keycode", 0)) as Key
|
|
341
|
+
key.physical_keycode = GdhRead.as_int(event.get("physical_keycode", 0)) as Key
|
|
331
342
|
key.ctrl_pressed = GdhRead.as_bool(event.get("ctrl_pressed", false))
|
|
332
343
|
key.alt_pressed = GdhRead.as_bool(event.get("alt_pressed", false))
|
|
333
344
|
key.shift_pressed = GdhRead.as_bool(event.get("shift_pressed", false))
|
|
@@ -354,6 +365,10 @@ func build_input_action(deadzone: float, events: Array) -> Dictionary:
|
|
|
354
365
|
|
|
355
366
|
_:
|
|
356
367
|
return _log.failure("Unknown input event class: " + evt_class)
|
|
368
|
+
# Every device, as the editor's Input Map binds an event: a new event names one device, and
|
|
369
|
+
# an action bound to it ignored a second joypad.
|
|
370
|
+
var bound: InputEvent = built.back()
|
|
371
|
+
bound.device = ALL_DEVICES
|
|
357
372
|
|
|
358
373
|
return {"deadzone": deadzone, "events": built}
|
|
359
374
|
|