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 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
- var _should_reconnect: bool = true
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.7",
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",