gdharness 1.2.33 → 1.2.34

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.
@@ -170,19 +170,63 @@ func add_input_action(params: Dictionary) -> Dictionary:
170
170
  if action.is_empty():
171
171
  return {}
172
172
 
173
+ # An action that exists is rebound, which is how one of the engine's ui_ actions is changed, so
174
+ # what it held is answered rather than lost without a word.
175
+ var setting: String = "input/" + action_name
176
+ var answer: Dictionary = {
177
+ "action_name": action_name,
178
+ "events_count": events_config.size(),
179
+ "deadzone": deadzone,
180
+ "events": events_config,
181
+ "was_new": not ProjectSettings.has_setting(setting),
182
+ }
183
+ if not answer["was_new"]:
184
+ var held: Variant = ProjectSettings.get_setting(setting)
185
+ var old: Dictionary = held if held is Dictionary else {}
186
+ var old_events: Array[Dictionary] = []
187
+ var listed: Variant = old.get("events", [])
188
+ if listed is Array:
189
+ var events_held: Array = listed
190
+ for event: Variant in events_held:
191
+ if event is InputEvent:
192
+ var bound: InputEvent = event
193
+ old_events.append(_shaped(bound))
194
+ answer["old_events"] = old_events
195
+ answer["old_deadzone"] = Read.as_float(old.get("deadzone", 0.5), 0.5)
196
+
173
197
  # Through ProjectSettings so the file is saved the way the editor saves it, header and
174
198
  # every other line kept; a ConfigFile of project.godot drops the comments on the way out.
175
- ProjectSettings.set_setting("input/" + action_name, action)
199
+ ProjectSettings.set_setting(setting, action)
176
200
  var err: Error = ProjectFile.save()
177
201
  if err != OK:
178
202
  return _log.failure("Failed to save project.godot: " + error_string(err))
179
203
 
180
- return {
181
- "action_name": action_name,
182
- "events_count": events_config.size(),
183
- "deadzone": deadzone,
184
- "events": events_config
185
- }
204
+ return answer
205
+
206
+
207
+ ## [param event] in the form add_input_action answers its events with, and as the engine names it
208
+ ## under text, which also covers a kind of event that form has no fields for.
209
+ static func _shaped(event: InputEvent) -> Dictionary:
210
+ var shaped: Dictionary = {"class_name": event.get_class(), "text": event.as_text()}
211
+ if event is InputEventKey:
212
+ var key: InputEventKey = event
213
+ shaped["keycode"] = key.keycode
214
+ if key.physical_keycode != KEY_NONE:
215
+ shaped["physical_keycode"] = key.physical_keycode
216
+ for modifier: String in ["ctrl_pressed", "alt_pressed", "shift_pressed"]:
217
+ if key.get(modifier):
218
+ shaped[modifier] = true
219
+ elif event is InputEventMouseButton:
220
+ var mouse: InputEventMouseButton = event
221
+ shaped["button_index"] = mouse.button_index
222
+ elif event is InputEventJoypadButton:
223
+ var pad: InputEventJoypadButton = event
224
+ shaped["button_index"] = pad.button_index
225
+ elif event is InputEventJoypadMotion:
226
+ var motion: InputEventJoypadMotion = event
227
+ shaped["axis"] = motion.axis
228
+ shaped["axis_value"] = motion.axis_value
229
+ return shaped
186
230
 
187
231
 
188
232
  # Remove an input action the project defines, which add_input_action had no way back from.
package/build/index.js CHANGED
@@ -25071,6 +25071,12 @@ function isWithinRoot(rootPath, candidatePath) {
25071
25071
  const step = relative(resolve(rootPath), resolve(candidatePath));
25072
25072
  return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
25073
25073
  }
25074
+ var KEPT_ID = /^[A-Za-z0-9_]+$/;
25075
+ function keptInside(given) {
25076
+ const at = given.indexOf("::");
25077
+ const inner = at === -1 ? "" : given.slice(at + 2);
25078
+ return KEPT_ID.test(inner) ? { file: given.slice(0, at), inner } : { file: given, inner: undefined };
25079
+ }
25074
25080
  function resolveWithinProject(projectPath, candidatePath, options = {}) {
25075
25081
  const trimmed = candidatePath.trim();
25076
25082
  if (trimmed === "") {
@@ -25099,6 +25105,13 @@ function resolveWithinProject(projectPath, candidatePath, options = {}) {
25099
25105
  reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
25100
25106
  };
25101
25107
  }
25108
+ const kept = keptInside(trimmed);
25109
+ if (kept.inner !== undefined) {
25110
+ return {
25111
+ ok: false,
25112
+ reason: `Path '${trimmed}' names the resource ${kept.inner} kept inside ${kept.file}, not a file. resource_edit read reads one by that path; this argument takes a file.`
25113
+ };
25114
+ }
25102
25115
  if (withoutScheme.includes(":")) {
25103
25116
  return {
25104
25117
  ok: false,
@@ -35040,7 +35053,7 @@ var TOOL_SPECS = [
35040
35053
  {
35041
35054
  name: "project_settings",
35042
35055
  title: "Project settings",
35043
- description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change touches only its own lines. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
35056
+ description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events and deadzone, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change touches only its own lines. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
35044
35057
  parameters: {
35045
35058
  projectPath: PROJECT_PATH2,
35046
35059
  setting: {
@@ -35138,8 +35151,8 @@ var TOOL_SPECS = [
35138
35151
  requires: ["scenePath"]
35139
35152
  },
35140
35153
  add_input_action: {
35141
- effect: "additive",
35142
- summary: "register an input action and its events",
35154
+ effect: "destructive",
35155
+ summary: "register an input action and its events, or rebind one that exists",
35143
35156
  requires: ["actionName", "events"]
35144
35157
  },
35145
35158
  remove_input_action: {
@@ -35487,7 +35500,7 @@ var TOOL_SPECS = [
35487
35500
  {
35488
35501
  name: "scene_animation",
35489
35502
  title: "Animations",
35490
- description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. Needs the editor connected.",
35503
+ description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write need the editor connected.",
35491
35504
  parameters: {
35492
35505
  projectPath: PROJECT_PATH2,
35493
35506
  scenePath: SCENE_PATH,
@@ -35519,7 +35532,11 @@ var TOOL_SPECS = [
35519
35532
  ops: ["read_states", "add_state", "connect_states"],
35520
35533
  description: "The AnimationTree node."
35521
35534
  },
35522
- stateName: { type: "string", ops: ["add_state"], description: "add_state: the state to add." },
35535
+ stateName: {
35536
+ type: "string",
35537
+ ops: ["add_state"],
35538
+ description: "add_state: the state to add. It goes right of the rightmost state in the editor's graph, answered under position."
35539
+ },
35523
35540
  stateMachinePath: {
35524
35541
  type: "string",
35525
35542
  ops: ["read_states", "add_state", "connect_states"],
@@ -35694,7 +35711,7 @@ var TOOL_SPECS = [
35694
35711
  projectPath: PROJECT_PATH2,
35695
35712
  resourcePath: {
35696
35713
  ...RESOURCE_PATH,
35697
- description: `${RESOURCE_FILE} move: a directory too.`
35714
+ description: `${RESOURCE_FILE} move: a directory too. read: also a resource kept inside a scene or resource file, by the path a read answers a property holding it with, such as "res://hero.tscn::Animation_jump".`
35698
35715
  },
35699
35716
  resourceType: {
35700
35717
  type: "string",
@@ -38950,7 +38967,11 @@ class GodotServer {
38950
38967
  case "scene_create":
38951
38968
  return op === "create" ? await bridge("create_scene") : await bridge("save_scene");
38952
38969
  case "scene_tree":
38953
- return await this.readFromFile(args, "scenePath", "read_scene_tree", async () => await bridge("list_scene_nodes"));
38970
+ return await this.readFromFile(args, "scenePath", "read_scene_tree", (file) => ({
38971
+ scenePath: file,
38972
+ depth: readNumber(args, "depth") ?? -1,
38973
+ includeProperties: readBoolean(args, "includeProperties") ?? false
38974
+ }), async () => await bridge("list_scene_nodes"));
38954
38975
  case "scene_node":
38955
38976
  switch (op) {
38956
38977
  case "add":
@@ -38980,9 +39001,17 @@ class GodotServer {
38980
39001
  case "scene_animation":
38981
39002
  switch (op) {
38982
39003
  case "read":
38983
- return await bridge("read_animations");
39004
+ return await this.readFromFile(args, "scenePath", "read_animations", (file) => ({
39005
+ scenePath: file,
39006
+ playerNodePath: readNonEmptyString(args, "playerNodePath") ?? ".",
39007
+ animationName: readString(args, "animationName") ?? ""
39008
+ }), async () => await bridge("read_animations"));
38984
39009
  case "read_states":
38985
- return await bridge("read_animation_states");
39010
+ return await this.readFromFile(args, "scenePath", "read_animation_states", (file) => ({
39011
+ scenePath: file,
39012
+ animTreePath: readString(args, "animTreePath") ?? "",
39013
+ stateMachinePath: readString(args, "stateMachinePath") ?? ""
39014
+ }), async () => await bridge("read_animation_states"));
38986
39015
  case "create":
38987
39016
  return await bridge("create_animation");
38988
39017
  case "add_track":
@@ -39009,8 +39038,14 @@ class GodotServer {
39009
39038
  switch (op) {
39010
39039
  case "move":
39011
39040
  return await this.handleMoveFile(args);
39012
- case "read":
39013
- return await this.readFromFile(args, "resourcePath", "read_resource", async () => await bridge("read_resource"));
39041
+ case "read": {
39042
+ const { file, inner } = keptInside(readString(args, "resourcePath") ?? "");
39043
+ const kept = inner === undefined ? {} : { resourcePath: file, subResource: inner };
39044
+ return await this.readFromFile({ ...args, ...kept }, "resourcePath", "read_resource", (found) => ({
39045
+ resourcePath: inner === undefined ? found : `${found}::${inner}`,
39046
+ includeDefaults: readBoolean(args, "includeDefaults") ?? false
39047
+ }), async () => await bridge("read_resource", kept));
39048
+ }
39014
39049
  case "create":
39015
39050
  return await bridge("create_resource");
39016
39051
  case "modify":
@@ -43400,7 +43435,7 @@ class GodotServer {
43400
43435
  ...leftChanged === undefined ? {} : { note: leftChanged }
43401
43436
  });
43402
43437
  }
43403
- async readFromFile(args, pathKey, operation, throughTheEditor) {
43438
+ async readFromFile(args, pathKey, operation, paramsFor, throughTheEditor) {
43404
43439
  const project = this.project(args);
43405
43440
  if (!project.ok) {
43406
43441
  return project.response;
@@ -43418,12 +43453,7 @@ class GodotServer {
43418
43453
  if (file === null) {
43419
43454
  return this.createErrorResponse(`${pathKey} must name a file inside the project.`);
43420
43455
  }
43421
- const params = operation === "read_scene_tree" ? {
43422
- scenePath: file,
43423
- depth: readNumber(args, "depth") ?? -1,
43424
- includeProperties: readBoolean(args, "includeProperties") ?? false
43425
- } : { resourcePath: file, includeDefaults: readBoolean(args, "includeDefaults") ?? false };
43426
- const outcome = await this.operation(operation, params, projectPath);
43456
+ const outcome = await this.operation(operation, paramsFor(file), projectPath);
43427
43457
  if (!outcome.ok) {
43428
43458
  return this.answer(outcome);
43429
43459
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.33",
3
+ "version": "1.2.34",
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",