gdharness 0.5.6 → 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 +50 -5
- 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 +49 -4
- 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
|
}
|
|
@@ -34164,6 +34172,8 @@ var init_godot_bridge = __esm(() => {
|
|
|
34164
34172
|
server.once("error", (error) => {
|
|
34165
34173
|
if (!settled) {
|
|
34166
34174
|
settled = true;
|
|
34175
|
+
server.close();
|
|
34176
|
+
godotWss.close();
|
|
34167
34177
|
reject(error);
|
|
34168
34178
|
return;
|
|
34169
34179
|
}
|
|
@@ -35730,6 +35740,7 @@ class GodotServer {
|
|
|
35730
35740
|
lspClient = null;
|
|
35731
35741
|
dapClient = null;
|
|
35732
35742
|
bridgeStartupError = null;
|
|
35743
|
+
bridgeRetry = null;
|
|
35733
35744
|
lastProjectPath = null;
|
|
35734
35745
|
shutdownInitiated = false;
|
|
35735
35746
|
constructor() {
|
|
@@ -35786,11 +35797,43 @@ class GodotServer {
|
|
|
35786
35797
|
const reason = errorMessage(bridgeError);
|
|
35787
35798
|
this.bridgeStartupError = code && !reason.includes(code) ? `${code}: ${reason}` : reason;
|
|
35788
35799
|
console.error(`[SERVER] Warning: Godot Editor Bridge failed to start: ${this.bridgeStartupError}`);
|
|
35789
|
-
console.error("[SERVER] Continuing without bridge-backed editor tools.");
|
|
35800
|
+
console.error("[SERVER] Continuing without bridge-backed editor tools, and trying again.");
|
|
35801
|
+
this.keepTryingTheBridge();
|
|
35802
|
+
}
|
|
35803
|
+
}
|
|
35804
|
+
keepTryingTheBridge() {
|
|
35805
|
+
if (this.bridgeRetry !== null || this.shutdownInitiated) {
|
|
35806
|
+
return;
|
|
35807
|
+
}
|
|
35808
|
+
this.bridgeRetry = setInterval(() => {
|
|
35809
|
+
this.tryTheBridgeAgain();
|
|
35810
|
+
}, BRIDGE_RETRY_MS);
|
|
35811
|
+
this.bridgeRetry.unref();
|
|
35812
|
+
}
|
|
35813
|
+
async tryTheBridgeAgain() {
|
|
35814
|
+
if (this.shutdownInitiated) {
|
|
35815
|
+
this.stopTryingTheBridge();
|
|
35816
|
+
return;
|
|
35817
|
+
}
|
|
35818
|
+
try {
|
|
35819
|
+
await this.godotBridge.start();
|
|
35820
|
+
} catch {
|
|
35821
|
+
return;
|
|
35822
|
+
}
|
|
35823
|
+
this.bridgeStartupError = null;
|
|
35824
|
+
this.stopTryingTheBridge();
|
|
35825
|
+
const bridgeStatus = this.godotBridge.getStatus();
|
|
35826
|
+
console.error(`[SERVER] Godot Editor Bridge came up on ${bridgeStatus.host}:${bridgeStatus.port}; editor tools are live.`);
|
|
35827
|
+
}
|
|
35828
|
+
stopTryingTheBridge() {
|
|
35829
|
+
if (this.bridgeRetry !== null) {
|
|
35830
|
+
clearInterval(this.bridgeRetry);
|
|
35831
|
+
this.bridgeRetry = null;
|
|
35790
35832
|
}
|
|
35791
35833
|
}
|
|
35792
35834
|
async cleanup() {
|
|
35793
35835
|
this.logDebug("Cleaning up resources");
|
|
35836
|
+
this.stopTryingTheBridge();
|
|
35794
35837
|
if (this.activeProcess) {
|
|
35795
35838
|
this.activeProcess.process?.kill();
|
|
35796
35839
|
this.activeProcess = null;
|
|
@@ -36612,8 +36655,9 @@ class GodotServer {
|
|
|
36612
36655
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
36613
36656
|
startupError: this.bridgeStartupError,
|
|
36614
36657
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
36615
|
-
|
|
36616
|
-
|
|
36658
|
+
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
36659
|
+
note: isPortConflict ? "Bridge port is already in use. Another gdharness instance owns the editor bridge, so this server cannot reach the editor. Usually the server this one replaced, still on its way out." : undefined,
|
|
36660
|
+
suggestion: isPortConflict ? `This server is asking for the port again every ${BRIDGE_RETRY_MS / 1000}s and takes it the moment the other one lets go, so editor tools come back on their own. Ask again, or end the older gdharness process to have it now.` : undefined
|
|
36617
36661
|
};
|
|
36618
36662
|
}
|
|
36619
36663
|
async editorPlayingState() {
|
|
@@ -37091,7 +37135,7 @@ async function runGodotServer() {
|
|
|
37091
37135
|
const server = new GodotServer;
|
|
37092
37136
|
await server.run();
|
|
37093
37137
|
}
|
|
37094
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS;
|
|
37138
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, BRIDGE_RETRY_MS = 2000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS;
|
|
37095
37139
|
var init_server3 = __esm(() => {
|
|
37096
37140
|
init_mcp();
|
|
37097
37141
|
init_stdio2();
|
|
@@ -38433,6 +38477,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
38433
38477
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
38434
38478
|
| What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
38435
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\` |
|
|
38436
38481
|
| Wait for something | \`runtime_wait\`, never a sleep |
|
|
38437
38482
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|
|
38438
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
|
@@ -25468,6 +25468,8 @@ class GodotBridge extends EventEmitter {
|
|
|
25468
25468
|
server.once("error", (error) => {
|
|
25469
25469
|
if (!settled) {
|
|
25470
25470
|
settled = true;
|
|
25471
|
+
server.close();
|
|
25472
|
+
godotWss.close();
|
|
25471
25473
|
reject(error);
|
|
25472
25474
|
return;
|
|
25473
25475
|
}
|
|
@@ -28249,7 +28251,7 @@ var TOOL_SPECS = [
|
|
|
28249
28251
|
},
|
|
28250
28252
|
{
|
|
28251
28253
|
name: "runtime_input",
|
|
28252
|
-
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.",
|
|
28253
28255
|
parameters: {
|
|
28254
28256
|
projectPath: RUNNING_PROJECT_PATH,
|
|
28255
28257
|
nodePath: { type: "string", description: "click: the Control to click, at its centre." },
|
|
@@ -28257,6 +28259,10 @@ var TOOL_SPECS = [
|
|
|
28257
28259
|
pressed: { type: "boolean", description: "Press or release. Default true." },
|
|
28258
28260
|
strength: { type: "number", description: "action: 0 to 1. Default 1." },
|
|
28259
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
|
+
},
|
|
28260
28266
|
shift: { type: "boolean" },
|
|
28261
28267
|
ctrl: { type: "boolean" },
|
|
28262
28268
|
alt: { type: "boolean" },
|
|
@@ -28279,6 +28285,10 @@ var TOOL_SPECS = [
|
|
|
28279
28285
|
},
|
|
28280
28286
|
action: { summary: "press or release an action", requires: ["action"] },
|
|
28281
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
|
+
},
|
|
28282
28292
|
mouse_click: { summary: "one mouse button event at a position", requires: ["x", "y"] },
|
|
28283
28293
|
mouse_motion: { summary: "move the mouse to a position", requires: ["x", "y"] }
|
|
28284
28294
|
}
|
|
@@ -28405,6 +28415,7 @@ var FEEDBACK_NOTICE_EVERY = 250;
|
|
|
28405
28415
|
var run3 = promisify3(execFile3);
|
|
28406
28416
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
28407
28417
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
28418
|
+
var BRIDGE_RETRY_MS = 2000;
|
|
28408
28419
|
var PATH_SOLUTIONS = [
|
|
28409
28420
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
28410
28421
|
"Point projectPath at the project the file belongs to"
|
|
@@ -28511,6 +28522,7 @@ class GodotServer {
|
|
|
28511
28522
|
lspClient = null;
|
|
28512
28523
|
dapClient = null;
|
|
28513
28524
|
bridgeStartupError = null;
|
|
28525
|
+
bridgeRetry = null;
|
|
28514
28526
|
lastProjectPath = null;
|
|
28515
28527
|
shutdownInitiated = false;
|
|
28516
28528
|
constructor() {
|
|
@@ -28567,11 +28579,43 @@ class GodotServer {
|
|
|
28567
28579
|
const reason = errorMessage(bridgeError);
|
|
28568
28580
|
this.bridgeStartupError = code && !reason.includes(code) ? `${code}: ${reason}` : reason;
|
|
28569
28581
|
console.error(`[SERVER] Warning: Godot Editor Bridge failed to start: ${this.bridgeStartupError}`);
|
|
28570
|
-
console.error("[SERVER] Continuing without bridge-backed editor tools.");
|
|
28582
|
+
console.error("[SERVER] Continuing without bridge-backed editor tools, and trying again.");
|
|
28583
|
+
this.keepTryingTheBridge();
|
|
28584
|
+
}
|
|
28585
|
+
}
|
|
28586
|
+
keepTryingTheBridge() {
|
|
28587
|
+
if (this.bridgeRetry !== null || this.shutdownInitiated) {
|
|
28588
|
+
return;
|
|
28589
|
+
}
|
|
28590
|
+
this.bridgeRetry = setInterval(() => {
|
|
28591
|
+
this.tryTheBridgeAgain();
|
|
28592
|
+
}, BRIDGE_RETRY_MS);
|
|
28593
|
+
this.bridgeRetry.unref();
|
|
28594
|
+
}
|
|
28595
|
+
async tryTheBridgeAgain() {
|
|
28596
|
+
if (this.shutdownInitiated) {
|
|
28597
|
+
this.stopTryingTheBridge();
|
|
28598
|
+
return;
|
|
28599
|
+
}
|
|
28600
|
+
try {
|
|
28601
|
+
await this.godotBridge.start();
|
|
28602
|
+
} catch {
|
|
28603
|
+
return;
|
|
28604
|
+
}
|
|
28605
|
+
this.bridgeStartupError = null;
|
|
28606
|
+
this.stopTryingTheBridge();
|
|
28607
|
+
const bridgeStatus = this.godotBridge.getStatus();
|
|
28608
|
+
console.error(`[SERVER] Godot Editor Bridge came up on ${bridgeStatus.host}:${bridgeStatus.port}; editor tools are live.`);
|
|
28609
|
+
}
|
|
28610
|
+
stopTryingTheBridge() {
|
|
28611
|
+
if (this.bridgeRetry !== null) {
|
|
28612
|
+
clearInterval(this.bridgeRetry);
|
|
28613
|
+
this.bridgeRetry = null;
|
|
28571
28614
|
}
|
|
28572
28615
|
}
|
|
28573
28616
|
async cleanup() {
|
|
28574
28617
|
this.logDebug("Cleaning up resources");
|
|
28618
|
+
this.stopTryingTheBridge();
|
|
28575
28619
|
if (this.activeProcess) {
|
|
28576
28620
|
this.activeProcess.process?.kill();
|
|
28577
28621
|
this.activeProcess = null;
|
|
@@ -29393,8 +29437,9 @@ class GodotServer {
|
|
|
29393
29437
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
29394
29438
|
startupError: this.bridgeStartupError,
|
|
29395
29439
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
29396
|
-
|
|
29397
|
-
|
|
29440
|
+
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
29441
|
+
note: isPortConflict ? "Bridge port is already in use. Another gdharness instance owns the editor bridge, so this server cannot reach the editor. Usually the server this one replaced, still on its way out." : undefined,
|
|
29442
|
+
suggestion: isPortConflict ? `This server is asking for the port again every ${BRIDGE_RETRY_MS / 1000}s and takes it the moment the other one lets go, so editor tools come back on their own. Ask again, or end the older gdharness process to have it now.` : undefined
|
|
29398
29443
|
};
|
|
29399
29444
|
}
|
|
29400
29445
|
async editorPlayingState() {
|
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",
|