Replace dasher-pack with unified animation-pack using original Blender bone names
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
@tool
|
||||
class_name McpScenePath
|
||||
extends RefCounted
|
||||
|
||||
const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
|
||||
|
||||
## Utility for converting between Godot internal node paths and clean
|
||||
## scene-relative paths like /Main/Camera3D.
|
||||
|
||||
|
||||
## Return a clean path relative to the scene root (e.g. /Main/Camera3D).
|
||||
## Returns "" when `node` is not the scene root or a descendant of it —
|
||||
## without the ancestry guard, get_path_to() returns an empty NodePath that
|
||||
## concatenates into a plausible-looking but invalid "/Main/".
|
||||
static func from_node(node: Node, scene_root: Node) -> String:
|
||||
if scene_root == null or node == null:
|
||||
return ""
|
||||
if node == scene_root:
|
||||
return "/" + scene_root.name
|
||||
if not scene_root.is_ancestor_of(node):
|
||||
return ""
|
||||
var relative := scene_root.get_path_to(node)
|
||||
return "/" + scene_root.name + "/" + str(relative)
|
||||
|
||||
|
||||
## Resolve a clean scene path like "/Main/Camera3D" to the actual node.
|
||||
##
|
||||
## Accepts forms relative to the edited scene root:
|
||||
## "/Main" — explicit root prefix (canonical)
|
||||
## "/Main/Camera3D" — descendant path
|
||||
## "Camera3D" — bare relative to scene_root
|
||||
## "World/Ground" — nested bare relative to scene_root
|
||||
##
|
||||
## Also accepts SceneTree-style "/root/<scene_root_name>[/...]" as an alias for
|
||||
## the edited scene root. Agents reach for /root/Foo right after creating a
|
||||
## scene because that's where scenes live at runtime; we honor it so the call
|
||||
## doesn't fail with a confusing "not found" error. The alias only kicks in
|
||||
## when the segment after /root matches the scene root's name — paths like
|
||||
## "/root/@EditorNode@.../Main/..." (returned by Node.get_path() in the editor)
|
||||
## fall through to the absolute-path fallback unchanged.
|
||||
static func resolve(scene_path: String, scene_root: Node) -> Node:
|
||||
if scene_root == null:
|
||||
return null
|
||||
|
||||
## /root/<scene_root_name>[/...] alias: strip the /root prefix and recurse.
|
||||
## Match the scene root by name explicitly so we don't capture editor-
|
||||
## internal paths that legitimately live under /root.
|
||||
var alias_prefix := "/root/" + scene_root.name
|
||||
if scene_path == alias_prefix or scene_path.begins_with(alias_prefix + "/"):
|
||||
return resolve(scene_path.substr(5), scene_root) # keep leading slash
|
||||
|
||||
var root_prefix := "/" + scene_root.name
|
||||
if scene_path == root_prefix:
|
||||
return scene_root
|
||||
if scene_path.begins_with(root_prefix + "/"):
|
||||
var relative := scene_path.substr(root_prefix.length() + 1)
|
||||
return scene_root.get_node_or_null(relative)
|
||||
|
||||
# Try as-is (relative path, or absolute SceneTree path).
|
||||
return scene_root.get_node_or_null(scene_path)
|
||||
|
||||
|
||||
## Return the edited scene root, or an error dict if the editor has no open
|
||||
## scene or the open scene doesn't match `expected_scene_file`.
|
||||
##
|
||||
## `expected_scene_file` is the caller's `scene_file` parameter — an empty
|
||||
## string means "target whatever is currently edited" (current behaviour,
|
||||
## no guard). A non-empty value must match `scene_file_path` on the current
|
||||
## edited scene root exactly, or we return EDITED_SCENE_MISMATCH so the
|
||||
## caller can re-open the right scene.
|
||||
##
|
||||
## Shape on success: {"node": <scene_root>}. Shape on error matches
|
||||
## `ErrorCodes.make()` so callers can propagate the result directly.
|
||||
static func require_edited_scene(expected_scene_file: String) -> Dictionary:
|
||||
var root := EditorInterface.get_edited_scene_root()
|
||||
if root == null:
|
||||
# Mirrors the structured payload that the Python-side require_writable
|
||||
# gate attaches for `playing` / `importing`. Together these cover the
|
||||
# three recoverable editor *states* (playing / importing / no_scene)
|
||||
# — the EDITOR_NOT_READY paths an AI caller can act on. Other
|
||||
# EDITOR_NOT_READY callsites describing internal-state failures
|
||||
# ("EditorFileSystem not available" etc.) intentionally don't carry
|
||||
# this payload because there's no useful caller hint to give.
|
||||
var err := ErrorCodes.make(ErrorCodes.EDITOR_NOT_READY, "No scene open")
|
||||
err["error"]["data"] = {
|
||||
"editor_state": "no_scene",
|
||||
"retryable": false,
|
||||
"hint": (
|
||||
"No scene is open. Call scene_open with a scene path "
|
||||
+ "(e.g. \"res://main.tscn\") before issuing scene-mutating tools."
|
||||
),
|
||||
}
|
||||
return err
|
||||
if not expected_scene_file.is_empty() and root.scene_file_path != expected_scene_file:
|
||||
var actual := root.scene_file_path if not root.scene_file_path.is_empty() else "<unsaved>"
|
||||
return ErrorCodes.make(
|
||||
ErrorCodes.EDITED_SCENE_MISMATCH,
|
||||
(
|
||||
"Expected edited scene \"%s\" but \"%s\" is active. "
|
||||
+ "Call scene_open(\"%s\") first, or omit scene_file to target the active scene."
|
||||
) % [expected_scene_file, actual, expected_scene_file],
|
||||
)
|
||||
return {"node": root}
|
||||
|
||||
|
||||
## Format a "parent not found" error that names the path convention.
|
||||
## Agents routinely try /root/Foo or absolute SceneTree paths; the bare
|
||||
## "Parent not found: X" gave them no hint that paths are scene-relative.
|
||||
## Wording is generic ("Paths are relative...") so the helper works for any
|
||||
## param name (parent_path, new_parent, …).
|
||||
static func format_parent_error(path: String, scene_root: Node) -> String:
|
||||
if scene_root == null:
|
||||
return "Parent not found: %s. No edited scene is open." % path
|
||||
var root_name := str(scene_root.name)
|
||||
return "Parent not found: %s. Paths are relative to the edited scene root (e.g. \"/%s\" or \"\"), not the SceneTree. Scene root is \"/%s\"." % [path, root_name, root_name]
|
||||
|
||||
|
||||
## Format a "node not found" error that names the path convention and, when
|
||||
## possible, suggests a corrected path. Agents routinely pass /root/Foo
|
||||
## (runtime SceneTree) or unprefixed names; the bare "Node not found: X"
|
||||
## gives no hint that paths are edited-scene-relative.
|
||||
##
|
||||
## Suggestion logic (highest-confidence first):
|
||||
## 1. /root/<X>[/...] where <X> is not the scene root → suggest /<sceneRoot>/<X>[/...]
|
||||
## 2. path doesn't start with "/" → suggest "/<sceneRoot>/<path>"
|
||||
## 3. otherwise no concrete "did you mean", just the convention reminder.
|
||||
static func format_node_error(path: String, scene_root: Node) -> String:
|
||||
if scene_root == null:
|
||||
return "Node not found: %s. No edited scene is open." % path
|
||||
var root_name := str(scene_root.name)
|
||||
var suggestion := ""
|
||||
|
||||
if path.begins_with("/root/"):
|
||||
var after_root := path.substr(6) # "/root/" is 6 chars
|
||||
# Only suggest if the segment after /root/ isn't already the scene root
|
||||
# (resolve() handles /root/<sceneRoot>/... as an alias, so a failure
|
||||
# with that prefix means a deeper segment is wrong — no clean rewrite).
|
||||
var first_seg := after_root.split("/")[0]
|
||||
if first_seg != root_name and not first_seg.is_empty():
|
||||
suggestion = "/" + root_name + "/" + after_root
|
||||
elif not path.begins_with("/") and not path.is_empty():
|
||||
suggestion = "/" + root_name + "/" + path
|
||||
|
||||
if suggestion.is_empty():
|
||||
return "Node not found: %s. Paths are relative to the edited scene root (e.g. \"/%s/Child\"), not runtime /root/... paths. Scene root is \"/%s\"." % [path, root_name, root_name]
|
||||
return "Node not found: %s. Did you mean \"%s\"? Paths are relative to the edited scene root, not runtime /root/... paths. Scene root is \"/%s\"." % [path, suggestion, root_name]
|
||||
Reference in New Issue
Block a user