gdharness 0.6.4 → 0.6.6
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
|
@@ -8962,7 +8962,7 @@ var init_versions = __esm(() => {
|
|
|
8962
8962
|
version = {
|
|
8963
8963
|
major: 4,
|
|
8964
8964
|
minor: 6,
|
|
8965
|
-
patch:
|
|
8965
|
+
patch: 5
|
|
8966
8966
|
};
|
|
8967
8967
|
});
|
|
8968
8968
|
|
|
@@ -15744,12 +15744,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
15744
15744
|
},
|
|
15745
15745
|
{
|
|
15746
15746
|
name: "runtime_inspect",
|
|
15747
|
-
description: "Questions about the running game: the scene tree, the nodes matching a query, where one node is on screen, what one property reads, or the performance metrics. Needs the game running with the runtime addon.",
|
|
15747
|
+
description: "Questions about the running game: what is written on the screen, the scene tree, the nodes matching a query, where one node is on screen, what one property reads, or the performance metrics. Needs the game running with the runtime addon.",
|
|
15748
15748
|
parameters: {
|
|
15749
15749
|
projectPath: RUNNING_PROJECT_PATH,
|
|
15750
15750
|
nodePath: {
|
|
15751
15751
|
type: "string",
|
|
15752
|
-
description: "tree, find: where to start, default /root. rect: the node to place. property: the node to read."
|
|
15752
|
+
description: "tree, find, text: where to start, default /root. rect: the node to place. property: the node to read."
|
|
15753
15753
|
},
|
|
15754
15754
|
property: {
|
|
15755
15755
|
type: "string",
|
|
@@ -15771,6 +15771,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15771
15771
|
},
|
|
15772
15772
|
group: { type: "string", description: "find: a group the node is in." },
|
|
15773
15773
|
limit: { type: "number", description: "find: the most nodes to answer with. Default 100." },
|
|
15774
|
+
includeHidden: {
|
|
15775
|
+
type: "boolean",
|
|
15776
|
+
description: "text: read hidden nodes as well, for checking that something is not showing. Default false."
|
|
15777
|
+
},
|
|
15774
15778
|
metrics: {
|
|
15775
15779
|
type: "array",
|
|
15776
15780
|
items: { type: "string" },
|
|
@@ -15780,6 +15784,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15780
15784
|
requires: [],
|
|
15781
15785
|
operations: {
|
|
15782
15786
|
tree: { summary: "the live scene tree", requires: [] },
|
|
15787
|
+
text: {
|
|
15788
|
+
summary: "every line of text under nodePath, in the order somebody reads the screen, leaving out what is hidden and everything under it",
|
|
15789
|
+
requires: []
|
|
15790
|
+
},
|
|
15783
15791
|
find: {
|
|
15784
15792
|
summary: "the paths of every node matching className, script, namePattern or group, with property read off each",
|
|
15785
15793
|
requires: []
|
|
@@ -15842,9 +15850,15 @@ var init_tool_definitions = __esm(() => {
|
|
|
15842
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."
|
|
15843
15851
|
},
|
|
15844
15852
|
action: { type: "string", description: "action: the InputMap action name." },
|
|
15845
|
-
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
|
+
},
|
|
15846
15857
|
strength: { type: "number", description: "action: 0 to 1. Default 1." },
|
|
15847
|
-
keycode: {
|
|
15858
|
+
keycode: {
|
|
15859
|
+
type: ["string", "number"],
|
|
15860
|
+
description: 'key: the key name, such as "Space" or "A", or its Godot keycode.'
|
|
15861
|
+
},
|
|
15848
15862
|
text: {
|
|
15849
15863
|
type: "string",
|
|
15850
15864
|
description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
|
|
@@ -15859,9 +15873,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
15859
15873
|
x: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
|
|
15860
15874
|
y: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
|
|
15861
15875
|
button: {
|
|
15862
|
-
type: "string",
|
|
15863
|
-
|
|
15864
|
-
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."
|
|
15865
15878
|
},
|
|
15866
15879
|
doubleClick: { type: "boolean", description: "click, mouse_click: default false." },
|
|
15867
15880
|
relativeX: { type: "number", description: "mouse_motion: movement since the last event." },
|
|
@@ -15877,8 +15890,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
15877
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",
|
|
15878
15891
|
requires: ["nodePath"]
|
|
15879
15892
|
},
|
|
15880
|
-
action: { summary: "press or
|
|
15881
|
-
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"] },
|
|
15882
15895
|
text: {
|
|
15883
15896
|
summary: "type a string wherever the focus is, a character at a time",
|
|
15884
15897
|
requires: ["text"]
|
|
@@ -35844,6 +35857,48 @@ import { basename as basename2, dirname as dirname9, join as join15, normalize a
|
|
|
35844
35857
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
35845
35858
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
35846
35859
|
import { promisify as promisify5 } from "node:util";
|
|
35860
|
+
function patienceForFrames(frames, atLeast) {
|
|
35861
|
+
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
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
|
+
}
|
|
35847
35902
|
function escapingPropertyValue(properties) {
|
|
35848
35903
|
if (typeof properties !== "object" || properties === null) {
|
|
35849
35904
|
return;
|
|
@@ -36182,6 +36237,10 @@ class GodotServer {
|
|
|
36182
36237
|
response: this.createErrorResponse(`${spec.name} does not take ${unknown.join(", ")}. It takes: ${[...known].join(", ")}.`)
|
|
36183
36238
|
};
|
|
36184
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
|
+
}
|
|
36185
36244
|
let op = null;
|
|
36186
36245
|
if (spec.operations) {
|
|
36187
36246
|
const valid = Object.keys(spec.operations);
|
|
@@ -36323,6 +36382,12 @@ class GodotServer {
|
|
|
36323
36382
|
});
|
|
36324
36383
|
case "find":
|
|
36325
36384
|
return await this.handleFindRuntimeNodes(args);
|
|
36385
|
+
case "text":
|
|
36386
|
+
return await this.handleRuntimeCommand("read_text", {
|
|
36387
|
+
projectPath: args["projectPath"],
|
|
36388
|
+
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
36389
|
+
include_hidden: readBoolean(args, "includeHidden") ?? false
|
|
36390
|
+
});
|
|
36326
36391
|
case "rect":
|
|
36327
36392
|
return await this.handleRuntimeCommand("get_rect", {
|
|
36328
36393
|
projectPath: args["projectPath"],
|
|
@@ -37367,8 +37432,11 @@ class GodotServer {
|
|
|
37367
37432
|
const nodePath = readNonEmptyString(args, "nodePath") ?? "";
|
|
37368
37433
|
const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
|
|
37369
37434
|
switch (op) {
|
|
37370
|
-
case "frames":
|
|
37371
|
-
|
|
37435
|
+
case "frames": {
|
|
37436
|
+
const frames = readPositiveNumber(args, "frames") ?? 1;
|
|
37437
|
+
const waited = patienceForFrames(frames, patience);
|
|
37438
|
+
return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
|
|
37439
|
+
}
|
|
37372
37440
|
case "signal":
|
|
37373
37441
|
return await this.handleRuntimeCommand("wait_signal", {
|
|
37374
37442
|
projectPath: args["projectPath"],
|
|
@@ -37391,7 +37459,7 @@ async function runGodotServer() {
|
|
|
37391
37459
|
const server = new GodotServer;
|
|
37392
37460
|
await server.run();
|
|
37393
37461
|
}
|
|
37394
|
-
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;
|
|
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;
|
|
37395
37463
|
var init_server3 = __esm(() => {
|
|
37396
37464
|
init_mcp();
|
|
37397
37465
|
init_stdio2();
|
|
@@ -37418,6 +37486,12 @@ var init_server3 = __esm(() => {
|
|
|
37418
37486
|
init_update_check();
|
|
37419
37487
|
run5 = promisify5(execFile5);
|
|
37420
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
|
+
};
|
|
37421
37495
|
PATH_SOLUTIONS = [
|
|
37422
37496
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
37423
37497
|
"Point projectPath at the project the file belongs to"
|
|
@@ -38660,6 +38734,15 @@ import { basename, dirname as dirname4, join as join9 } from "node:path";
|
|
|
38660
38734
|
|
|
38661
38735
|
// src/tool-reference.ts
|
|
38662
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
|
+
}
|
|
38663
38746
|
function renderToolsMarkdown() {
|
|
38664
38747
|
const lines = [
|
|
38665
38748
|
"# Tools",
|
|
@@ -38690,7 +38773,7 @@ function renderToolsMarkdown() {
|
|
|
38690
38773
|
if (parameters.length > 0) {
|
|
38691
38774
|
lines.push("Arguments:", "");
|
|
38692
38775
|
for (const [name, schema] of parameters) {
|
|
38693
|
-
const type =
|
|
38776
|
+
const type = namedType(schema["type"]);
|
|
38694
38777
|
const note = typeof schema["description"] === "string" ? ` ${schema["description"]}` : "";
|
|
38695
38778
|
lines.push(`- \`${name}\` (${type}):${note}`);
|
|
38696
38779
|
}
|
|
@@ -38740,12 +38823,14 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
38740
38823
|
|
|
38741
38824
|
| Ask | Tool |
|
|
38742
38825
|
| --- | --- |
|
|
38826
|
+
| What a screen says | \`runtime_inspect text\`, which reads what is drawn and leaves out what is hidden |
|
|
38743
38827
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
38744
38828
|
| What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
38745
38829
|
| Press a button | \`runtime_input click\`, which says what was under the pointer |
|
|
38746
38830
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` |
|
|
38747
38831
|
| Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
|
|
38748
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 |
|
|
38749
38834
|
| Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
|
|
38750
38835
|
| Wait for something | \`runtime_wait\`, never a sleep |
|
|
38751
38836
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|
|
@@ -57,6 +57,7 @@ func _init() -> void:
|
|
|
57
57
|
"ping": _ping,
|
|
58
58
|
"get_tree": _queries.get_tree,
|
|
59
59
|
"find_nodes": _queries.find_nodes,
|
|
60
|
+
"read_text": _queries.read_text,
|
|
60
61
|
"get_rect": _queries.get_rect,
|
|
61
62
|
"get_property": _queries.get_property,
|
|
62
63
|
"set_property": _queries.set_property,
|
|
@@ -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
|
|
|
@@ -334,6 +371,8 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
334
371
|
)
|
|
335
372
|
}
|
|
336
373
|
var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
|
|
374
|
+
if button < 0:
|
|
375
|
+
return _no_such_button(params.get("button"))
|
|
337
376
|
var double: bool = bool(params.get("double", false))
|
|
338
377
|
|
|
339
378
|
# Pushed into the viewport rather than through Input: Input accumulates events and flushes
|
|
@@ -594,6 +633,8 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
|
|
|
594
633
|
|
|
595
634
|
var position: Vector2 = viewport.get_final_transform() * aim
|
|
596
635
|
var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
|
|
636
|
+
if button < 0:
|
|
637
|
+
return _no_such_button(params.get("button"))
|
|
597
638
|
var double: bool = bool(params.get("double", false))
|
|
598
639
|
|
|
599
640
|
viewport.push_input(_motion(position, Vector2.ZERO))
|
|
@@ -642,6 +683,23 @@ func _button(position: Vector2, button: int, pressed: bool, double: bool) -> Inp
|
|
|
642
683
|
return event
|
|
643
684
|
|
|
644
685
|
|
|
686
|
+
## What to say about a name that is not a mouse button, with the ones that are.
|
|
687
|
+
func _no_such_button(named: Variant) -> Dictionary:
|
|
688
|
+
return {
|
|
689
|
+
"type": "error",
|
|
690
|
+
"message":
|
|
691
|
+
(
|
|
692
|
+
"%s is not a mouse button. It takes: left, right, middle, wheel_up, wheel_down, or the number of one."
|
|
693
|
+
% JSON.stringify(named)
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
## The button [param raw] names, or -1 for a name that is not one of them.
|
|
699
|
+
##
|
|
700
|
+
## A name nobody recognises used to come back as the left button, so a right click asked for by a
|
|
701
|
+
## spelling this does not know went to the left button and reported the left button. The caller
|
|
702
|
+
## reads the answer and believes it, which is the whole reason nothing here guesses.
|
|
645
703
|
func _resolve_mouse_button(raw: Variant) -> int:
|
|
646
704
|
if raw is String:
|
|
647
705
|
var named: String = raw
|
|
@@ -657,5 +715,5 @@ func _resolve_mouse_button(raw: Variant) -> int:
|
|
|
657
715
|
"wheel_down", "wheeldown":
|
|
658
716
|
return MOUSE_BUTTON_WHEEL_DOWN
|
|
659
717
|
_:
|
|
660
|
-
return
|
|
718
|
+
return -1
|
|
661
719
|
return int(raw)
|
|
@@ -10,6 +10,10 @@ const Values = preload("runtime_values.gd")
|
|
|
10
10
|
const FIND_LIMIT: int = 100
|
|
11
11
|
const FIND_LIMIT_CEILING: int = 1000
|
|
12
12
|
|
|
13
|
+
## The most lines one read answers with. A screen is a few dozen; a thousand is a tree somebody
|
|
14
|
+
## pointed this at by mistake.
|
|
15
|
+
const READ_LIMIT: int = 500
|
|
16
|
+
|
|
13
17
|
var _host: Node
|
|
14
18
|
var _values: Values
|
|
15
19
|
|
|
@@ -138,6 +142,60 @@ func _matches(
|
|
|
138
142
|
## is drawn in, in both the canvas coordinates the node reports and the window pixels input
|
|
139
143
|
## arrives in. The two differ whenever the project stretches its viewport, which is what made a
|
|
140
144
|
## rect unusable for a click.
|
|
145
|
+
## Every line of text under a node, in the order somebody reads the screen.
|
|
146
|
+
##
|
|
147
|
+
## The question a caller asks most often, and the one that cost the most to answer: reading a panel
|
|
148
|
+
## was a find for every label with the path of each beside it, a few hundred characters apiece to
|
|
149
|
+
## carry a sentence of six words. Long is the smaller half of it. A find answers off nodes nobody
|
|
150
|
+
## can see, and a panel keeps its empty state in the tree beside its rows, so "Nothing posted" came
|
|
151
|
+
## back next to the three things posted and was believed twice in one session.
|
|
152
|
+
##
|
|
153
|
+
## A hidden node is left out and so is everything under it, because what a player reads is what is
|
|
154
|
+
## drawn. [param include_hidden] asks for the lot instead, which is what a caller checking that
|
|
155
|
+
## something is not showing wants.
|
|
156
|
+
func read_text(params: Dictionary) -> Dictionary:
|
|
157
|
+
var root_path: String = str(params.get("root", "/root"))
|
|
158
|
+
var include_hidden: bool = bool(params.get("include_hidden", false))
|
|
159
|
+
|
|
160
|
+
var root: Node = _host.get_tree().root.get_node_or_null(root_path)
|
|
161
|
+
if root == null:
|
|
162
|
+
return {"type": "error", "message": "Node not found: " + root_path}
|
|
163
|
+
|
|
164
|
+
var lines: PackedStringArray = PackedStringArray()
|
|
165
|
+
_read_into(root, include_hidden, lines)
|
|
166
|
+
return {
|
|
167
|
+
"type": "text",
|
|
168
|
+
"root": root_path,
|
|
169
|
+
"lines": lines,
|
|
170
|
+
"count": lines.size(),
|
|
171
|
+
"truncated": lines.size() >= READ_LIMIT,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## Walks [param node] depth first, which is the order the screen is laid out in and the order a
|
|
176
|
+
## person reads it.
|
|
177
|
+
func _read_into(node: Node, include_hidden: bool, into: PackedStringArray) -> void:
|
|
178
|
+
if into.size() >= READ_LIMIT:
|
|
179
|
+
return
|
|
180
|
+
var control: CanvasItem = node as CanvasItem
|
|
181
|
+
if not include_hidden and control != null and not control.visible:
|
|
182
|
+
return
|
|
183
|
+
var said: String = _said_by(node)
|
|
184
|
+
if not said.is_empty():
|
|
185
|
+
into.append(said)
|
|
186
|
+
for child: Node in node.get_children():
|
|
187
|
+
_read_into(child, include_hidden, into)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
## What one node says, or "" for a node that says nothing. Anything with a `text` property, which
|
|
191
|
+
## is every label, button and field the interface is built out of.
|
|
192
|
+
static func _said_by(node: Node) -> String:
|
|
193
|
+
for property: Dictionary in node.get_property_list():
|
|
194
|
+
if str(property.get("name", "")) == "text" and int(property.get("type", 0)) == TYPE_STRING:
|
|
195
|
+
return str(node.get("text")).strip_edges()
|
|
196
|
+
return ""
|
|
197
|
+
|
|
198
|
+
|
|
141
199
|
func get_rect(params: Dictionary) -> Dictionary:
|
|
142
200
|
var node_path: String = str(params.get("path", ""))
|
|
143
201
|
if node_path.is_empty():
|
package/build/index.js
CHANGED
|
@@ -15311,7 +15311,7 @@ ${content.join(`
|
|
|
15311
15311
|
var version = {
|
|
15312
15312
|
major: 4,
|
|
15313
15313
|
minor: 6,
|
|
15314
|
-
patch:
|
|
15314
|
+
patch: 5
|
|
15315
15315
|
};
|
|
15316
15316
|
|
|
15317
15317
|
// node_modules/zod/v4/core/schemas.js
|
|
@@ -28298,12 +28298,12 @@ var TOOL_SPECS = [
|
|
|
28298
28298
|
},
|
|
28299
28299
|
{
|
|
28300
28300
|
name: "runtime_inspect",
|
|
28301
|
-
description: "Questions about the running game: the scene tree, the nodes matching a query, where one node is on screen, what one property reads, or the performance metrics. Needs the game running with the runtime addon.",
|
|
28301
|
+
description: "Questions about the running game: what is written on the screen, the scene tree, the nodes matching a query, where one node is on screen, what one property reads, or the performance metrics. Needs the game running with the runtime addon.",
|
|
28302
28302
|
parameters: {
|
|
28303
28303
|
projectPath: RUNNING_PROJECT_PATH,
|
|
28304
28304
|
nodePath: {
|
|
28305
28305
|
type: "string",
|
|
28306
|
-
description: "tree, find: where to start, default /root. rect: the node to place. property: the node to read."
|
|
28306
|
+
description: "tree, find, text: where to start, default /root. rect: the node to place. property: the node to read."
|
|
28307
28307
|
},
|
|
28308
28308
|
property: {
|
|
28309
28309
|
type: "string",
|
|
@@ -28325,6 +28325,10 @@ var TOOL_SPECS = [
|
|
|
28325
28325
|
},
|
|
28326
28326
|
group: { type: "string", description: "find: a group the node is in." },
|
|
28327
28327
|
limit: { type: "number", description: "find: the most nodes to answer with. Default 100." },
|
|
28328
|
+
includeHidden: {
|
|
28329
|
+
type: "boolean",
|
|
28330
|
+
description: "text: read hidden nodes as well, for checking that something is not showing. Default false."
|
|
28331
|
+
},
|
|
28328
28332
|
metrics: {
|
|
28329
28333
|
type: "array",
|
|
28330
28334
|
items: { type: "string" },
|
|
@@ -28334,6 +28338,10 @@ var TOOL_SPECS = [
|
|
|
28334
28338
|
requires: [],
|
|
28335
28339
|
operations: {
|
|
28336
28340
|
tree: { summary: "the live scene tree", requires: [] },
|
|
28341
|
+
text: {
|
|
28342
|
+
summary: "every line of text under nodePath, in the order somebody reads the screen, leaving out what is hidden and everything under it",
|
|
28343
|
+
requires: []
|
|
28344
|
+
},
|
|
28337
28345
|
find: {
|
|
28338
28346
|
summary: "the paths of every node matching className, script, namePattern or group, with property read off each",
|
|
28339
28347
|
requires: []
|
|
@@ -28396,9 +28404,15 @@ var TOOL_SPECS = [
|
|
|
28396
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."
|
|
28397
28405
|
},
|
|
28398
28406
|
action: { type: "string", description: "action: the InputMap action name." },
|
|
28399
|
-
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
|
+
},
|
|
28400
28411
|
strength: { type: "number", description: "action: 0 to 1. Default 1." },
|
|
28401
|
-
keycode: {
|
|
28412
|
+
keycode: {
|
|
28413
|
+
type: ["string", "number"],
|
|
28414
|
+
description: 'key: the key name, such as "Space" or "A", or its Godot keycode.'
|
|
28415
|
+
},
|
|
28402
28416
|
text: {
|
|
28403
28417
|
type: "string",
|
|
28404
28418
|
description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
|
|
@@ -28413,9 +28427,8 @@ var TOOL_SPECS = [
|
|
|
28413
28427
|
x: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
|
|
28414
28428
|
y: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
|
|
28415
28429
|
button: {
|
|
28416
|
-
type: "string",
|
|
28417
|
-
|
|
28418
|
-
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."
|
|
28419
28432
|
},
|
|
28420
28433
|
doubleClick: { type: "boolean", description: "click, mouse_click: default false." },
|
|
28421
28434
|
relativeX: { type: "number", description: "mouse_motion: movement since the last event." },
|
|
@@ -28431,8 +28444,8 @@ var TOOL_SPECS = [
|
|
|
28431
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",
|
|
28432
28445
|
requires: ["nodePath"]
|
|
28433
28446
|
},
|
|
28434
|
-
action: { summary: "press or
|
|
28435
|
-
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"] },
|
|
28436
28449
|
text: {
|
|
28437
28450
|
summary: "type a string wherever the focus is, a character at a time",
|
|
28438
28451
|
requires: ["text"]
|
|
@@ -28563,6 +28576,55 @@ var FEEDBACK_NOTICE_EVERY = 250;
|
|
|
28563
28576
|
var run3 = promisify3(execFile3);
|
|
28564
28577
|
var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
28565
28578
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
28579
|
+
var SLOWEST_FRAME_RATE = 20;
|
|
28580
|
+
function patienceForFrames(frames, atLeast) {
|
|
28581
|
+
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
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
|
+
}
|
|
28566
28628
|
var BRIDGE_RETRY_MS = 2000;
|
|
28567
28629
|
var PATH_SOLUTIONS = [
|
|
28568
28630
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
@@ -28955,6 +29017,10 @@ class GodotServer {
|
|
|
28955
29017
|
response: this.createErrorResponse(`${spec.name} does not take ${unknown.join(", ")}. It takes: ${[...known].join(", ")}.`)
|
|
28956
29018
|
};
|
|
28957
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
|
+
}
|
|
28958
29024
|
let op = null;
|
|
28959
29025
|
if (spec.operations) {
|
|
28960
29026
|
const valid = Object.keys(spec.operations);
|
|
@@ -29096,6 +29162,12 @@ class GodotServer {
|
|
|
29096
29162
|
});
|
|
29097
29163
|
case "find":
|
|
29098
29164
|
return await this.handleFindRuntimeNodes(args);
|
|
29165
|
+
case "text":
|
|
29166
|
+
return await this.handleRuntimeCommand("read_text", {
|
|
29167
|
+
projectPath: args["projectPath"],
|
|
29168
|
+
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
29169
|
+
include_hidden: readBoolean(args, "includeHidden") ?? false
|
|
29170
|
+
});
|
|
29099
29171
|
case "rect":
|
|
29100
29172
|
return await this.handleRuntimeCommand("get_rect", {
|
|
29101
29173
|
projectPath: args["projectPath"],
|
|
@@ -30140,8 +30212,11 @@ class GodotServer {
|
|
|
30140
30212
|
const nodePath = readNonEmptyString(args, "nodePath") ?? "";
|
|
30141
30213
|
const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
|
|
30142
30214
|
switch (op) {
|
|
30143
|
-
case "frames":
|
|
30144
|
-
|
|
30215
|
+
case "frames": {
|
|
30216
|
+
const frames = readPositiveNumber(args, "frames") ?? 1;
|
|
30217
|
+
const waited = patienceForFrames(frames, patience);
|
|
30218
|
+
return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
|
|
30219
|
+
}
|
|
30145
30220
|
case "signal":
|
|
30146
30221
|
return await this.handleRuntimeCommand("wait_signal", {
|
|
30147
30222
|
projectPath: args["projectPath"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
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",
|