gdharness 0.6.2 → 0.6.3
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
|
@@ -15834,12 +15834,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
15834
15834
|
},
|
|
15835
15835
|
{
|
|
15836
15836
|
name: "runtime_input",
|
|
15837
|
-
description: "Input to the running game: a whole click on a Control or a 3D node 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.",
|
|
15837
|
+
description: "Input to the running game: a whole click on a Control or a 3D node named by path, an item chosen out of a menu, 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.",
|
|
15838
15838
|
parameters: {
|
|
15839
15839
|
projectPath: RUNNING_PROJECT_PATH,
|
|
15840
15840
|
nodePath: {
|
|
15841
15841
|
type: "string",
|
|
15842
|
-
description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn."
|
|
15842
|
+
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
15843
|
},
|
|
15844
15844
|
action: { type: "string", description: "action: the InputMap action name." },
|
|
15845
15845
|
pressed: { type: "boolean", description: "Press or release. Default true." },
|
|
@@ -15847,7 +15847,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
15847
15847
|
keycode: { type: "string", description: 'key: the key name, such as "Space" or "A".' },
|
|
15848
15848
|
text: {
|
|
15849
15849
|
type: "string",
|
|
15850
|
-
description: "text: what to type. A newline is Enter and a tab is Tab."
|
|
15850
|
+
description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
|
|
15851
|
+
},
|
|
15852
|
+
index: {
|
|
15853
|
+
type: "number",
|
|
15854
|
+
description: "choose: the item to take, by where it is in the list, when text will not do."
|
|
15851
15855
|
},
|
|
15852
15856
|
shift: { type: "boolean" },
|
|
15853
15857
|
ctrl: { type: "boolean" },
|
|
@@ -15869,6 +15873,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15869
15873
|
summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the view moved. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press",
|
|
15870
15874
|
requires: ["nodePath"]
|
|
15871
15875
|
},
|
|
15876
|
+
choose: {
|
|
15877
|
+
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
|
+
requires: ["nodePath"]
|
|
15879
|
+
},
|
|
15872
15880
|
action: { summary: "press or release an action", requires: ["action"] },
|
|
15873
15881
|
key: { summary: "press or release a key", requires: ["keycode"] },
|
|
15874
15882
|
text: {
|
|
@@ -36307,12 +36315,23 @@ class GodotServer {
|
|
|
36307
36315
|
case "runtime_capture":
|
|
36308
36316
|
return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args);
|
|
36309
36317
|
case "runtime_input":
|
|
36310
|
-
|
|
36311
|
-
|
|
36312
|
-
|
|
36313
|
-
|
|
36314
|
-
|
|
36315
|
-
|
|
36318
|
+
if (op === "click") {
|
|
36319
|
+
return await this.handleRuntimeCommand("click", {
|
|
36320
|
+
projectPath: args["projectPath"],
|
|
36321
|
+
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
36322
|
+
button: readString(args, "button") ?? "left",
|
|
36323
|
+
double: readBoolean(args, "doubleClick") ?? false
|
|
36324
|
+
});
|
|
36325
|
+
}
|
|
36326
|
+
if (op === "choose") {
|
|
36327
|
+
return await this.handleRuntimeCommand("choose", {
|
|
36328
|
+
projectPath: args["projectPath"],
|
|
36329
|
+
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
36330
|
+
text: readString(args, "text") ?? "",
|
|
36331
|
+
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
36332
|
+
});
|
|
36333
|
+
}
|
|
36334
|
+
return await this.handleRuntimeCommand(`inject_${op}`, args);
|
|
36316
36335
|
case "runtime_wait":
|
|
36317
36336
|
return await this.handleRuntimeWait(op, args);
|
|
36318
36337
|
case "debug_breakpoint": {
|
|
@@ -38645,6 +38664,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
38645
38664
|
| Press a button | \`runtime_input click\`, which says what was under the pointer |
|
|
38646
38665
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` |
|
|
38647
38666
|
| Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
|
|
38667
|
+
| Pick something out of a dropdown | \`runtime_input choose\` on it, by what the item says |
|
|
38648
38668
|
| Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
|
|
38649
38669
|
| Wait for something | \`runtime_wait\`, never a sleep |
|
|
38650
38670
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|
|
@@ -70,6 +70,7 @@ func _init() -> void:
|
|
|
70
70
|
"inject_mouse_click": _input.inject_mouse_click,
|
|
71
71
|
"inject_mouse_motion": _input.inject_mouse_motion,
|
|
72
72
|
"click": _input.click,
|
|
73
|
+
"choose": _input.choose,
|
|
73
74
|
"wait_frames": _waits.wait_frames,
|
|
74
75
|
"wait_signal": _waits.wait_signal,
|
|
75
76
|
"wait_until": _waits.wait_until,
|
|
@@ -400,6 +400,148 @@ func _no_window_note(viewport: Viewport) -> String:
|
|
|
400
400
|
return ". This game has no window: run it with a window to reach this control"
|
|
401
401
|
|
|
402
402
|
|
|
403
|
+
## Chooses an item out of a menu, by what it says or by where it is in the list.
|
|
404
|
+
##
|
|
405
|
+
## A menu's items are drawn rather than built, so there is no node under the pointer to aim at and
|
|
406
|
+
## no rectangle to ask for: [PopupMenu] exposes their text, their ids and which one has the focus,
|
|
407
|
+
## and nothing about where any of them is. So a click cannot reach one, and a whole click on the
|
|
408
|
+
## [OptionButton] in front of it opens the menu on the press and closes it again on the release.
|
|
409
|
+
## Every language picker, every filter and every dropdown in a game was unreachable, and the way
|
|
410
|
+
## past it was to call `select` and emit `item_selected`, which sets a number and runs none of the
|
|
411
|
+
## engine's own path.
|
|
412
|
+
##
|
|
413
|
+
## Chosen the way a keyboard chooses: the item takes the focus and then Enter presses it, which is
|
|
414
|
+
## the same route through [PopupMenu] a pointer takes and which needs no geometry, so it works in a
|
|
415
|
+
## game with no window as well.
|
|
416
|
+
##
|
|
417
|
+
## [param path] may be the menu or the button in front of it. Naming the button is what a caller
|
|
418
|
+
## has, since the menu is an internal child with a generated name that changes between runs.
|
|
419
|
+
func choose(params: Dictionary) -> Dictionary:
|
|
420
|
+
var node_path: String = str(params.get("path", ""))
|
|
421
|
+
if node_path.is_empty():
|
|
422
|
+
return {"type": "error", "message": "Node path required"}
|
|
423
|
+
var node: Node = _host.get_tree().root.get_node_or_null(node_path)
|
|
424
|
+
if node == null:
|
|
425
|
+
return {"type": "error", "message": "Node not found: " + node_path}
|
|
426
|
+
|
|
427
|
+
var menu: PopupMenu = _menu_of(node)
|
|
428
|
+
if menu == null:
|
|
429
|
+
return {
|
|
430
|
+
"type": "error",
|
|
431
|
+
"message":
|
|
432
|
+
(
|
|
433
|
+
"%s is a %s, which is neither a PopupMenu nor something holding one"
|
|
434
|
+
% [node_path, node.get_class()]
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
var index: int = _wanted_item(menu, params)
|
|
439
|
+
if index < 0:
|
|
440
|
+
return {
|
|
441
|
+
"type": "error",
|
|
442
|
+
"message": "%s has no such item. It holds: %s" % [node_path, ", ".join(_items_of(menu))]
|
|
443
|
+
}
|
|
444
|
+
if menu.is_item_separator(index):
|
|
445
|
+
return {"type": "error", "message": "%s item %d is a separator, not a choice" % [node_path, index]}
|
|
446
|
+
if menu.is_item_disabled(index):
|
|
447
|
+
return {
|
|
448
|
+
"type": "error",
|
|
449
|
+
"message": "%s item %d, %s, is disabled" % [node_path, index, menu.get_item_text(index)]
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
# Shown first, because a menu nobody has opened has no focus to move and Enter would go to
|
|
453
|
+
# whatever is behind it. An OptionButton opens its own; a bare PopupMenu is popped where it
|
|
454
|
+
# already sits, which leaves a menu that was already open where it is.
|
|
455
|
+
var opened: bool = _open_the_menu(node, menu)
|
|
456
|
+
await _host.get_tree().process_frame
|
|
457
|
+
|
|
458
|
+
menu.scroll_to_item(index)
|
|
459
|
+
menu.set_focused_item(index)
|
|
460
|
+
# Through Input rather than pushed at the menu, which is how a keyboard reaches an open one: a
|
|
461
|
+
# popup is a Window, it takes the focus when it opens, and Input delivers to whichever window
|
|
462
|
+
# has it. Pushed straight at the menu the event arrived and nothing happened.
|
|
463
|
+
Input.parse_input_event(_accept(true))
|
|
464
|
+
await _host.get_tree().process_frame
|
|
465
|
+
Input.parse_input_event(_accept(false))
|
|
466
|
+
await _host.get_tree().process_frame
|
|
467
|
+
|
|
468
|
+
var answer: Dictionary = {
|
|
469
|
+
"type": "chosen",
|
|
470
|
+
"path": node_path,
|
|
471
|
+
"index": index,
|
|
472
|
+
"text": menu.get_item_text(index),
|
|
473
|
+
"id": menu.get_item_id(index),
|
|
474
|
+
"opened": opened,
|
|
475
|
+
"menu": str(menu.get_path()),
|
|
476
|
+
}
|
|
477
|
+
# What the button in front of the menu reads now, which is the answer to "did it take": a menu
|
|
478
|
+
# item that fired changes the thing holding it, and nothing else about the press says so.
|
|
479
|
+
var chooser: OptionButton = node as OptionButton
|
|
480
|
+
if chooser != null:
|
|
481
|
+
answer["selected"] = chooser.get_selected()
|
|
482
|
+
answer["shows"] = chooser.text
|
|
483
|
+
return answer
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
## The menu [param node] is, or the one it holds. An [OptionButton] and a [MenuButton] both keep
|
|
487
|
+
## theirs as an internal child, which is a node a caller cannot name and should not have to.
|
|
488
|
+
static func _menu_of(node: Node) -> PopupMenu:
|
|
489
|
+
var menu: PopupMenu = node as PopupMenu
|
|
490
|
+
if menu != null:
|
|
491
|
+
return menu
|
|
492
|
+
if node.has_method("get_popup"):
|
|
493
|
+
var held: Variant = node.call("get_popup")
|
|
494
|
+
if held is PopupMenu:
|
|
495
|
+
return held
|
|
496
|
+
return null
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
## Which item was asked for: `text`, matched exactly and then case-insensitively, or `index`.
|
|
500
|
+
## Minus one when neither names one that is there.
|
|
501
|
+
static func _wanted_item(menu: PopupMenu, params: Dictionary) -> int:
|
|
502
|
+
if params.has("index"):
|
|
503
|
+
var asked: int = int(params.get("index", -1))
|
|
504
|
+
return asked if asked >= 0 and asked < menu.get_item_count() else -1
|
|
505
|
+
var wanted: String = str(params.get("text", ""))
|
|
506
|
+
if wanted.is_empty():
|
|
507
|
+
return -1
|
|
508
|
+
for index: int in menu.get_item_count():
|
|
509
|
+
if menu.get_item_text(index) == wanted:
|
|
510
|
+
return index
|
|
511
|
+
for index: int in menu.get_item_count():
|
|
512
|
+
if menu.get_item_text(index).nocasecmp_to(wanted) == 0:
|
|
513
|
+
return index
|
|
514
|
+
return -1
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
## What the menu says, for a refusal that names the choices rather than the miss.
|
|
518
|
+
static func _items_of(menu: PopupMenu) -> PackedStringArray:
|
|
519
|
+
var said: PackedStringArray = PackedStringArray()
|
|
520
|
+
for index: int in menu.get_item_count():
|
|
521
|
+
said.append("%d: %s" % [index, menu.get_item_text(index)])
|
|
522
|
+
return said
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
## Opens the menu if it is not already, and answers whether anything opened.
|
|
526
|
+
static func _open_the_menu(node: Node, menu: PopupMenu) -> bool:
|
|
527
|
+
if menu.visible:
|
|
528
|
+
return false
|
|
529
|
+
if node.has_method("show_popup"):
|
|
530
|
+
node.call("show_popup")
|
|
531
|
+
return true
|
|
532
|
+
menu.popup()
|
|
533
|
+
return true
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
func _accept(pressed: bool) -> InputEventKey:
|
|
537
|
+
var event: InputEventKey = InputEventKey.new()
|
|
538
|
+
event.keycode = KEY_ENTER
|
|
539
|
+
event.physical_keycode = KEY_ENTER
|
|
540
|
+
event.key_label = KEY_ENTER
|
|
541
|
+
event.pressed = pressed
|
|
542
|
+
return event
|
|
543
|
+
|
|
544
|
+
|
|
403
545
|
## A whole click aimed at where a 3D node is drawn, for a game that picks with a ray out of the
|
|
404
546
|
## cursor rather than with a Control.
|
|
405
547
|
##
|
package/build/index.js
CHANGED
|
@@ -28348,12 +28348,12 @@ var TOOL_SPECS = [
|
|
|
28348
28348
|
},
|
|
28349
28349
|
{
|
|
28350
28350
|
name: "runtime_input",
|
|
28351
|
-
description: "Input to the running game: a whole click on a Control or a 3D node 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.",
|
|
28351
|
+
description: "Input to the running game: a whole click on a Control or a 3D node named by path, an item chosen out of a menu, 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.",
|
|
28352
28352
|
parameters: {
|
|
28353
28353
|
projectPath: RUNNING_PROJECT_PATH,
|
|
28354
28354
|
nodePath: {
|
|
28355
28355
|
type: "string",
|
|
28356
|
-
description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn."
|
|
28356
|
+
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."
|
|
28357
28357
|
},
|
|
28358
28358
|
action: { type: "string", description: "action: the InputMap action name." },
|
|
28359
28359
|
pressed: { type: "boolean", description: "Press or release. Default true." },
|
|
@@ -28361,7 +28361,11 @@ var TOOL_SPECS = [
|
|
|
28361
28361
|
keycode: { type: "string", description: 'key: the key name, such as "Space" or "A".' },
|
|
28362
28362
|
text: {
|
|
28363
28363
|
type: "string",
|
|
28364
|
-
description: "text: what to type. A newline is Enter and a tab is Tab."
|
|
28364
|
+
description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
|
|
28365
|
+
},
|
|
28366
|
+
index: {
|
|
28367
|
+
type: "number",
|
|
28368
|
+
description: "choose: the item to take, by where it is in the list, when text will not do."
|
|
28365
28369
|
},
|
|
28366
28370
|
shift: { type: "boolean" },
|
|
28367
28371
|
ctrl: { type: "boolean" },
|
|
@@ -28383,6 +28387,10 @@ var TOOL_SPECS = [
|
|
|
28383
28387
|
summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the view moved. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press",
|
|
28384
28388
|
requires: ["nodePath"]
|
|
28385
28389
|
},
|
|
28390
|
+
choose: {
|
|
28391
|
+
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",
|
|
28392
|
+
requires: ["nodePath"]
|
|
28393
|
+
},
|
|
28386
28394
|
action: { summary: "press or release an action", requires: ["action"] },
|
|
28387
28395
|
key: { summary: "press or release a key", requires: ["keycode"] },
|
|
28388
28396
|
text: {
|
|
@@ -29080,12 +29088,23 @@ class GodotServer {
|
|
|
29080
29088
|
case "runtime_capture":
|
|
29081
29089
|
return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args);
|
|
29082
29090
|
case "runtime_input":
|
|
29083
|
-
|
|
29084
|
-
|
|
29085
|
-
|
|
29086
|
-
|
|
29087
|
-
|
|
29088
|
-
|
|
29091
|
+
if (op === "click") {
|
|
29092
|
+
return await this.handleRuntimeCommand("click", {
|
|
29093
|
+
projectPath: args["projectPath"],
|
|
29094
|
+
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
29095
|
+
button: readString(args, "button") ?? "left",
|
|
29096
|
+
double: readBoolean(args, "doubleClick") ?? false
|
|
29097
|
+
});
|
|
29098
|
+
}
|
|
29099
|
+
if (op === "choose") {
|
|
29100
|
+
return await this.handleRuntimeCommand("choose", {
|
|
29101
|
+
projectPath: args["projectPath"],
|
|
29102
|
+
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
29103
|
+
text: readString(args, "text") ?? "",
|
|
29104
|
+
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
29105
|
+
});
|
|
29106
|
+
}
|
|
29107
|
+
return await this.handleRuntimeCommand(`inject_${op}`, args);
|
|
29089
29108
|
case "runtime_wait":
|
|
29090
29109
|
return await this.handleRuntimeWait(op, args);
|
|
29091
29110
|
case "debug_breakpoint": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
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",
|