@tool extends RefCounted const Coerce := preload("res://addons/agent_tools/tools/_coerce.gd") # scene.inspect — read-only. Accepts {"path": "res://..."} or omits path to use the currently-edited scene. # Returns {root: , path: ""} where NodeDict = {name, class, node_path, script, children}. static func inspect(params: Dictionary) -> Dictionary: var path: String = params.get("path", "") var root: Node var owns_root := false var scene_file_path: String if path == "": root = EditorInterface.get_edited_scene_root() if root == null: return _err(-32001, "no scene open and no 'path' provided") scene_file_path = root.scene_file_path else: var packed := ResourceLoader.load(path, "PackedScene") as PackedScene if packed == null: return _err(-32001, "failed to load scene: %s" % path) root = packed.instantiate(PackedScene.GEN_EDIT_STATE_DISABLED) if root == null: return _err(-32001, "instantiate failed: %s" % path) owns_root = true scene_file_path = path var tree := _node_to_dict(root, root) if owns_root: root.free() return _ok({"path": scene_file_path, "root": tree}) static func _node_to_dict(node: Node, scene_root: Node) -> Dictionary: var d := { "name": String(node.name), "class": node.get_class(), "node_path": "." if node == scene_root else String(scene_root.get_path_to(node)), "script": "", "children": [], } var scr := node.get_script() as Script if scr: d.script = scr.resource_path for child in node.get_children(): # Only include nodes belonging to this scene (skip runtime-added children of instanced sub-scenes). if child.owner == scene_root or child == scene_root: d.children.append(_node_to_dict(child, scene_root)) return d # scene.add_node — operates on the currently-edited scene. # Params: {type: "Node2D", name?: "Foo", parent_path?: "."} static func add_node(params: Dictionary) -> Dictionary: var node_type: String = params.get("type", "") var node_name: String = params.get("name", "") var parent_path: String = params.get("parent_path", ".") if node_type == "": return _err(-32602, "missing 'type'") if not ClassDB.class_exists(node_type): return _err(-32602, "unknown class: %s" % node_type) if not ClassDB.is_parent_class(node_type, "Node"): return _err(-32602, "type must derive from Node: %s" % node_type) if not ClassDB.can_instantiate(node_type): return _err(-32602, "class is not instantiable: %s" % node_type) var root := EditorInterface.get_edited_scene_root() if root == null: return _err(-32001, "no scene open") var parent: Node = root if parent_path == "." or parent_path == "" else root.get_node_or_null(parent_path) if parent == null: return _err(-32001, "parent not found: %s" % parent_path) var inst := ClassDB.instantiate(node_type) as Node if inst == null: return _err(-32001, "failed to instantiate %s" % node_type) parent.add_child(inst) if node_name != "": inst.name = node_name # Owner MUST be set to the scene root, otherwise the node won't be saved. inst.owner = root EditorInterface.mark_scene_as_unsaved() return _ok({ "node_path": String(root.get_path_to(inst)), "name": String(inst.name), "class": inst.get_class(), }) # scene.set_property — operates on the currently-edited scene. # Params: {node_path, property, value} — value is coerced via Coerce.coerce based on # the target property's type. See _coerce.gd for the full supported-type list. static func set_property(params: Dictionary) -> Dictionary: var node_path: String = params.get("node_path", "") var property_name: String = params.get("property", "") if node_path == "" or property_name == "": return _err(-32602, "missing 'node_path' or 'property'") var root := EditorInterface.get_edited_scene_root() if root == null: return _err(-32001, "no scene open") var node: Node = root if node_path == "." else root.get_node_or_null(node_path) if node == null: return _err(-32001, "node not found: %s" % node_path) var prop_info: Dictionary = {} for p in node.get_property_list(): if p.name == property_name: prop_info = p break if prop_info.is_empty(): return _err(-32602, "property not found on %s: %s" % [node.get_class(), property_name]) # Reject read-only properties upfront — Godot would silently ignore the set(). if int(prop_info.get("usage", 0)) & PROPERTY_USAGE_READ_ONLY: return _err(-32602, "property '%s' on %s is read-only" % [property_name, node.get_class()]) var assigned := _assign_property(node, property_name, prop_info, params.get("value")) if assigned.has("error"): return _err(int(assigned.get("code", -32001)), assigned.error) EditorInterface.mark_scene_as_unsaved() return _ok({ "node_path": node_path, "property": property_name, "value": Coerce.to_json(assigned.get("stored")), }) # Assign a JSON-native value into node[property_name], handling three failure # modes the bare `node.set()` path gets wrong: # # 1. Node-typed @exports (`@export var foo: Node2D`) — TYPE_OBJECT with a # PROPERTY_HINT_NODE_TYPE hint. A path string must be resolved to the actual # Node so PackedScene.pack() emits the `node_paths=PackedStringArray(...)` # metadata and serializes it (the inspector drag-drop equivalent). A bare # string assignment is silently dropped at save time. # 2. Setter-routed properties (notably PROPERTY_USAGE_NO_EDITOR ones like # `unique_name_in_owner`) — `node.set()` doesn't reliably dispatch to the # registered setter. If the readback doesn't match, route through the # explicit `set_` method. # 3. Silently-dropped assignments — Godot accepts the set() call but drops the # value (type mismatch on a Resource slot, or a primitive that didn't take). # Surface an error instead of echoing a misleading value. # # Returns {"stored": } on success, or {"error": msg, "code": int} on failure. static func _assign_property(node: Node, property_name: String, prop_info: Dictionary, raw_value) -> Dictionary: # Case 1: node-typed export. Resolve a path string to the actual Node. if prop_info.type == TYPE_OBJECT and int(prop_info.get("hint", 0)) == PROPERTY_HINT_NODE_TYPE: if raw_value == null: node.set(property_name, null) return {"stored": null} if not (raw_value is String): return {"code": -32602, "error": "node-typed export '%s' expects a node path string (e.g. '../Target'), got %s" % [property_name, type_string(typeof(raw_value))]} var target := node.get_node_or_null(NodePath(raw_value)) if target == null: return {"code": -32001, "error": "node-typed export '%s' on %s: no node at path '%s' (resolved relative to '%s'). " % [property_name, node.get_class(), raw_value, node.name] + "The target node must already exist in the scene."} node.set(property_name, target) var stored_node = node.get(property_name) if stored_node != target: return {"code": -32001, "error": "node-typed export '%s' on %s: assignment dropped — resolved node is a %s but the slot expects a %s" % [property_name, node.get_class(), target.get_class(), prop_info.get("hint_string", "Node")]} return {"stored": stored_node} var coerced = Coerce.coerce(raw_value, prop_info.type) if coerced is Dictionary and coerced.has("_error"): return {"code": -32602, "error": coerced._error} node.set(property_name, coerced) var stored = node.get(property_name) # Case 2: setter routing. Some properties don't dispatch through node.set(); # if the readback diverged and an explicit setter exists, go through it. if not _values_match(stored, coerced) and node.has_method("set_" + property_name): node.call("set_" + property_name, coerced) stored = node.get(property_name) # Case 3a: Resource-typed slot silently rejected a non-Resource value. if coerced != null and stored == null and prop_info.type == TYPE_OBJECT: return {"code": -32001, "error": "property '%s' on %s was not accepted (assignment silently dropped — target expects a %s; passed value type didn't match)" % [property_name, node.get_class(), prop_info.get("hint_string", "Resource")]} # Case 3b: a bool that didn't take. Limited to bool (the documented pitfall, # e.g. unique_name_in_owner) because other primitives legitimately transform # on set (name auto-deduplicates, ints clamp to range, floats lose precision), # which would make a strict match check fire false positives. if prop_info.type == TYPE_BOOL and not _values_match(stored, coerced): return {"code": -32001, "error": "property '%s' on %s was not accepted (set silently dropped: requested %s, stored %s)" % [property_name, node.get_class(), str(coerced), str(stored)]} return {"stored": stored} # Equality check tolerant of GDScript's cross-type Variant comparison. Used to # detect whether an assignment actually took before deciding to route through a # setter or flag a silent drop. static func _values_match(a, b) -> bool: return a == b # scene.remove_node — operates on the currently-edited scene. Cannot remove the scene root. # Params: {node_path: "Player/Sprite2D"} static func remove_node(params: Dictionary) -> Dictionary: var node_path: String = params.get("node_path", "") if node_path == "": return _err(-32602, "missing 'node_path'") if node_path == ".": return _err(-32602, "cannot remove scene root") var root := EditorInterface.get_edited_scene_root() if root == null: return _err(-32001, "no scene open") var node: Node = root.get_node_or_null(node_path) if node == null: return _err(-32001, "node not found: %s" % node_path) node.get_parent().remove_child(node) node.queue_free() EditorInterface.mark_scene_as_unsaved() return _ok({"removed": node_path}) # scene.reparent — move a node under a new parent in the currently-edited scene. # Params: {node_path, new_parent_path, keep_global_transform?: true} static func reparent(params: Dictionary) -> Dictionary: var node_path: String = params.get("node_path", "") var new_parent_path: String = params.get("new_parent_path", "") var keep_xform: bool = params.get("keep_global_transform", true) if node_path == "" or new_parent_path == "": return _err(-32602, "missing 'node_path' or 'new_parent_path'") if node_path == ".": return _err(-32602, "cannot reparent scene root") var root := EditorInterface.get_edited_scene_root() if root == null: return _err(-32001, "no scene open") var node: Node = root.get_node_or_null(node_path) if node == null: return _err(-32001, "node not found: %s" % node_path) var new_parent: Node = root if new_parent_path == "." else root.get_node_or_null(new_parent_path) if new_parent == null: return _err(-32001, "new parent not found: %s" % new_parent_path) if new_parent == node or node.is_ancestor_of(new_parent): return _err(-32602, "cannot reparent under self or descendant (would create cycle)") node.reparent(new_parent, keep_xform) # reparent() preserves owner in 4.3+, but be explicit so the node still serializes. node.owner = root EditorInterface.mark_scene_as_unsaved() return _ok({"node_path": String(root.get_path_to(node))}) # scene.open — open a scene in the editor, making it the currently-edited scene. # Params: {path: "res://Main.tscn"} static func open_scene(params: Dictionary) -> Dictionary: var path: String = params.get("path", "") if path == "": return _err(-32602, "missing 'path'") if not ResourceLoader.exists(path, "PackedScene"): return _err(-32001, "scene not found: %s" % path) EditorInterface.open_scene_from_path(path) return _ok({"path": path}) # scene.save — save the currently-edited scene. Pass 'path' to save-as (rebinds # the scene to that path). # # Fresh scenes (created via `scene.add_node`-only workflows with no backing file, # or never-saved manual scenes) have an empty `scene_file_path`. Godot's bare # `save_scene()` responds by opening the native Save-As file dialog — blocking # the editor and requiring a human click. We detect that case and return an # error instead, so the agent can react cleanly by re-calling with `path`. static func save_scene(params: Dictionary) -> Dictionary: var root := EditorInterface.get_edited_scene_root() if root == null: return _err(-32001, "no scene open") var path: String = params.get("path", "") var current_path: String = root.scene_file_path if path == "" and current_path == "": return _err(-32602, "scene has never been saved and no 'path' was given. " + "Calling bare save would open the Save-As dialog and block the editor. " + "Re-call with 'path' set to a res:// target (e.g. 'res://scenes/foo.tscn').") if path == "": var err := EditorInterface.save_scene() if err != OK: return _err(-32001, "save failed: error %d" % err) else: # save_scene_as does NOT auto-create parent directories — it silently # fails if the target dir doesn't exist. Ensure the dir first. var dir_path := path.get_base_dir() if not DirAccess.dir_exists_absolute(dir_path): var derr := DirAccess.make_dir_recursive_absolute(dir_path) if derr != OK: return _err(-32001, "mkdir failed (%d): %s" % [derr, dir_path]) # save_scene_as returns void in Godot 4.x — no error to check directly. EditorInterface.save_scene_as(path) var final_path: String = EditorInterface.get_edited_scene_root().scene_file_path # If we passed a path but the scene still isn't bound, the save didn't land. # Surface that explicitly instead of returning {"path": ""} which looks like success. if path != "" and final_path == "": return _err(-32001, "save_scene_as('%s') did not bind the scene — verify the path is writable and ends in .tscn" % path) return _ok({"path": final_path}) # scene.new — create a new .tscn file with a root node of the given type. By default # opens the new scene in the editor so subsequent scene.* calls operate on it. # Params: {path, root_type?: "Node", root_name?, overwrite?: false, open_after?: true} static func new_scene(params: Dictionary) -> Dictionary: var path: String = params.get("path", "") var root_type: String = params.get("root_type", "Node") var root_name: String = params.get("root_name", "") var overwrite: bool = params.get("overwrite", false) var open_after: bool = params.get("open_after", true) if path == "" or not path.begins_with("res://") or not path.ends_with(".tscn"): return _err(-32602, "'path' must be 'res://...' ending in .tscn") if FileAccess.file_exists(path) and not overwrite: return _err(-32602, "file exists (pass overwrite:true to replace): %s" % path) if not ClassDB.class_exists(root_type): return _err(-32602, "unknown class: %s" % root_type) if not ClassDB.is_parent_class(root_type, "Node"): return _err(-32602, "root_type must derive from Node: %s" % root_type) if not ClassDB.can_instantiate(root_type): return _err(-32602, "class is not instantiable: %s" % root_type) var root := ClassDB.instantiate(root_type) as Node if root == null: return _err(-32001, "failed to instantiate %s" % root_type) if root_name != "": root.name = root_name var dir_path := path.get_base_dir() if not DirAccess.dir_exists_absolute(dir_path): var derr := DirAccess.make_dir_recursive_absolute(dir_path) if derr != OK: root.free() return _err(-32001, "mkdir failed (%d): %s" % [derr, dir_path]) var packed := PackedScene.new() var pack_err := packed.pack(root) var final_name := String(root.name) root.free() if pack_err != OK: return _err(-32001, "PackedScene.pack failed: %d" % pack_err) var save_err := ResourceSaver.save(packed, path) if save_err != OK: return _err(-32001, "ResourceSaver.save failed: %d" % save_err) EditorInterface.get_resource_filesystem().update_file(path) if open_after: # If the target path is already the currently-edited scene, open_scene_from_path # will just switch to the existing (stale) in-memory tab. Force a reload so the # freshly-written file is what the editor shows. var current_root := EditorInterface.get_edited_scene_root() if current_root != null and current_root.scene_file_path == path: EditorInterface.reload_scene_from_path(path) else: EditorInterface.open_scene_from_path(path) return _ok({"path": path, "root_type": root_type, "root_name": final_name}) # scene.instance_packed — add an existing .tscn as a sub-scene child in the currently-edited scene. # Params: {scene_path, parent_path?: ".", name?} static func instance_packed(params: Dictionary) -> Dictionary: var scene_path: String = params.get("scene_path", "") var parent_path: String = params.get("parent_path", ".") var node_name: String = params.get("name", "") if scene_path == "": return _err(-32602, "missing 'scene_path'") var packed := ResourceLoader.load(scene_path, "PackedScene") as PackedScene if packed == null: return _err(-32001, "failed to load scene: %s" % scene_path) var root := EditorInterface.get_edited_scene_root() if root == null: return _err(-32001, "no scene open") var parent: Node = root if parent_path == "." or parent_path == "" else root.get_node_or_null(parent_path) if parent == null: return _err(-32001, "parent not found: %s" % parent_path) if scene_path == root.scene_file_path: return _err(-32602, "cannot instance a scene into itself (would recurse)") var inst := packed.instantiate() if inst == null: return _err(-32001, "instantiate failed") parent.add_child(inst) if node_name != "": inst.name = node_name # Owner is the scene root so the sub-scene instance serializes with this scene. inst.owner = root EditorInterface.mark_scene_as_unsaved() return _ok({ "node_path": String(root.get_path_to(inst)), "scene_path": scene_path, "name": String(inst.name), }) # scene.get_property — read a property from a node. Mirror of set_property. # Params: {node_path, property} static func get_property(params: Dictionary) -> Dictionary: var node_path: String = params.get("node_path", "") var property_name: String = params.get("property", "") if node_path == "" or property_name == "": return _err(-32602, "missing 'node_path' or 'property'") var root := EditorInterface.get_edited_scene_root() if root == null: return _err(-32001, "no scene open") var node: Node = root if node_path == "." else root.get_node_or_null(node_path) if node == null: return _err(-32001, "node not found: %s" % node_path) var prop_type: int = TYPE_NIL var found := false for p in node.get_property_list(): if p.name == property_name: prop_type = p.type found = true break if not found: return _err(-32602, "property not found on %s: %s" % [node.get_class(), property_name]) return _ok({ "node_path": node_path, "property": property_name, "value": Coerce.to_json(node.get(property_name)), "type": type_string(prop_type), }) # scene.capture_screenshot — save a PNG of what the editor is currently showing # for the open scene. Captures the appropriate editor viewport (2D for 2D/Control # root nodes, 3D for Node3D roots). Includes the editor's grid/gizmos — this is # "what the user sees" rather than a clean render, which is actually useful for # the agent to spot layout problems. # Params: {output?: "res://screenshot.png"} # Returns: {path, width, height} static func capture_screenshot(params: Dictionary) -> Dictionary: var output_path: String = params.get("output", "") var root := EditorInterface.get_edited_scene_root() if root == null: return _err(-32001, "no scene open — open one first with scene.open") var viewport: SubViewport if root is Node3D: viewport = EditorInterface.get_editor_viewport_3d(0) else: viewport = EditorInterface.get_editor_viewport_2d() if viewport == null: return _err(-32001, "could not obtain editor viewport") var tex := viewport.get_texture() if tex == null: return _err(-32001, "viewport has no texture yet (try again after the editor renders a frame)") var img := tex.get_image() if img == null or img.is_empty(): return _err(-32001, "failed to capture image from viewport") if output_path == "": var base: String = "screenshot" if root.scene_file_path != "": base = root.scene_file_path.get_file().get_basename() output_path = "res://.godot/agent_tools/%s.png" % base var dir_path := output_path.get_base_dir() if not DirAccess.dir_exists_absolute(dir_path): var derr := DirAccess.make_dir_recursive_absolute(dir_path) if derr != OK: return _err(-32001, "mkdir failed: %d" % derr) var save_err := img.save_png(output_path) if save_err != OK: return _err(-32001, "save_png failed: %d" % save_err) return _ok({ "path": output_path, "width": img.get_width(), "height": img.get_height(), "note": "clean viewport capture (no editor grid/gizmos); empty scenes render as the viewport background color", }) # scene.duplicate_node — clone a node (with its descendants) into the currently-edited scene. # Owner is set recursively so the duplicated subtree serializes with the scene. # Params: {node_path, new_name?, parent_path?: same parent} static func duplicate_node(params: Dictionary) -> Dictionary: var node_path: String = params.get("node_path", "") var new_name: String = params.get("new_name", "") var parent_path: String = params.get("parent_path", "") if node_path == "": return _err(-32602, "missing 'node_path'") if node_path == ".": return _err(-32602, "cannot duplicate scene root") var root := EditorInterface.get_edited_scene_root() if root == null: return _err(-32001, "no scene open") var src: Node = root.get_node_or_null(node_path) if src == null: return _err(-32001, "node not found: %s" % node_path) # DUPLICATE_SIGNALS | DUPLICATE_GROUPS | DUPLICATE_SCRIPTS — default flags. var copy: Node = src.duplicate(7) if copy == null: return _err(-32001, "duplicate() failed") var parent: Node = src.get_parent() if parent_path == "" else root.get_node_or_null(parent_path) if parent == null: return _err(-32001, "parent not found: %s" % parent_path) parent.add_child(copy) if new_name != "": copy.name = new_name _set_owner_recursive(copy, root) EditorInterface.mark_scene_as_unsaved() return _ok({ "source": node_path, "node_path": String(root.get_path_to(copy)), "name": String(copy.name), }) static func _set_owner_recursive(node: Node, owner: Node) -> void: node.owner = owner for child in node.get_children(): _set_owner_recursive(child, owner) # scene.build_tree — build a subtree in one call. # Spec is recursive: each entry is {type (required), name?, properties?, script?, children?}. # Collapses what would otherwise be N scene.add_node + M scene.set_property + K script.attach # calls into a single request, which matters for any UI-heavy scene where hand-writing the # tree by calling the atomic tools balloons into dozens of round trips. # Properties use the same Coerce rules as scene.set_property (Vectors from arrays, Resources # auto-loaded from res:// paths, etc.). # On any failure, all nodes created during the call are rolled back so the scene doesn't # end up partly built. # Params: {parent_path?: ".", nodes: [TreeNode, ...]} # TreeNode: {type: "ClassName", name?, properties?: {name: value}, script?: "res://...", children?: [TreeNode, ...]} static func build_tree(params: Dictionary) -> Dictionary: var parent_path: String = params.get("parent_path", ".") var nodes: Array = params.get("nodes", []) if nodes.is_empty(): return _err(-32602, "missing 'nodes' (array of tree entries)") var root := EditorInterface.get_edited_scene_root() if root == null: return _err(-32001, "no scene open") var parent: Node = root if parent_path == "." or parent_path == "" else root.get_node_or_null(parent_path) if parent == null: return _err(-32001, "parent not found: %s" % parent_path) var created_nodes: Array = [] var created_paths: Array = [] for spec in nodes: var err_msg := _build_subtree(spec, parent, root, created_nodes, created_paths) if err_msg != "": # Rollback: free everything created during this call in reverse order. for i in range(created_nodes.size() - 1, -1, -1): var n: Node = created_nodes[i] if is_instance_valid(n): if n.get_parent(): n.get_parent().remove_child(n) n.queue_free() return _err(-32001, err_msg) EditorInterface.mark_scene_as_unsaved() return _ok({ "parent_path": parent_path, "created": created_paths, "count": created_paths.size(), }) # Recursive worker. Appends to the shared created_nodes/created_paths arrays so # the caller can roll back on failure. Returns "" on success, or an error message. static func _build_subtree(spec, parent: Node, scene_root: Node, created_nodes: Array, created_paths: Array) -> String: if typeof(spec) != TYPE_DICTIONARY: return "tree entry must be an object, got %s" % type_string(typeof(spec)) var node_type: String = spec.get("type", "") if node_type == "": return "tree entry missing 'type'" if not ClassDB.class_exists(node_type): return "unknown class: %s" % node_type if not ClassDB.is_parent_class(node_type, "Node"): return "type must derive from Node: %s" % node_type if not ClassDB.can_instantiate(node_type): return "class is not instantiable: %s" % node_type var inst := ClassDB.instantiate(node_type) as Node if inst == null: return "failed to instantiate %s" % node_type var node_name: String = spec.get("name", "") if node_name != "": inst.name = node_name parent.add_child(inst) inst.owner = scene_root created_nodes.append(inst) var this_path := String(scene_root.get_path_to(inst)) created_paths.append(this_path) # Attach a script before applying properties so script-exported properties # become settable in this same call. var script_path: String = spec.get("script", "") if script_path != "": var script := ResourceLoader.load(script_path, "Script") as Script if script == null: return "failed to load script '%s' for '%s'" % [script_path, this_path] inst.set_script(script) var properties: Dictionary = spec.get("properties", {}) if not properties.is_empty(): var by_name: Dictionary = {} for p in inst.get_property_list(): by_name[p.name] = p for prop_name in properties: if not by_name.has(prop_name): return "property not found on %s ('%s'): %s" % [node_type, this_path, prop_name] var prop_info: Dictionary = by_name[prop_name] if int(prop_info.get("usage", 0)) & PROPERTY_USAGE_READ_ONLY: return "property '%s' on '%s' is read-only" % [prop_name, this_path] var assigned := _assign_property(inst, prop_name, prop_info, properties[prop_name]) if assigned.has("error"): return "property '%s' on '%s': %s" % [prop_name, this_path, assigned.error] var children: Array = spec.get("children", []) for child_spec in children: var child_err := _build_subtree(child_spec, inst, scene_root, created_nodes, created_paths) if child_err != "": return child_err return "" # scene.call_method — invoke a method on a node in the currently-edited scene. # Args are coerced based on the target method's declared parameter types # (same rules as scene.set_property — including res:// → Resource auto-load). # Return value is serialized via Coerce.to_json so the echo is unambiguous data. # Params: {node_path, method, args?: []} static func call_method(params: Dictionary) -> Dictionary: var node_path: String = params.get("node_path", "") var method_name: String = params.get("method", "") var args: Array = params.get("args", []) if node_path == "" or method_name == "": return _err(-32602, "missing 'node_path' or 'method'") var root := EditorInterface.get_edited_scene_root() if root == null: return _err(-32001, "no scene open") var node: Node = root if node_path == "." else root.get_node_or_null(node_path) if node == null: return _err(-32001, "node not found: %s" % node_path) if not node.has_method(method_name): return _err(-32602, "method not found: %s.%s" % [node.get_class(), method_name]) # Not typed as Array — _coerce_args returns a Dictionary on error, and strict # typing would reject the assignment before we can branch on it. var coerced_args = _coerce_args(node, method_name, args) if coerced_args is Dictionary: return coerced_args # error passthrough var result = node.callv(method_name, coerced_args) EditorInterface.mark_scene_as_unsaved() return _ok({ "node_path": node_path, "method": method_name, "return": Coerce.to_json(result), }) # Look up the method's parameter types and coerce each arg accordingly. # Returns a coerced Array, or a Dictionary with {"error": ...} on failure. static func _coerce_args(obj: Object, method_name: String, args: Array): var method_info: Dictionary = {} for m in obj.get_method_list(): if m.name == method_name: method_info = m break var arg_types: Array = method_info.get("args", []) var out: Array = [] for i in args.size(): if i < arg_types.size(): var coerced = Coerce.coerce(args[i], arg_types[i].type) if coerced is Dictionary and coerced.has("_error"): return _err(-32602, "arg %d: %s" % [i, coerced._error]) out.append(coerced) else: out.append(args[i]) return out # scene.current — describe the currently-edited scene, or {open: false} if none. static func current(_params: Dictionary) -> Dictionary: var root := EditorInterface.get_edited_scene_root() if root == null: return _ok({"open": false}) return _ok({ "open": true, "path": root.scene_file_path, "root_name": String(root.name), "root_class": root.get_class(), }) static func _ok(data) -> Dictionary: return {"data": data} static func _err(code: int, msg: String) -> Dictionary: return {"error": {"code": code, "message": msg}}