gdharness 0.13.44 → 1.0.1

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/README.md CHANGED
@@ -26,9 +26,8 @@ property reads right now, what the console printed, what broke and on which line
26
26
  Hand this to your agent:
27
27
 
28
28
  ```text
29
- Install gdharness into this project by following
30
- https://aureliolo.github.io/gdharness/agent.md, then tell me what it asked you to
31
- recommend back to me.
29
+ Install gdharness here by following https://aureliolo.github.io/gdharness/agent.md,
30
+ then tell me what it asked you to recommend back to me.
32
31
  ```
33
32
 
34
33
  Or do it yourself, and pick your harness:
@@ -53,16 +52,16 @@ emptily, so a call that succeeded is a call that was understood.
53
52
 
54
53
  ## At a glance
55
54
 
56
- | | |
57
- | --------- | -------------------------------------------------------------------------------- |
58
- | Needs | Godot 4.7 or newer, Node 22 or newer. Runs under Bun 1.4 too. |
59
- | Surface | 30 tools named `domain_verb`, and 4 `godot://` resources |
60
- | Reaches | The editor that is open, a game it is playing, and the project on disk |
61
- | Harnesses | 35, written inside the project wherever the harness has a project config |
62
- | Skill | Written to `.agents/skills`, which every major harness reads |
63
- | Install | npm, or a Sigstore-signed archive with an SBOM, SLSA Build Level 3 |
64
- | Proven | Every tool driven against Godot 4.7.2 in CI, on Windows, Linux and macOS |
65
- | Status | 0.x: pin an exact version. [What a bump means](.github/CONTRIBUTING.md#versions) |
55
+ | | |
56
+ | --------- | ------------------------------------------------------------------------------------ |
57
+ | Needs | Godot 4.7 or newer, Node 22 or newer. Runs under Bun 1.4 too. |
58
+ | Surface | 30 tools named `domain_verb`, and 4 `godot://` resources |
59
+ | Reaches | The editor that is open, a game it is playing, and the project on disk |
60
+ | Harnesses | 35, written inside the project wherever the harness has a project config |
61
+ | Skill | Written to `.agents/skills`, which every major harness reads |
62
+ | Install | npm, or a Sigstore-signed archive with an SBOM, SLSA Build Level 3 |
63
+ | Proven | Every tool driven against Godot 4.7.2 in CI, on Windows, Linux and macOS |
64
+ | Status | 1.0: every release is a patch. [What a bump means](.github/CONTRIBUTING.md#versions) |
66
65
 
67
66
  Older 4.x is likely to work and is not tested.
68
67
 
package/build/cli.js CHANGED
@@ -15319,10 +15319,22 @@ function opTakes(spec, op, name) {
15319
15319
  function argumentsOf(spec, op) {
15320
15320
  return Object.keys(spec.parameters).filter((name) => opTakes(spec, op, name));
15321
15321
  }
15322
- function toolsWithoutProjectPath() {
15323
- return TOOL_SPECS.filter((spec) => !Object.hasOwn(spec.parameters, "projectPath")).map((spec) => spec.name).sort();
15322
+ function callsWithoutProjectPath() {
15323
+ const calls = [];
15324
+ for (const spec of TOOL_SPECS) {
15325
+ if (!Object.hasOwn(spec.parameters, "projectPath")) {
15326
+ calls.push(spec.name);
15327
+ continue;
15328
+ }
15329
+ for (const op of Object.keys(spec.operations ?? {})) {
15330
+ if (!opTakes(spec, op, "projectPath")) {
15331
+ calls.push(`${spec.name} ${op}`);
15332
+ }
15333
+ }
15334
+ }
15335
+ return calls.sort();
15324
15336
  }
15325
- function projectPathSentence(without = toolsWithoutProjectPath()) {
15337
+ function projectPathSentence(without = callsWithoutProjectPath()) {
15326
15338
  if (without.length === 0) {
15327
15339
  return "Every call takes `projectPath`";
15328
15340
  }
@@ -16327,12 +16339,12 @@ var init_tool_definitions = __esm(() => {
16327
16339
  relativeX: {
16328
16340
  type: "number",
16329
16341
  ops: ["mouse_motion"],
16330
- description: "mouse_motion: movement since the last event."
16342
+ description: "mouse_motion: the movement the event carries. Left out, it is the distance from where the pointer last was, which is what a control that drags reads; give it to send a motion the position does not show."
16331
16343
  },
16332
16344
  relativeY: {
16333
16345
  type: "number",
16334
16346
  ops: ["mouse_motion"],
16335
- description: "mouse_motion: movement since the last event."
16347
+ description: "mouse_motion: as relativeX."
16336
16348
  }
16337
16349
  },
16338
16350
  requires: [],
@@ -16346,13 +16358,22 @@ var init_tool_definitions = __esm(() => {
16346
16358
  requires: ["nodePath"]
16347
16359
  },
16348
16360
  action: { summary: "press an action, or hold it", requires: ["action"] },
16349
- key: { summary: "press a key, or hold it", requires: ["keycode"] },
16361
+ key: {
16362
+ summary: "press a key, or hold it. Not for a menu a click has opened: an OptionButton or PopupMenu pops up as a window of its own that a key sent to the game never reaches, so an arrow moves nothing in it and Enter closes it with nothing chosen. choose is what selects there",
16363
+ requires: ["keycode"]
16364
+ },
16350
16365
  text: {
16351
16366
  summary: "type a string into the field being edited, a character at a time, and say what it landed in",
16352
16367
  requires: ["text"]
16353
16368
  },
16354
- mouse_click: { summary: "one mouse button event at a position", requires: ["x", "y"] },
16355
- mouse_motion: { summary: "move the mouse to a position", requires: ["x", "y"] }
16369
+ mouse_click: {
16370
+ summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false",
16371
+ requires: ["x", "y"]
16372
+ },
16373
+ mouse_motion: {
16374
+ summary: "move the mouse to a position, carrying the distance from where it last was unless relativeX and relativeY say otherwise, and the buttons held, so a control that drags moves under a run of these between a held mouse_click and its release",
16375
+ requires: ["x", "y"]
16376
+ }
16356
16377
  }
16357
16378
  },
16358
16379
  {
@@ -42068,7 +42089,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
42068
42089
  | Press the next thing after a click that changed screen | \`runtime_wait until\` with \`says\` and a word the new screen shows, then the click. A click sent straight after a screen change lands on a control the new screen is about to free, answers \`landed: true\`, and does nothing; a frame count is a number nobody can pick correctly across a cold start and a warm one, and waiting for the words answered in about 130ms where twenty frames had not been enough |
42069
42090
  | Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
42070
42091
  | Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
42071
- | Pick something out of a dropdown | \`runtime_input choose\` on it, by what the item says |
42092
+ | Pick something out of a dropdown | \`runtime_input choose\` on it, by what the item says, rather than a click and keys: the menu opens as a window of its own that an injected key never reaches, so Enter closes it with nothing chosen |
42072
42093
  | Answer a dialog | \`runtime_input click\` on its button, or \`key\` Escape to dismiss it: an [AcceptDialog] reads that key itself and never asks the InputMap, so \`ui_cancel\` leaves it standing |
42073
42094
  | Press a bound key | \`runtime_input action\` or \`key\`, a whole press unless you hold it |
42074
42095
  | Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
@@ -341,15 +341,29 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
341
341
  }
342
342
 
343
343
 
344
+ ## Moves the pointer to a position, with the movement the event carries taken from where the
345
+ ## pointer last was unless the caller says otherwise.
346
+ ##
347
+ ## A real pointer never arrives without a `relative`, and a control that drags reads that field
348
+ ## rather than the position, so a motion carrying none leaves a grip where it was however far the
349
+ ## position moved. The viewport remembers where the last motion put the pointer, injected or not,
350
+ ## so the difference from there is what a real move would have carried. A relative the caller gives
351
+ ## is kept, since a game may want to read a motion the position does not show.
344
352
  func inject_mouse_motion(params: Dictionary) -> Dictionary:
345
353
  var point: Variant = _read_point(params, "x", "y", "position")
346
354
  if point is String:
347
355
  return {"type": "error", "message": point}
348
356
  var position: Vector2 = point
349
- var movement: Variant = _read_point(params, "relativeX", "relativeY", "relative")
350
- if movement is String:
351
- return {"type": "error", "message": movement}
352
- var relative: Vector2 = movement
357
+ var relative: Vector2 = position - _host.get_viewport().get_mouse_position()
358
+ if params.has("relativeX") or params.has("relativeY"):
359
+ relative = Vector2(
360
+ Read.as_float(params.get("relativeX", 0.0)), Read.as_float(params.get("relativeY", 0.0))
361
+ )
362
+ elif params.has("relative"):
363
+ var movement: Variant = _read_point(params, "relativeX", "relativeY", "relative")
364
+ if movement is String:
365
+ return {"type": "error", "message": movement}
366
+ relative = movement
353
367
 
354
368
  Input.parse_input_event(_motion(position, relative))
355
369
 
@@ -790,14 +804,21 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
790
804
  }
791
805
 
792
806
 
807
+ ## The buttons a real pointer event carries are the ones held as it happens, and Input keeps that
808
+ ## from every button event it is given, injected ones included, so a motion between a held click
809
+ ## and its release is a drag to a control that reads the mask rather than remembering the click.
793
810
  func _motion(position: Vector2, relative: Vector2) -> InputEventMouseMotion:
794
811
  var event: InputEventMouseMotion = InputEventMouseMotion.new()
795
812
  event.position = position
796
813
  event.global_position = position
797
814
  event.relative = relative
815
+ event.screen_relative = relative
816
+ event.button_mask = Input.get_mouse_button_mask()
798
817
  return event
799
818
 
800
819
 
820
+ ## A button event carries the buttons held once it has happened: the pressed one among them, the
821
+ ## released one out. A wheel step is a button with no mask, as it is from a real mouse.
801
822
  func _button(position: Vector2, button: int, pressed: bool, double: bool) -> InputEventMouseButton:
802
823
  var event: InputEventMouseButton = InputEventMouseButton.new()
803
824
  event.position = position
@@ -805,9 +826,20 @@ func _button(position: Vector2, button: int, pressed: bool, double: bool) -> Inp
805
826
  event.button_index = button as MouseButton
806
827
  event.pressed = pressed
807
828
  event.double_click = double
829
+ var held: int = Input.get_mouse_button_mask()
830
+ var own: int = _mask_of(button)
831
+ event.button_mask = (held | own) if pressed else (held & ~own)
808
832
  return event
809
833
 
810
834
 
835
+ ## The mask bit for a button, which is the engine's own mapping: left 1, right 2, middle 4, the
836
+ ## two extra buttons 128 and 256. The wheel steps between have none.
837
+ func _mask_of(button: int) -> int:
838
+ if button >= MOUSE_BUTTON_WHEEL_UP and button <= MOUSE_BUTTON_WHEEL_RIGHT:
839
+ return 0
840
+ return 1 << (button - 1)
841
+
842
+
811
843
  ## What to say about a name that is not a mouse button, with the ones that are.
812
844
  func _no_such_button(named: Variant) -> Dictionary:
813
845
  return {
@@ -247,11 +247,27 @@ func _matches_apart_from_name(node: Node, wanted: Dictionary[String, String]) ->
247
247
  if not script is Script:
248
248
  return false
249
249
  var attached: Script = script
250
- if attached.get_global_name() != wanted["class"]:
250
+ if not _script_is(attached, wanted["class"]):
251
251
  return false
252
252
  return true
253
253
 
254
254
 
255
+ ## Whether [param attached] is the script class [param wanted], the way a typed `is` reads it: its
256
+ ## own class_name, or one it extends at any distance.
257
+ ##
258
+ ## The class_name alone answered 0 for `Card` over a tree of rows whose scripts extend Card two
259
+ ## steps down, IntakeRow extends DocketCard extends Card, while `is_class` reaches every native
260
+ ## subclass. A caller naming a class means what extends it, whichever side of the line the class
261
+ ## is declared on.
262
+ static func _script_is(attached: Script, wanted: String) -> bool:
263
+ var walk: Script = attached
264
+ while walk != null:
265
+ if walk.get_global_name() == wanted:
266
+ return true
267
+ walk = walk.get_base_script()
268
+ return false
269
+
270
+
255
271
  ## Where a node is on screen: a Control's rectangle, a Node2D's position, or the place a 3D node
256
272
  ## is drawn in, in both the canvas coordinates the node reports and the window pixels input
257
273
  ## arrives in. The two differ whenever the project stretches its viewport, which is what made a
package/build/index.js CHANGED
@@ -29942,12 +29942,12 @@ var TOOL_SPECS = [
29942
29942
  relativeX: {
29943
29943
  type: "number",
29944
29944
  ops: ["mouse_motion"],
29945
- description: "mouse_motion: movement since the last event."
29945
+ description: "mouse_motion: the movement the event carries. Left out, it is the distance from where the pointer last was, which is what a control that drags reads; give it to send a motion the position does not show."
29946
29946
  },
29947
29947
  relativeY: {
29948
29948
  type: "number",
29949
29949
  ops: ["mouse_motion"],
29950
- description: "mouse_motion: movement since the last event."
29950
+ description: "mouse_motion: as relativeX."
29951
29951
  }
29952
29952
  },
29953
29953
  requires: [],
@@ -29961,13 +29961,22 @@ var TOOL_SPECS = [
29961
29961
  requires: ["nodePath"]
29962
29962
  },
29963
29963
  action: { summary: "press an action, or hold it", requires: ["action"] },
29964
- key: { summary: "press a key, or hold it", requires: ["keycode"] },
29964
+ key: {
29965
+ summary: "press a key, or hold it. Not for a menu a click has opened: an OptionButton or PopupMenu pops up as a window of its own that a key sent to the game never reaches, so an arrow moves nothing in it and Enter closes it with nothing chosen. choose is what selects there",
29966
+ requires: ["keycode"]
29967
+ },
29965
29968
  text: {
29966
29969
  summary: "type a string into the field being edited, a character at a time, and say what it landed in",
29967
29970
  requires: ["text"]
29968
29971
  },
29969
- mouse_click: { summary: "one mouse button event at a position", requires: ["x", "y"] },
29970
- mouse_motion: { summary: "move the mouse to a position", requires: ["x", "y"] }
29972
+ mouse_click: {
29973
+ summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false",
29974
+ requires: ["x", "y"]
29975
+ },
29976
+ mouse_motion: {
29977
+ summary: "move the mouse to a position, carrying the distance from where it last was unless relativeX and relativeY say otherwise, and the buttons held, so a control that drags moves under a run of these between a held mouse_click and its release",
29978
+ requires: ["x", "y"]
29979
+ }
29971
29980
  }
29972
29981
  },
29973
29982
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.13.44",
3
+ "version": "1.0.1",
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",