gdharness 0.6.5 → 0.6.7
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 +73 -9
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +91 -10
- package/build/index.js +61 -7
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -15850,9 +15850,15 @@ var init_tool_definitions = __esm(() => {
|
|
|
15850
15850
|
description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn. choose: the PopupMenu, or the OptionButton or MenuButton in front of one."
|
|
15851
15851
|
},
|
|
15852
15852
|
action: { type: "string", description: "action: the InputMap action name." },
|
|
15853
|
-
pressed: {
|
|
15853
|
+
pressed: {
|
|
15854
|
+
type: "boolean",
|
|
15855
|
+
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held."
|
|
15856
|
+
},
|
|
15854
15857
|
strength: { type: "number", description: "action: 0 to 1. Default 1." },
|
|
15855
|
-
keycode: {
|
|
15858
|
+
keycode: {
|
|
15859
|
+
type: ["string", "number"],
|
|
15860
|
+
description: 'key: the key name, such as "Space" or "A", or its Godot keycode.'
|
|
15861
|
+
},
|
|
15856
15862
|
text: {
|
|
15857
15863
|
type: "string",
|
|
15858
15864
|
description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
|
|
@@ -15867,9 +15873,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
15867
15873
|
x: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
|
|
15868
15874
|
y: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
|
|
15869
15875
|
button: {
|
|
15870
|
-
type: "string",
|
|
15871
|
-
|
|
15872
|
-
description: "click, mouse_click: default left."
|
|
15876
|
+
type: ["string", "number"],
|
|
15877
|
+
description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left."
|
|
15873
15878
|
},
|
|
15874
15879
|
doubleClick: { type: "boolean", description: "click, mouse_click: default false." },
|
|
15875
15880
|
relativeX: { type: "number", description: "mouse_motion: movement since the last event." },
|
|
@@ -15885,8 +15890,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
15885
15890
|
summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the item takes the focus and Enter presses it, which is the engine's own path and needs no window. Answers with what was chosen and what the button in front of it shows now",
|
|
15886
15891
|
requires: ["nodePath"]
|
|
15887
15892
|
},
|
|
15888
|
-
action: { summary: "press or
|
|
15889
|
-
key: { summary: "press or
|
|
15893
|
+
action: { summary: "press an action, or hold it", requires: ["action"] },
|
|
15894
|
+
key: { summary: "press a key, or hold it", requires: ["keycode"] },
|
|
15890
15895
|
text: {
|
|
15891
15896
|
summary: "type a string wherever the focus is, a character at a time",
|
|
15892
15897
|
requires: ["text"]
|
|
@@ -35855,6 +35860,45 @@ import { promisify as promisify5 } from "node:util";
|
|
|
35855
35860
|
function patienceForFrames(frames, atLeast) {
|
|
35856
35861
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
35857
35862
|
}
|
|
35863
|
+
function isOfType(value, kind) {
|
|
35864
|
+
switch (kind) {
|
|
35865
|
+
case "string":
|
|
35866
|
+
return typeof value === "string";
|
|
35867
|
+
case "number":
|
|
35868
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
35869
|
+
case "integer":
|
|
35870
|
+
return Number.isInteger(value);
|
|
35871
|
+
case "boolean":
|
|
35872
|
+
return typeof value === "boolean";
|
|
35873
|
+
case "array":
|
|
35874
|
+
return Array.isArray(value);
|
|
35875
|
+
case "object":
|
|
35876
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
35877
|
+
default:
|
|
35878
|
+
return true;
|
|
35879
|
+
}
|
|
35880
|
+
}
|
|
35881
|
+
function describeValue(value) {
|
|
35882
|
+
if (Array.isArray(value)) {
|
|
35883
|
+
return "a list";
|
|
35884
|
+
}
|
|
35885
|
+
return VALUE_NAMES[typeof value] ?? typeof value;
|
|
35886
|
+
}
|
|
35887
|
+
function wrongTypes(spec, args) {
|
|
35888
|
+
const complaints = [];
|
|
35889
|
+
for (const [name, value] of Object.entries(args)) {
|
|
35890
|
+
if (value === undefined || value === null) {
|
|
35891
|
+
continue;
|
|
35892
|
+
}
|
|
35893
|
+
const declared = spec.parameters[name]?.["type"];
|
|
35894
|
+
const wanted = typeof declared === "string" ? [declared] : Array.isArray(declared) ? declared : [];
|
|
35895
|
+
if (wanted.length === 0 || wanted.some((kind) => isOfType(value, kind))) {
|
|
35896
|
+
continue;
|
|
35897
|
+
}
|
|
35898
|
+
complaints.push(`${name} as ${wanted.join(" or ")}, not ${describeValue(value)}`);
|
|
35899
|
+
}
|
|
35900
|
+
return complaints;
|
|
35901
|
+
}
|
|
35858
35902
|
function escapingPropertyValue(properties) {
|
|
35859
35903
|
if (typeof properties !== "object" || properties === null) {
|
|
35860
35904
|
return;
|
|
@@ -36193,6 +36237,10 @@ class GodotServer {
|
|
|
36193
36237
|
response: this.createErrorResponse(`${spec.name} does not take ${unknown.join(", ")}. It takes: ${[...known].join(", ")}.`)
|
|
36194
36238
|
};
|
|
36195
36239
|
}
|
|
36240
|
+
const wrong = wrongTypes(spec, args);
|
|
36241
|
+
if (wrong.length > 0) {
|
|
36242
|
+
return { ok: false, response: this.createErrorResponse(`${spec.name} takes ${wrong.join("; ")}.`) };
|
|
36243
|
+
}
|
|
36196
36244
|
let op = null;
|
|
36197
36245
|
if (spec.operations) {
|
|
36198
36246
|
const valid = Object.keys(spec.operations);
|
|
@@ -37411,7 +37459,7 @@ async function runGodotServer() {
|
|
|
37411
37459
|
const server = new GodotServer;
|
|
37412
37460
|
await server.run();
|
|
37413
37461
|
}
|
|
37414
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, BRIDGE_RETRY_MS = 2000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS;
|
|
37462
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS;
|
|
37415
37463
|
var init_server3 = __esm(() => {
|
|
37416
37464
|
init_mcp();
|
|
37417
37465
|
init_stdio2();
|
|
@@ -37438,6 +37486,12 @@ var init_server3 = __esm(() => {
|
|
|
37438
37486
|
init_update_check();
|
|
37439
37487
|
run5 = promisify5(execFile5);
|
|
37440
37488
|
__dirname2 = dirname9(fileURLToPath4(import.meta.url));
|
|
37489
|
+
VALUE_NAMES = {
|
|
37490
|
+
string: "a string",
|
|
37491
|
+
number: "a number",
|
|
37492
|
+
boolean: "a boolean",
|
|
37493
|
+
object: "an object"
|
|
37494
|
+
};
|
|
37441
37495
|
PATH_SOLUTIONS = [
|
|
37442
37496
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
37443
37497
|
"Point projectPath at the project the file belongs to"
|
|
@@ -38680,6 +38734,15 @@ import { basename, dirname as dirname4, join as join9 } from "node:path";
|
|
|
38680
38734
|
|
|
38681
38735
|
// src/tool-reference.ts
|
|
38682
38736
|
init_tool_definitions();
|
|
38737
|
+
function namedType(declared) {
|
|
38738
|
+
if (typeof declared === "string") {
|
|
38739
|
+
return declared;
|
|
38740
|
+
}
|
|
38741
|
+
if (Array.isArray(declared) && declared.every((kind) => typeof kind === "string")) {
|
|
38742
|
+
return declared.join(" or ");
|
|
38743
|
+
}
|
|
38744
|
+
return "any";
|
|
38745
|
+
}
|
|
38683
38746
|
function renderToolsMarkdown() {
|
|
38684
38747
|
const lines = [
|
|
38685
38748
|
"# Tools",
|
|
@@ -38710,7 +38773,7 @@ function renderToolsMarkdown() {
|
|
|
38710
38773
|
if (parameters.length > 0) {
|
|
38711
38774
|
lines.push("Arguments:", "");
|
|
38712
38775
|
for (const [name, schema] of parameters) {
|
|
38713
|
-
const type =
|
|
38776
|
+
const type = namedType(schema["type"]);
|
|
38714
38777
|
const note = typeof schema["description"] === "string" ? ` ${schema["description"]}` : "";
|
|
38715
38778
|
lines.push(`- \`${name}\` (${type}):${note}`);
|
|
38716
38779
|
}
|
|
@@ -38767,6 +38830,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
38767
38830
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` |
|
|
38768
38831
|
| Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
|
|
38769
38832
|
| Pick something out of a dropdown | \`runtime_input choose\` on it, by what the item says |
|
|
38833
|
+
| Answer a dialog, or press a bound key | \`runtime_input action\` or \`key\`, a whole press unless you hold it |
|
|
38770
38834
|
| Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
|
|
38771
38835
|
| Wait for something | \`runtime_wait\`, never a sleep |
|
|
38772
38836
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|
|
@@ -30,9 +30,19 @@ func _init(host: Node, values: Values) -> void:
|
|
|
30
30
|
_values = values
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
## Presses [param params].action, and lets go of it again unless asked to hold it.
|
|
34
|
+
##
|
|
35
|
+
## The whole press by default, for the reason [method click] is the whole click: an action left
|
|
36
|
+
## down is not a press that did nothing, it is a press that never ends, and everything reading
|
|
37
|
+
## [method Input.is_action_pressed] goes on seeing it for the rest of the session. Answering a
|
|
38
|
+
## dialog with `ui_accept` took two calls and left the first one held, which is the shape that
|
|
39
|
+
## found this.
|
|
40
|
+
##
|
|
41
|
+
## `pressed` is how a caller says otherwise: true holds it down, false lets go of one being held.
|
|
33
42
|
func inject_action(params: Dictionary) -> Dictionary:
|
|
34
43
|
var action: String = String(params.get("action", ""))
|
|
35
|
-
var
|
|
44
|
+
var held: bool = bool(params.get("pressed", true))
|
|
45
|
+
var whole: bool = not params.has("pressed")
|
|
36
46
|
var strength: float = float(params.get("strength", 1.0))
|
|
37
47
|
|
|
38
48
|
if action.is_empty():
|
|
@@ -41,18 +51,36 @@ func inject_action(params: Dictionary) -> Dictionary:
|
|
|
41
51
|
if not InputMap.has_action(action):
|
|
42
52
|
return {"type": "error", "message": "Action not found: " + action}
|
|
43
53
|
|
|
54
|
+
_say_action(action, whole or held, strength)
|
|
55
|
+
if whole:
|
|
56
|
+
# A frame between the halves, as a click has: a listener that acts on the press and a
|
|
57
|
+
# listener that acts on the release both get their own frame to do it in.
|
|
58
|
+
await _host.get_tree().process_frame
|
|
59
|
+
_say_action(action, false, strength)
|
|
60
|
+
await _host.get_tree().process_frame
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
"type": "input_injected",
|
|
64
|
+
"input_type": "action",
|
|
65
|
+
"action": action,
|
|
66
|
+
"pressed": held and not whole,
|
|
67
|
+
"whole": whole,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
func _say_action(action: String, down: bool, strength: float) -> void:
|
|
44
72
|
var event: InputEventAction = InputEventAction.new()
|
|
45
73
|
event.action = action
|
|
46
|
-
event.pressed =
|
|
74
|
+
event.pressed = down
|
|
47
75
|
event.strength = strength
|
|
48
76
|
Input.parse_input_event(event)
|
|
49
77
|
|
|
50
|
-
return {"type": "input_injected", "input_type": "action", "action": action, "pressed": pressed}
|
|
51
|
-
|
|
52
78
|
|
|
79
|
+
## Presses one key, and lets go of it again unless asked to hold it. See [method inject_action].
|
|
53
80
|
func inject_key(params: Dictionary) -> Dictionary:
|
|
54
81
|
var keycode_raw: Variant = params.get("keycode", 0)
|
|
55
|
-
var
|
|
82
|
+
var held: bool = bool(params.get("pressed", true))
|
|
83
|
+
var whole: bool = not params.has("pressed")
|
|
56
84
|
var key_label: String = String(params.get("key_label", ""))
|
|
57
85
|
|
|
58
86
|
if keycode_raw is String:
|
|
@@ -62,7 +90,7 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
62
90
|
var keycode: int = 0 if keycode_raw is String else int(keycode_raw)
|
|
63
91
|
|
|
64
92
|
var event: InputEventKey = InputEventKey.new()
|
|
65
|
-
event.pressed =
|
|
93
|
+
event.pressed = whole or held
|
|
66
94
|
|
|
67
95
|
if not key_label.is_empty():
|
|
68
96
|
event.keycode = OS.find_keycode_from_string(key_label)
|
|
@@ -92,6 +120,12 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
92
120
|
event.unicode = _glyph_of(event.keycode, event.shift_pressed)
|
|
93
121
|
|
|
94
122
|
Input.parse_input_event(event)
|
|
123
|
+
if whole:
|
|
124
|
+
await _host.get_tree().process_frame
|
|
125
|
+
var up: InputEventKey = event.duplicate()
|
|
126
|
+
up.pressed = false
|
|
127
|
+
Input.parse_input_event(up)
|
|
128
|
+
await _host.get_tree().process_frame
|
|
95
129
|
|
|
96
130
|
return {
|
|
97
131
|
"type": "input_injected",
|
|
@@ -102,7 +136,8 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
102
136
|
"ctrl": event.ctrl_pressed,
|
|
103
137
|
"alt": event.alt_pressed,
|
|
104
138
|
"unicode": event.unicode,
|
|
105
|
-
"pressed":
|
|
139
|
+
"pressed": held and not whole,
|
|
140
|
+
"whole": whole,
|
|
106
141
|
}
|
|
107
142
|
|
|
108
143
|
|
|
@@ -190,6 +225,8 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
190
225
|
return {"type": "error", "message": point}
|
|
191
226
|
var position: Vector2 = point
|
|
192
227
|
var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
|
|
228
|
+
if button < 0:
|
|
229
|
+
return _no_such_button(params.get("button"))
|
|
193
230
|
var pressed: bool = bool(params.get("pressed", true))
|
|
194
231
|
var double: bool = bool(params.get("doubleClick", false))
|
|
195
232
|
|
|
@@ -262,6 +299,24 @@ static func _centre_of(control: Control) -> Vector2:
|
|
|
262
299
|
## the control within the outer one, so the outer has to be asked after the inner has finished
|
|
263
300
|
## moving it. A control with no ScrollContainer over it moves nothing and answers false, which is
|
|
264
301
|
## what keeps the refusal below saying the right thing about a control that is simply off screen.
|
|
302
|
+
## Whether [param centre] is somewhere a click can reach it: inside the viewport, and inside every
|
|
303
|
+
## ScrollContainer between the control and the root, each of which clips what it holds.
|
|
304
|
+
static func _in_sight(control: Control, viewport: Viewport, centre: Vector2) -> bool:
|
|
305
|
+
if not viewport.get_visible_rect().has_point(centre):
|
|
306
|
+
return false
|
|
307
|
+
var walking: Node = control.get_parent()
|
|
308
|
+
while walking != null:
|
|
309
|
+
var holder: ScrollContainer = walking as ScrollContainer
|
|
310
|
+
# Carried into the same space the centre is in, which is the canvas rather than the
|
|
311
|
+
# container's own: the two are only the same while nothing above it is transformed.
|
|
312
|
+
if holder != null:
|
|
313
|
+
var seen: Rect2 = holder.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, holder.size)
|
|
314
|
+
if not seen.has_point(centre):
|
|
315
|
+
return false
|
|
316
|
+
walking = walking.get_parent()
|
|
317
|
+
return true
|
|
318
|
+
|
|
319
|
+
|
|
265
320
|
static func _scroll_into_view(control: Control) -> bool:
|
|
266
321
|
var moved: bool = false
|
|
267
322
|
var walking: Node = control.get_parent()
|
|
@@ -304,12 +359,17 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
304
359
|
var viewport: Viewport = _clicking_viewport(control)
|
|
305
360
|
var centre: Vector2 = _centre_of(control)
|
|
306
361
|
|
|
307
|
-
# A control
|
|
362
|
+
# A control out of sight inside a ScrollContainer is not out of reach, it is one scroll away,
|
|
308
363
|
# which is what a person does without thinking about it before they click. Refusing it
|
|
309
364
|
# instead sent callers to emit the button's own signal, which presses nothing, runs none of
|
|
310
365
|
# the input path and reports success.
|
|
366
|
+
#
|
|
367
|
+
# Out of sight against what it is clipped to rather than against the viewport: a row scrolled
|
|
368
|
+
# off the top of its container is inside the viewport, behind whatever is drawn up there, so
|
|
369
|
+
# the click went to that instead and said so. Measured on a hall whose staff panel had been
|
|
370
|
+
# scrolled past: the button was at y 69 and its container started at y 166.
|
|
311
371
|
var scrolled: bool = false
|
|
312
|
-
if not viewport
|
|
372
|
+
if not _in_sight(control, viewport, centre):
|
|
313
373
|
scrolled = _scroll_into_view(control)
|
|
314
374
|
if scrolled:
|
|
315
375
|
# A container moves its child on the next layout pass rather than inside the call.
|
|
@@ -334,6 +394,8 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
334
394
|
)
|
|
335
395
|
}
|
|
336
396
|
var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
|
|
397
|
+
if button < 0:
|
|
398
|
+
return _no_such_button(params.get("button"))
|
|
337
399
|
var double: bool = bool(params.get("double", false))
|
|
338
400
|
|
|
339
401
|
# Pushed into the viewport rather than through Input: Input accumulates events and flushes
|
|
@@ -594,6 +656,8 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
|
|
|
594
656
|
|
|
595
657
|
var position: Vector2 = viewport.get_final_transform() * aim
|
|
596
658
|
var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
|
|
659
|
+
if button < 0:
|
|
660
|
+
return _no_such_button(params.get("button"))
|
|
597
661
|
var double: bool = bool(params.get("double", false))
|
|
598
662
|
|
|
599
663
|
viewport.push_input(_motion(position, Vector2.ZERO))
|
|
@@ -642,6 +706,23 @@ func _button(position: Vector2, button: int, pressed: bool, double: bool) -> Inp
|
|
|
642
706
|
return event
|
|
643
707
|
|
|
644
708
|
|
|
709
|
+
## What to say about a name that is not a mouse button, with the ones that are.
|
|
710
|
+
func _no_such_button(named: Variant) -> Dictionary:
|
|
711
|
+
return {
|
|
712
|
+
"type": "error",
|
|
713
|
+
"message":
|
|
714
|
+
(
|
|
715
|
+
"%s is not a mouse button. It takes: left, right, middle, wheel_up, wheel_down, or the number of one."
|
|
716
|
+
% JSON.stringify(named)
|
|
717
|
+
)
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
## The button [param raw] names, or -1 for a name that is not one of them.
|
|
722
|
+
##
|
|
723
|
+
## A name nobody recognises used to come back as the left button, so a right click asked for by a
|
|
724
|
+
## spelling this does not know went to the left button and reported the left button. The caller
|
|
725
|
+
## reads the answer and believes it, which is the whole reason nothing here guesses.
|
|
645
726
|
func _resolve_mouse_button(raw: Variant) -> int:
|
|
646
727
|
if raw is String:
|
|
647
728
|
var named: String = raw
|
|
@@ -657,5 +738,5 @@ func _resolve_mouse_button(raw: Variant) -> int:
|
|
|
657
738
|
"wheel_down", "wheeldown":
|
|
658
739
|
return MOUSE_BUTTON_WHEEL_DOWN
|
|
659
740
|
_:
|
|
660
|
-
return
|
|
741
|
+
return -1
|
|
661
742
|
return int(raw)
|
package/build/index.js
CHANGED
|
@@ -28404,9 +28404,15 @@ var TOOL_SPECS = [
|
|
|
28404
28404
|
description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn. choose: the PopupMenu, or the OptionButton or MenuButton in front of one."
|
|
28405
28405
|
},
|
|
28406
28406
|
action: { type: "string", description: "action: the InputMap action name." },
|
|
28407
|
-
pressed: {
|
|
28407
|
+
pressed: {
|
|
28408
|
+
type: "boolean",
|
|
28409
|
+
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held."
|
|
28410
|
+
},
|
|
28408
28411
|
strength: { type: "number", description: "action: 0 to 1. Default 1." },
|
|
28409
|
-
keycode: {
|
|
28412
|
+
keycode: {
|
|
28413
|
+
type: ["string", "number"],
|
|
28414
|
+
description: 'key: the key name, such as "Space" or "A", or its Godot keycode.'
|
|
28415
|
+
},
|
|
28410
28416
|
text: {
|
|
28411
28417
|
type: "string",
|
|
28412
28418
|
description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
|
|
@@ -28421,9 +28427,8 @@ var TOOL_SPECS = [
|
|
|
28421
28427
|
x: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
|
|
28422
28428
|
y: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
|
|
28423
28429
|
button: {
|
|
28424
|
-
type: "string",
|
|
28425
|
-
|
|
28426
|
-
description: "click, mouse_click: default left."
|
|
28430
|
+
type: ["string", "number"],
|
|
28431
|
+
description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left."
|
|
28427
28432
|
},
|
|
28428
28433
|
doubleClick: { type: "boolean", description: "click, mouse_click: default false." },
|
|
28429
28434
|
relativeX: { type: "number", description: "mouse_motion: movement since the last event." },
|
|
@@ -28439,8 +28444,8 @@ var TOOL_SPECS = [
|
|
|
28439
28444
|
summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the item takes the focus and Enter presses it, which is the engine's own path and needs no window. Answers with what was chosen and what the button in front of it shows now",
|
|
28440
28445
|
requires: ["nodePath"]
|
|
28441
28446
|
},
|
|
28442
|
-
action: { summary: "press or
|
|
28443
|
-
key: { summary: "press or
|
|
28447
|
+
action: { summary: "press an action, or hold it", requires: ["action"] },
|
|
28448
|
+
key: { summary: "press a key, or hold it", requires: ["keycode"] },
|
|
28444
28449
|
text: {
|
|
28445
28450
|
summary: "type a string wherever the focus is, a character at a time",
|
|
28446
28451
|
requires: ["text"]
|
|
@@ -28575,6 +28580,51 @@ var SLOWEST_FRAME_RATE = 20;
|
|
|
28575
28580
|
function patienceForFrames(frames, atLeast) {
|
|
28576
28581
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
28577
28582
|
}
|
|
28583
|
+
function isOfType(value, kind) {
|
|
28584
|
+
switch (kind) {
|
|
28585
|
+
case "string":
|
|
28586
|
+
return typeof value === "string";
|
|
28587
|
+
case "number":
|
|
28588
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
28589
|
+
case "integer":
|
|
28590
|
+
return Number.isInteger(value);
|
|
28591
|
+
case "boolean":
|
|
28592
|
+
return typeof value === "boolean";
|
|
28593
|
+
case "array":
|
|
28594
|
+
return Array.isArray(value);
|
|
28595
|
+
case "object":
|
|
28596
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28597
|
+
default:
|
|
28598
|
+
return true;
|
|
28599
|
+
}
|
|
28600
|
+
}
|
|
28601
|
+
var VALUE_NAMES = {
|
|
28602
|
+
string: "a string",
|
|
28603
|
+
number: "a number",
|
|
28604
|
+
boolean: "a boolean",
|
|
28605
|
+
object: "an object"
|
|
28606
|
+
};
|
|
28607
|
+
function describeValue(value) {
|
|
28608
|
+
if (Array.isArray(value)) {
|
|
28609
|
+
return "a list";
|
|
28610
|
+
}
|
|
28611
|
+
return VALUE_NAMES[typeof value] ?? typeof value;
|
|
28612
|
+
}
|
|
28613
|
+
function wrongTypes(spec, args) {
|
|
28614
|
+
const complaints = [];
|
|
28615
|
+
for (const [name, value] of Object.entries(args)) {
|
|
28616
|
+
if (value === undefined || value === null) {
|
|
28617
|
+
continue;
|
|
28618
|
+
}
|
|
28619
|
+
const declared = spec.parameters[name]?.["type"];
|
|
28620
|
+
const wanted = typeof declared === "string" ? [declared] : Array.isArray(declared) ? declared : [];
|
|
28621
|
+
if (wanted.length === 0 || wanted.some((kind) => isOfType(value, kind))) {
|
|
28622
|
+
continue;
|
|
28623
|
+
}
|
|
28624
|
+
complaints.push(`${name} as ${wanted.join(" or ")}, not ${describeValue(value)}`);
|
|
28625
|
+
}
|
|
28626
|
+
return complaints;
|
|
28627
|
+
}
|
|
28578
28628
|
var BRIDGE_RETRY_MS = 2000;
|
|
28579
28629
|
var PATH_SOLUTIONS = [
|
|
28580
28630
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
@@ -28967,6 +29017,10 @@ class GodotServer {
|
|
|
28967
29017
|
response: this.createErrorResponse(`${spec.name} does not take ${unknown.join(", ")}. It takes: ${[...known].join(", ")}.`)
|
|
28968
29018
|
};
|
|
28969
29019
|
}
|
|
29020
|
+
const wrong = wrongTypes(spec, args);
|
|
29021
|
+
if (wrong.length > 0) {
|
|
29022
|
+
return { ok: false, response: this.createErrorResponse(`${spec.name} takes ${wrong.join("; ")}.`) };
|
|
29023
|
+
}
|
|
28970
29024
|
let op = null;
|
|
28971
29025
|
if (spec.operations) {
|
|
28972
29026
|
const valid = Object.keys(spec.operations);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
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",
|