gdharness 0.5.7 → 0.5.8
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 +10 -1
- package/build/godot/addons/auto_reload/auto_reload.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/bridge_client.gd +7 -1
- package/build/godot/addons/gdharness_editor/bridge_client.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/plugin.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +76 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +9 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd.uid +1 -0
- package/build/godot/operations/serialisation.gd +8 -5
- package/build/index.js +9 -1
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -15821,7 +15821,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15821
15821
|
},
|
|
15822
15822
|
{
|
|
15823
15823
|
name: "runtime_input",
|
|
15824
|
-
description: "Input to the running game: a whole click on a Control named by path, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
|
|
15824
|
+
description: "Input to the running game: a whole click on a Control named by path, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
|
|
15825
15825
|
parameters: {
|
|
15826
15826
|
projectPath: RUNNING_PROJECT_PATH,
|
|
15827
15827
|
nodePath: { type: "string", description: "click: the Control to click, at its centre." },
|
|
@@ -15829,6 +15829,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15829
15829
|
pressed: { type: "boolean", description: "Press or release. Default true." },
|
|
15830
15830
|
strength: { type: "number", description: "action: 0 to 1. Default 1." },
|
|
15831
15831
|
keycode: { type: "string", description: 'key: the key name, such as "Space" or "A".' },
|
|
15832
|
+
text: {
|
|
15833
|
+
type: "string",
|
|
15834
|
+
description: "text: what to type. A newline is Enter and a tab is Tab."
|
|
15835
|
+
},
|
|
15832
15836
|
shift: { type: "boolean" },
|
|
15833
15837
|
ctrl: { type: "boolean" },
|
|
15834
15838
|
alt: { type: "boolean" },
|
|
@@ -15851,6 +15855,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15851
15855
|
},
|
|
15852
15856
|
action: { summary: "press or release an action", requires: ["action"] },
|
|
15853
15857
|
key: { summary: "press or release a key", requires: ["keycode"] },
|
|
15858
|
+
text: {
|
|
15859
|
+
summary: "type a string wherever the focus is, a character at a time",
|
|
15860
|
+
requires: ["text"]
|
|
15861
|
+
},
|
|
15854
15862
|
mouse_click: { summary: "one mouse button event at a position", requires: ["x", "y"] },
|
|
15855
15863
|
mouse_motion: { summary: "move the mouse to a position", requires: ["x", "y"] }
|
|
15856
15864
|
}
|
|
@@ -38469,6 +38477,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
38469
38477
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
38470
38478
|
| What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
38471
38479
|
| Press a button | \`runtime_input click\`, which says what was under the pointer |
|
|
38480
|
+
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` |
|
|
38472
38481
|
| Wait for something | \`runtime_wait\`, never a sleep |
|
|
38473
38482
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|
|
38474
38483
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://dfde0vm55ws7m
|
|
@@ -19,7 +19,8 @@ var server_url: String = DEFAULT_URL
|
|
|
19
19
|
var _is_connected: bool = false
|
|
20
20
|
var _reconnect_timer: Timer
|
|
21
21
|
var _current_reconnect_delay: float = RECONNECT_DELAY
|
|
22
|
-
|
|
22
|
+
## Nothing reconnects until somebody has asked to connect once.
|
|
23
|
+
var _should_reconnect: bool = false
|
|
23
24
|
var _project_path: String
|
|
24
25
|
var _initialized: bool = false
|
|
25
26
|
|
|
@@ -43,6 +44,11 @@ func _process(_delta: float) -> void:
|
|
|
43
44
|
if socket.get_ready_state() == WebSocketPeer.STATE_CLOSED:
|
|
44
45
|
if _is_connected:
|
|
45
46
|
_handle_disconnect()
|
|
47
|
+
elif _should_reconnect and _reconnect_timer.is_stopped():
|
|
48
|
+
# A connection refused never opened, so it reaches CLOSED without passing through
|
|
49
|
+
# _handle_disconnect and nothing would ask again. An editor opened before the server
|
|
50
|
+
# is the ordinary way that happens, and it then sat there for the rest of the day.
|
|
51
|
+
_schedule_reconnect()
|
|
46
52
|
return
|
|
47
53
|
|
|
48
54
|
socket.poll()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://t1telvjuccb0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://bxebcmjteyrwl
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://dmjxtcqxfrure
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://bbmtwsaoi4bh0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://bpusjmmbajpxq
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://d27rjl7bpjvss
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://bdfggiknkfi0f
|
|
@@ -66,6 +66,7 @@ func _init() -> void:
|
|
|
66
66
|
"capture_viewport": _capture.capture_viewport,
|
|
67
67
|
"inject_action": _input.inject_action,
|
|
68
68
|
"inject_key": _input.inject_key,
|
|
69
|
+
"inject_text": _input.inject_text,
|
|
69
70
|
"inject_mouse_click": _input.inject_mouse_click,
|
|
70
71
|
"inject_mouse_motion": _input.inject_mouse_motion,
|
|
71
72
|
"click": _input.click,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://c5crv4n4t7c5
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://cxhjoq814ox42
|
|
@@ -5,6 +5,13 @@ extends RefCounted
|
|
|
5
5
|
|
|
6
6
|
const Values = preload("runtime_values.gd")
|
|
7
7
|
|
|
8
|
+
## The distance from a capital letter to its small one in Unicode. A keycode holds the capital.
|
|
9
|
+
const TO_SMALL: int = 32
|
|
10
|
+
|
|
11
|
+
## The two characters a field reads as keys rather than as text.
|
|
12
|
+
const NEWLINE: int = 10
|
|
13
|
+
const TAB: int = 9
|
|
14
|
+
|
|
8
15
|
var _host: Node
|
|
9
16
|
var _values: Values
|
|
10
17
|
|
|
@@ -69,6 +76,12 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
69
76
|
event.ctrl_pressed = bool(params.get("ctrl", false))
|
|
70
77
|
event.alt_pressed = bool(params.get("alt", false))
|
|
71
78
|
|
|
79
|
+
# The fourth thing a real event carries, and the only one a text field reads: LineEdit and
|
|
80
|
+
# TextEdit insert `unicode` and never consult the keycode, so an injected key could press any
|
|
81
|
+
# action in the map and still type nothing into a search box. Godot's keycodes for printable
|
|
82
|
+
# keys are the code points themselves, which is what makes this a mapping and not a table.
|
|
83
|
+
event.unicode = _glyph_of(event.keycode, event.shift_pressed)
|
|
84
|
+
|
|
72
85
|
Input.parse_input_event(event)
|
|
73
86
|
|
|
74
87
|
return {
|
|
@@ -79,10 +92,73 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
79
92
|
"shift": event.shift_pressed,
|
|
80
93
|
"ctrl": event.ctrl_pressed,
|
|
81
94
|
"alt": event.alt_pressed,
|
|
95
|
+
"unicode": event.unicode,
|
|
82
96
|
"pressed": pressed
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
|
|
100
|
+
## Types [param text] wherever the focus is, one key event per character.
|
|
101
|
+
##
|
|
102
|
+
## A key on its own cannot do this and should not try: which character a key produces is the
|
|
103
|
+
## keyboard layout's business, and shift over a digit is an exclamation mark on one layout and
|
|
104
|
+
## something else on the next. Given the character instead there is nothing to guess, so a field
|
|
105
|
+
## can be filled with anything a player could type, this project's own two typefaces included.
|
|
106
|
+
##
|
|
107
|
+
## A newline and a tab are the two characters a field reads as keys rather than as text, so they
|
|
108
|
+
## are sent as those keys and carry no character of their own: typing a name and submitting it is
|
|
109
|
+
## one call rather than two.
|
|
110
|
+
##
|
|
111
|
+
## Pushed into the viewport for the reason [method click] is, and it matters more here: the focus
|
|
112
|
+
## is what decides where a character lands, so a caller that clicked a field and then typed would
|
|
113
|
+
## otherwise have both waiting in the same queue with nothing said about the order.
|
|
114
|
+
func inject_text(params: Dictionary) -> Dictionary:
|
|
115
|
+
var text: String = String(params.get("text", ""))
|
|
116
|
+
if text.is_empty():
|
|
117
|
+
return {"type": "error", "message": "text required"}
|
|
118
|
+
|
|
119
|
+
var viewport: Viewport = _host.get_tree().root
|
|
120
|
+
for index: int in text.length():
|
|
121
|
+
var down: InputEventKey = _typed(text.unicode_at(index))
|
|
122
|
+
viewport.push_input(down)
|
|
123
|
+
# The release as well, so nothing is left held down behind the caller.
|
|
124
|
+
var up: InputEventKey = down.duplicate()
|
|
125
|
+
up.pressed = false
|
|
126
|
+
viewport.push_input(up)
|
|
127
|
+
|
|
128
|
+
return {"type": "input_injected", "input_type": "text", "text": text, "characters": text.length()}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
## The key press that produces [param glyph], as a keyboard would send it.
|
|
132
|
+
func _typed(glyph: int) -> InputEventKey:
|
|
133
|
+
var event: InputEventKey = InputEventKey.new()
|
|
134
|
+
event.pressed = true
|
|
135
|
+
if glyph == NEWLINE:
|
|
136
|
+
event.keycode = KEY_ENTER
|
|
137
|
+
elif glyph == TAB:
|
|
138
|
+
event.keycode = KEY_TAB
|
|
139
|
+
else:
|
|
140
|
+
var capital: int = String.chr(glyph).to_upper().unicode_at(0)
|
|
141
|
+
event.keycode = capital as Key
|
|
142
|
+
event.shift_pressed = capital != glyph
|
|
143
|
+
event.unicode = glyph
|
|
144
|
+
event.physical_keycode = event.keycode
|
|
145
|
+
event.key_label = event.keycode
|
|
146
|
+
return event
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
## The character a key produces, or nothing for a key that produces none.
|
|
150
|
+
##
|
|
151
|
+
## Godot's keycodes below [constant KEY_SPECIAL] are the Unicode code points of the keys that
|
|
152
|
+
## print something, so the mapping is the value itself. Letters are held as their capitals, which
|
|
153
|
+
## is the one place the shift a caller asked for changes the answer rather than the key.
|
|
154
|
+
func _glyph_of(keycode: Key, shifted: bool) -> int:
|
|
155
|
+
if keycode >= KEY_SPECIAL:
|
|
156
|
+
return 0
|
|
157
|
+
if keycode >= KEY_A and keycode <= KEY_Z and not shifted:
|
|
158
|
+
return keycode + TO_SMALL
|
|
159
|
+
return keycode
|
|
160
|
+
|
|
161
|
+
|
|
86
162
|
## A point the tool schema sends as two flat numbers, or the older form of one [x, y] value.
|
|
87
163
|
## Answers a Vector2, or the String that says what was wrong with it.
|
|
88
164
|
func _read_point(params: Dictionary, x_key: String, y_key: String, pair_key: String) -> Variant:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://d1shdw1mlyig7
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://bueql5kulh1dm
|
|
@@ -28,7 +28,16 @@ const SERIALISERS: Dictionary = {
|
|
|
28
28
|
|
|
29
29
|
## Converts a Godot value into something JSON can carry. A type with no entry in the table
|
|
30
30
|
## passes through as itself, which is what the JSON-native ones want.
|
|
31
|
+
##
|
|
32
|
+
## A method or property typed as a class answers a null with a Variant of type OBJECT that has
|
|
33
|
+
## nothing behind it, and so does one whose object has been freed. Asking either for its class is
|
|
34
|
+
## a script error, and a script error here costs far more than the one answer: the request is
|
|
35
|
+
## never replied to at all, and an editor playing the game stops it dead on the error, so every
|
|
36
|
+
## question after it times out as well. `find_child` for a name nothing has cost a whole session
|
|
37
|
+
## that way.
|
|
31
38
|
func serialize(value: Variant) -> Variant:
|
|
39
|
+
if typeof(value) == TYPE_OBJECT and not is_instance_valid(value):
|
|
40
|
+
return null
|
|
32
41
|
var serialiser: String = SERIALISERS.get(typeof(value), "")
|
|
33
42
|
return call(serialiser, value) if not serialiser.is_empty() else value
|
|
34
43
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://bamdnyhpw2umv
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://cpx5hgxdn8dwd
|
|
@@ -27,7 +27,15 @@ const SERIALISERS: Dictionary = {
|
|
|
27
27
|
|
|
28
28
|
# Converts a Godot value into something JSON can carry. A type with no entry in the table
|
|
29
29
|
# passes through as itself, which is what the JSON-native ones want.
|
|
30
|
+
#
|
|
31
|
+
# A property that holds no object is Object-typed and null rather than nil, which is most of what
|
|
32
|
+
# a node's property list is, and an object that has been freed is the same shape again. Both are
|
|
33
|
+
# refused here rather than inside _serialize_object, because a freed one never reaches it: the
|
|
34
|
+
# call itself fails on the argument, with "previously freed is not a subclass of the expected
|
|
35
|
+
# argument class", and a guard behind that is a guard that never runs.
|
|
30
36
|
func serialize_value(value: Variant) -> Variant:
|
|
37
|
+
if typeof(value) == TYPE_OBJECT and not is_instance_valid(value):
|
|
38
|
+
return null
|
|
31
39
|
var serialiser: String = SERIALISERS.get(typeof(value), "")
|
|
32
40
|
return call(serialiser, value) if not serialiser.is_empty() else value
|
|
33
41
|
|
|
@@ -140,12 +148,7 @@ func _serialize_dictionary(value: Dictionary) -> Dictionary:
|
|
|
140
148
|
|
|
141
149
|
|
|
142
150
|
# A pathless Resource says so rather than inventing an empty path for the caller to load.
|
|
143
|
-
#
|
|
144
|
-
# A property that holds no object is Object-typed and null rather than nil, which is most of
|
|
145
|
-
# what a node's property list is, so this is the branch that runs the most.
|
|
146
151
|
func _serialize_object(value: Object) -> Variant:
|
|
147
|
-
if not is_instance_valid(value):
|
|
148
|
-
return null
|
|
149
152
|
if not value is Resource:
|
|
150
153
|
return {"_type": "Object", "class": value.get_class()}
|
|
151
154
|
var resource: Resource = value
|
package/build/index.js
CHANGED
|
@@ -28251,7 +28251,7 @@ var TOOL_SPECS = [
|
|
|
28251
28251
|
},
|
|
28252
28252
|
{
|
|
28253
28253
|
name: "runtime_input",
|
|
28254
|
-
description: "Input to the running game: a whole click on a Control named by path, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
|
|
28254
|
+
description: "Input to the running game: a whole click on a Control named by path, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
|
|
28255
28255
|
parameters: {
|
|
28256
28256
|
projectPath: RUNNING_PROJECT_PATH,
|
|
28257
28257
|
nodePath: { type: "string", description: "click: the Control to click, at its centre." },
|
|
@@ -28259,6 +28259,10 @@ var TOOL_SPECS = [
|
|
|
28259
28259
|
pressed: { type: "boolean", description: "Press or release. Default true." },
|
|
28260
28260
|
strength: { type: "number", description: "action: 0 to 1. Default 1." },
|
|
28261
28261
|
keycode: { type: "string", description: 'key: the key name, such as "Space" or "A".' },
|
|
28262
|
+
text: {
|
|
28263
|
+
type: "string",
|
|
28264
|
+
description: "text: what to type. A newline is Enter and a tab is Tab."
|
|
28265
|
+
},
|
|
28262
28266
|
shift: { type: "boolean" },
|
|
28263
28267
|
ctrl: { type: "boolean" },
|
|
28264
28268
|
alt: { type: "boolean" },
|
|
@@ -28281,6 +28285,10 @@ var TOOL_SPECS = [
|
|
|
28281
28285
|
},
|
|
28282
28286
|
action: { summary: "press or release an action", requires: ["action"] },
|
|
28283
28287
|
key: { summary: "press or release a key", requires: ["keycode"] },
|
|
28288
|
+
text: {
|
|
28289
|
+
summary: "type a string wherever the focus is, a character at a time",
|
|
28290
|
+
requires: ["text"]
|
|
28291
|
+
},
|
|
28284
28292
|
mouse_click: { summary: "one mouse button event at a position", requires: ["x", "y"] },
|
|
28285
28293
|
mouse_motion: { summary: "move the mouse to a position", requires: ["x", "y"] }
|
|
28286
28294
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|