From b2c243a819eae19213179a19c842411819287a66 Mon Sep 17 00:00:00 2001 From: Bryce Date: Wed, 29 Apr 2026 15:55:15 -0700 Subject: [PATCH] Remove stale root-level MCP server files and navigation test The MCP interaction server lives in scripts/mcp_interaction_server.gd; the root-level copies and test file are obsolete. --- mcp_interaction_server.gd | 4421 --------------------------------- mcp_interaction_server.gd.uid | 1 - tests/test_room_navigation.py | 516 ---- 3 files changed, 4938 deletions(-) delete mode 100644 mcp_interaction_server.gd delete mode 100644 mcp_interaction_server.gd.uid delete mode 100644 tests/test_room_navigation.py diff --git a/mcp_interaction_server.gd b/mcp_interaction_server.gd deleted file mode 100644 index 6ea8256..0000000 --- a/mcp_interaction_server.gd +++ /dev/null @@ -1,4421 +0,0 @@ -extends Node - -# MCP Interaction Server - TCP server for game interaction -# Runs as an autoload inside the Godot game, accepting JSON commands over TCP. -# No class_name to avoid autoload conflict. - -var _server: TCPServer -var _client: StreamPeerTCP -var _buffer: String = "" -var _busy: bool = false -var _busy_since: float = 0.0 -const PORT: int = 9090 -const BUSY_TIMEOUT: float = 30.0 -var _key_map: Dictionary -var _held_keys: Dictionary = {} - -func _ready() -> void: - # Ensure MCP server keeps processing even when game is paused - process_mode = Node.PROCESS_MODE_ALWAYS - _init_key_map() - _server = TCPServer.new() - var err: int = _server.listen(PORT, "127.0.0.1") - if err != OK: - push_error("McpInteractionServer: Failed to listen on port %d, error: %d" % [PORT, err]) - return - print("McpInteractionServer: Listening on 127.0.0.1:%d" % PORT) - - -func _process(_delta: float) -> void: - if _server == null: - return - - # Safety timeout: force-reset _busy if it's been stuck too long - if _busy and _busy_since > 0.0: - var elapsed: float = Time.get_ticks_msec() / 1000.0 - _busy_since - if elapsed > BUSY_TIMEOUT: - push_warning("McpInteractionServer: _busy flag stuck for %.1fs, force-resetting" % elapsed) - _busy = false - _busy_since = 0.0 - - # Accept new connections - if _server.is_connection_available(): - var new_client: StreamPeerTCP = _server.take_connection() - if new_client != null: - if _client != null: - _client.disconnect_from_host() - _client = new_client - _buffer = "" - print("McpInteractionServer: Client connected") - - # Read data from client - if _client == null: - return - - _client.poll() - var status: int = _client.get_status() - if status == StreamPeerTCP.STATUS_ERROR or status == StreamPeerTCP.STATUS_NONE: - print("McpInteractionServer: Client disconnected") - _client = null - _buffer = "" - _busy = false - _busy_since = 0.0 - return - - if status != StreamPeerTCP.STATUS_CONNECTED: - return - - var available: int = _client.get_available_bytes() - if available > 0: - var data: Array = _client.get_data(available) - if data[0] == OK: - var bytes: PackedByteArray = data[1] - _buffer += bytes.get_string_from_utf8() - - # Process complete lines (newline-delimited JSON) - while _buffer.find("\n") >= 0: - var newline_pos: int = _buffer.find("\n") - var line: String = _buffer.substr(0, newline_pos).strip_edges() - _buffer = _buffer.substr(newline_pos + 1) - if line.length() > 0: - _handle_command(line) - - -func _handle_command(json_str: String) -> void: - if _busy: - _send_response_raw({"error": "Server busy processing another command. Try again."}) - return - _busy = true - _busy_since = Time.get_ticks_msec() / 1000.0 - - var json: JSON = JSON.new() - var parse_err: int = json.parse(json_str) - if parse_err != OK: - _send_response({"error": "Invalid JSON: %s" % json.get_error_message()}) - return - - var data: Variant = json.data - if not data is Dictionary: - _send_response({"error": "Expected JSON object"}) - return - - var command: String = data.get("command", "") - var params: Dictionary = data.get("params", {}) - - match command: - # Async commands (use await) - "screenshot": - await _cmd_screenshot() - "click": - await _cmd_click(params) - "key_press": - await _cmd_key_press(params) - "eval": - await _cmd_eval(params) - "wait": - await _cmd_wait(params) - # Sync commands - "mouse_move": - _cmd_mouse_move(params) - "get_ui_elements": - _cmd_get_ui_elements() - "get_scene_tree": - _cmd_get_scene_tree() - "get_property": - _cmd_get_property(params) - "set_property": - _cmd_set_property(params) - "call_method": - _cmd_call_method(params) - "get_node_info": - _cmd_get_node_info(params) - "instantiate_scene": - _cmd_instantiate_scene(params) - "remove_node": - _cmd_remove_node(params) - "change_scene": - _cmd_change_scene(params) - "pause": - _cmd_pause(params) - "get_performance": - _cmd_get_performance(params) - "connect_signal": - _cmd_connect_signal(params) - "disconnect_signal": - _cmd_disconnect_signal(params) - "emit_signal": - _cmd_emit_signal(params) - "play_animation": - _cmd_play_animation(params) - "tween_property": - _cmd_tween_property(params) - "get_nodes_in_group": - _cmd_get_nodes_in_group(params) - "find_nodes_by_class": - _cmd_find_nodes_by_class(params) - "reparent_node": - _cmd_reparent_node(params) - # Enhanced input commands - "key_hold": - _cmd_key_hold(params) - "key_release": - _cmd_key_release(params) - "scroll": - _cmd_scroll(params) - "mouse_drag": - await _cmd_mouse_drag(params) - "gamepad": - _cmd_gamepad(params) - # Advanced runtime commands - "get_camera": - _cmd_get_camera() - "set_camera": - _cmd_set_camera(params) - "raycast": - await _cmd_raycast(params) - "get_audio": - _cmd_get_audio() - "spawn_node": - _cmd_spawn_node(params) - "set_shader_param": - _cmd_set_shader_param(params) - "audio_play": - _cmd_audio_play(params) - "audio_bus": - _cmd_audio_bus(params) - "navigate_path": - await _cmd_navigate_path(params) - "tilemap": - _cmd_tilemap(params) - "add_collision": - _cmd_add_collision(params) - "environment": - _cmd_environment(params) - "manage_group": - _cmd_manage_group(params) - "create_timer": - _cmd_create_timer(params) - "set_particles": - _cmd_set_particles(params) - "create_animation": - _cmd_create_animation(params) - "serialize_state": - _cmd_serialize_state(params) - "physics_body": - _cmd_physics_body(params) - "create_joint": - _cmd_create_joint(params) - "bone_pose": - _cmd_bone_pose(params) - "ui_theme": - _cmd_ui_theme(params) - "viewport": - _cmd_viewport(params) - "debug_draw": - _cmd_debug_draw(params) - # Batch 1: Networking + Input + System + Signals + Script - "http_request": - await _cmd_http_request(params) - "websocket": - _cmd_websocket(params) - "multiplayer": - _cmd_multiplayer(params) - "rpc": - _cmd_rpc(params) - "touch": - await _cmd_touch(params) - "input_state": - _cmd_input_state(params) - "input_action": - _cmd_input_action(params) - "list_signals": - _cmd_list_signals(params) - "await_signal": - await _cmd_await_signal(params) - "script": - _cmd_script(params) - "window": - _cmd_window(params) - "os_info": - _cmd_os_info() - "time_scale": - _cmd_time_scale(params) - "process_mode": - _cmd_process_mode(params) - "world_settings": - _cmd_world_settings(params) - # Batch 2: 3D Rendering + Lighting + Sky + Physics - "csg": - _cmd_csg(params) - "multimesh": - _cmd_multimesh(params) - "procedural_mesh": - _cmd_procedural_mesh(params) - "light_3d": - _cmd_light_3d(params) - "mesh_instance": - _cmd_mesh_instance(params) - "gridmap": - _cmd_gridmap(params) - "3d_effects": - _cmd_3d_effects(params) - "gi": - _cmd_gi(params) - "path_3d": - _cmd_path_3d(params) - "sky": - _cmd_sky(params) - "camera_attributes": - _cmd_camera_attributes(params) - "navigation_3d": - await _cmd_navigation_3d(params) - "physics_3d": - await _cmd_physics_3d(params) - # Batch 3: 2D Systems + Animation + Audio - "canvas": - _cmd_canvas(params) - "canvas_draw": - _cmd_canvas_draw(params) - "light_2d": - _cmd_light_2d(params) - "parallax": - _cmd_parallax(params) - "shape_2d": - _cmd_shape_2d(params) - "path_2d": - _cmd_path_2d(params) - "physics_2d": - await _cmd_physics_2d(params) - "animation_tree": - _cmd_animation_tree(params) - "animation_control": - _cmd_animation_control(params) - "skeleton_ik": - _cmd_skeleton_ik(params) - "audio_effect": - _cmd_audio_effect(params) - "audio_bus_layout": - _cmd_audio_bus_layout(params) - "audio_spatial": - _cmd_audio_spatial(params) - # Batch 4: Locale (runtime) - "locale": - _cmd_locale(params) - # Batch 5: UI Controls + Rendering + Resource - "ui_control": - _cmd_ui_control(params) - "ui_text": - _cmd_ui_text(params) - "ui_popup": - _cmd_ui_popup(params) - "ui_tree": - _cmd_ui_tree(params) - "ui_item_list": - _cmd_ui_item_list(params) - "ui_tabs": - _cmd_ui_tabs(params) - "ui_menu": - _cmd_ui_menu(params) - "ui_range": - _cmd_ui_range(params) - "render_settings": - _cmd_render_settings(params) - "resource": - _cmd_resource(params) - _: - _send_response({"error": "Unknown command: %s" % command}) - - -# Send response and clear busy flag -func _send_response(data: Dictionary) -> void: - _busy = false - _busy_since = 0.0 - _send_response_raw(data) - - -# Send response without clearing busy flag (used when rejecting during busy state) -func _send_response_raw(data: Dictionary) -> void: - if _client == null: - return - var json_str: String = JSON.stringify(data) + "\n" - var bytes: PackedByteArray = json_str.to_utf8_buffer() - _client.put_data(bytes) - - -# --- Screenshot --- -func _cmd_screenshot() -> void: - # Wait one frame so the viewport is fully rendered - await get_tree().process_frame - var image: Image = get_viewport().get_texture().get_image() - if image == null: - _send_response({"error": "Failed to capture screenshot"}) - return - var png_buffer: PackedByteArray = image.save_png_to_buffer() - var base64_str: String = Marshalls.raw_to_base64(png_buffer) - _send_response({ - "success": true, - "data": base64_str, - "width": image.get_width(), - "height": image.get_height() - }) - - -# --- Click --- -func _cmd_click(params: Dictionary) -> void: - var x: float = float(params.get("x", 0)) - var y: float = float(params.get("y", 0)) - var button: int = int(params.get("button", MOUSE_BUTTON_LEFT)) - - var pos: Vector2 = Vector2(x, y) - - # Mouse button press - var press_event: InputEventMouseButton = InputEventMouseButton.new() - press_event.position = pos - press_event.global_position = pos - press_event.button_index = button as MouseButton - press_event.pressed = true - Input.parse_input_event(press_event) - - # Wait a frame then release - await get_tree().process_frame - - var release_event: InputEventMouseButton = InputEventMouseButton.new() - release_event.position = pos - release_event.global_position = pos - release_event.button_index = button as MouseButton - release_event.pressed = false - Input.parse_input_event(release_event) - - _send_response({"success": true, "clicked": {"x": x, "y": y, "button": button}}) - - -# --- Key Press --- -func _cmd_key_press(params: Dictionary) -> void: - var action: String = params.get("action", "") - var key: String = params.get("key", "") - var pressed: bool = params.get("pressed", true) - - if action.length() > 0: - # Simulate an action press/release - if pressed: - Input.action_press(action) - else: - Input.action_release(action) - _send_response({"success": true, "action": action, "pressed": pressed}) - return - - if key.length() > 0: - var keycode: int = _string_to_keycode(key) - if keycode == KEY_NONE: - _send_response({"error": "Unknown key: %s" % key}) - return - - var event: InputEventKey = InputEventKey.new() - event.keycode = keycode as Key - event.physical_keycode = keycode as Key - event.pressed = pressed - Input.parse_input_event(event) - - if pressed: - # Auto-release after a frame - await get_tree().process_frame - var release_event: InputEventKey = InputEventKey.new() - release_event.keycode = keycode as Key - release_event.physical_keycode = keycode as Key - release_event.pressed = false - Input.parse_input_event(release_event) - - _send_response({"success": true, "key": key, "pressed": pressed}) - return - - _send_response({"error": "Must provide 'key' or 'action' parameter"}) - - -# --- Mouse Move --- -func _cmd_mouse_move(params: Dictionary) -> void: - var x: float = float(params.get("x", 0)) - var y: float = float(params.get("y", 0)) - var relative_x: float = float(params.get("relative_x", 0)) - var relative_y: float = float(params.get("relative_y", 0)) - - var event: InputEventMouseMotion = InputEventMouseMotion.new() - event.position = Vector2(x, y) - event.global_position = Vector2(x, y) - event.relative = Vector2(relative_x, relative_y) - Input.parse_input_event(event) - - _send_response({"success": true, "position": {"x": x, "y": y}}) - - -# --- Get UI Elements --- -func _cmd_get_ui_elements() -> void: - var elements: Array = [] - _collect_ui_elements(get_tree().root, elements) - _send_response({"success": true, "elements": elements}) - - -func _collect_ui_elements(node: Node, elements: Array) -> void: - if node is Control: - var ctrl: Control = node as Control - if ctrl.visible and ctrl.get_global_rect().size.x > 0: - var info: Dictionary = { - "name": ctrl.name, - "type": ctrl.get_class(), - "path": str(ctrl.get_path()), - "position": {"x": ctrl.global_position.x, "y": ctrl.global_position.y}, - "size": {"width": ctrl.size.x, "height": ctrl.size.y}, - } - # Get text content for common text-bearing nodes - if ctrl is Label: - info["text"] = (ctrl as Label).text - elif ctrl is Button: - info["text"] = (ctrl as Button).text - elif ctrl is LineEdit: - info["text"] = (ctrl as LineEdit).text - elif ctrl is RichTextLabel: - info["text"] = (ctrl as RichTextLabel).get_parsed_text() - - elements.append(info) - - for child in node.get_children(): - _collect_ui_elements(child, elements) - - -# --- Get Scene Tree --- -func _cmd_get_scene_tree() -> void: - var tree: Dictionary = _build_tree_node(get_tree().root) - _send_response({"success": true, "tree": tree}) - - -func _build_tree_node(node: Node) -> Dictionary: - var info: Dictionary = { - "name": node.name, - "type": node.get_class(), - } - var children_arr: Array = [] - for child in node.get_children(): - children_arr.append(_build_tree_node(child)) - if children_arr.size() > 0: - info["children"] = children_arr - return info - - -# --- Key String to Keycode --- -func _init_key_map() -> void: - _key_map = { - "A": KEY_A, "B": KEY_B, "C": KEY_C, "D": KEY_D, - "E": KEY_E, "F": KEY_F, "G": KEY_G, "H": KEY_H, - "I": KEY_I, "J": KEY_J, "K": KEY_K, "L": KEY_L, - "M": KEY_M, "N": KEY_N, "O": KEY_O, "P": KEY_P, - "Q": KEY_Q, "R": KEY_R, "S": KEY_S, "T": KEY_T, - "U": KEY_U, "V": KEY_V, "W": KEY_W, "X": KEY_X, - "Y": KEY_Y, "Z": KEY_Z, - "0": KEY_0, "1": KEY_1, "2": KEY_2, "3": KEY_3, - "4": KEY_4, "5": KEY_5, "6": KEY_6, "7": KEY_7, - "8": KEY_8, "9": KEY_9, - "SPACE": KEY_SPACE, "ENTER": KEY_ENTER, "RETURN": KEY_ENTER, - "ESCAPE": KEY_ESCAPE, "ESC": KEY_ESCAPE, - "TAB": KEY_TAB, "BACKSPACE": KEY_BACKSPACE, - "DELETE": KEY_DELETE, "INSERT": KEY_INSERT, - "HOME": KEY_HOME, "END": KEY_END, - "PAGEUP": KEY_PAGEUP, "PAGE_UP": KEY_PAGEUP, - "PAGEDOWN": KEY_PAGEDOWN, "PAGE_DOWN": KEY_PAGEDOWN, - "UP": KEY_UP, "DOWN": KEY_DOWN, "LEFT": KEY_LEFT, "RIGHT": KEY_RIGHT, - "SHIFT": KEY_SHIFT, "CTRL": KEY_CTRL, "CONTROL": KEY_CTRL, - "ALT": KEY_ALT, "CAPSLOCK": KEY_CAPSLOCK, "CAPS_LOCK": KEY_CAPSLOCK, - "F1": KEY_F1, "F2": KEY_F2, "F3": KEY_F3, "F4": KEY_F4, - "F5": KEY_F5, "F6": KEY_F6, "F7": KEY_F7, "F8": KEY_F8, - "F9": KEY_F9, "F10": KEY_F10, "F11": KEY_F11, "F12": KEY_F12, - } - -func _string_to_keycode(key_str: String) -> int: - var upper: String = key_str.to_upper() - if _key_map.has(upper): - return _key_map[upper] - if key_str.length() == 1: - return key_str.unicode_at(0) - return KEY_NONE - - -# --- Eval: Execute arbitrary GDScript at runtime --- -func _cmd_eval(params: Dictionary) -> void: - var code: String = params.get("code", "") - if code.is_empty(): - _send_response({"error": "No code provided"}) - return - - # Wrap user code in a function so we can capture the return value - var script_source: String = """extends Node - -func execute(): - var __result = null - __result = await _run() - return __result - -func _run(): -%s -""" % [_indent_code(code)] - - var script: GDScript = GDScript.new() - script.source_code = script_source - var err: int = script.reload() - if err != OK: - _send_response({"error": "Failed to compile GDScript (error %d). Check syntax." % err}) - return - - var temp_node: Node = Node.new() - temp_node.set_script(script) - # Allow eval to work even when game is paused - temp_node.process_mode = Node.PROCESS_MODE_ALWAYS - add_child(temp_node) - - var result: Variant = null - if temp_node.has_method("execute"): - result = await temp_node.execute() - - temp_node.queue_free() - _send_response({"success": true, "result": _variant_to_json(result)}) - - -func _indent_code(code: String) -> String: - var lines: PackedStringArray = code.split("\n") - var indented: String = "" - for line in lines: - indented += "\t" + line + "\n" - return indented - - -# --- Get Property --- -func _cmd_get_property(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var property: String = params.get("property", "") - if node_path.is_empty() or property.is_empty(): - _send_response({"error": "node_path and property are required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - var value: Variant = node.get(property) - _send_response({"success": true, "value": _variant_to_json(value), "property": property, "node_path": node_path}) - - -# --- Set Property --- -func _cmd_set_property(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var property: String = params.get("property", "") - if node_path.is_empty() or property.is_empty(): - _send_response({"error": "node_path and property are required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - var raw_value: Variant = params.get("value", null) - var type_hint: String = params.get("type_hint", "") - var value: Variant - if type_hint.is_empty(): - value = _json_to_variant_for_property(node, property, raw_value) - else: - value = _json_to_variant(raw_value, type_hint) - node.set(property, value) - _send_response({"success": true, "node_path": node_path, "property": property, "value": _variant_to_json(node.get(property))}) - - -# --- Call Method --- -func _cmd_call_method(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var method_name: String = params.get("method", "") - if node_path.is_empty() or method_name.is_empty(): - _send_response({"error": "node_path and method are required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - if not node.has_method(method_name): - _send_response({"error": "Method not found: %s on node %s" % [method_name, node_path]}) - return - - var args: Array = params.get("args", []) - var result: Variant = node.callv(method_name, args) - _send_response({"success": true, "result": _variant_to_json(result)}) - - -# --- Get Node Info --- -func _cmd_get_node_info(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - if node_path.is_empty(): - _send_response({"error": "node_path is required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - var properties: Array = [] - for prop in node.get_property_list(): - var prop_dict: Dictionary = prop - if prop_dict.get("usage", 0) & PROPERTY_USAGE_EDITOR: - properties.append({ - "name": prop_dict.get("name", ""), - "type": prop_dict.get("type", 0), - "value": _variant_to_json(node.get(prop_dict.get("name", ""))) - }) - - var signals: Array = [] - for sig in node.get_signal_list(): - var sig_dict: Dictionary = sig - signals.append(sig_dict.get("name", "")) - - var methods: Array = [] - for m in node.get_method_list(): - var m_dict: Dictionary = m - if not str(m_dict.get("name", "")).begins_with("_"): - methods.append(m_dict.get("name", "")) - - var children: Array = [] - for child in node.get_children(): - children.append({ - "name": child.name, - "type": child.get_class(), - "path": str(child.get_path()) - }) - - _send_response({ - "success": true, - "class": node.get_class(), - "name": node.name, - "path": str(node.get_path()), - "properties": properties, - "signals": signals, - "methods": methods, - "children": children - }) - - -# --- Instantiate Scene --- -func _cmd_instantiate_scene(params: Dictionary) -> void: - var scene_path: String = params.get("scene_path", "") - var parent_path: String = params.get("parent_path", "/root") - if scene_path.is_empty(): - _send_response({"error": "scene_path is required"}) - return - - var packed: PackedScene = load(scene_path) as PackedScene - if packed == null: - _send_response({"error": "Failed to load scene: %s" % scene_path}) - return - - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent node not found: %s" % parent_path}) - return - - var instance: Node = packed.instantiate() - parent.add_child(instance) - _send_response({"success": true, "instance_name": instance.name, "instance_path": str(instance.get_path())}) - - -# --- Remove Node --- -func _cmd_remove_node(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - if node_path.is_empty(): - _send_response({"error": "node_path is required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - var node_name: String = node.name - node.queue_free() - _send_response({"success": true, "removed": node_name}) - - -# --- Change Scene --- -func _cmd_change_scene(params: Dictionary) -> void: - var scene_path: String = params.get("scene_path", "") - if scene_path.is_empty(): - _send_response({"error": "scene_path is required"}) - return - - var err: int = get_tree().change_scene_to_file(scene_path) - if err != OK: - _send_response({"error": "Failed to change scene. Error code: %d" % err}) - return - - _send_response({"success": true, "scene": scene_path}) - - -# --- Pause --- -func _cmd_pause(params: Dictionary) -> void: - var paused: bool = params.get("paused", true) - get_tree().paused = paused - _send_response({"success": true, "paused": paused}) - - -# --- Get Performance --- -func _cmd_get_performance(_params: Dictionary) -> void: - _send_response({ - "success": true, - "fps": Performance.get_monitor(Performance.TIME_FPS), - "frame_time": Performance.get_monitor(Performance.TIME_PROCESS), - "physics_frame_time": Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS), - "memory_static": Performance.get_monitor(Performance.MEMORY_STATIC), - "memory_static_max": Performance.get_monitor(Performance.MEMORY_STATIC_MAX), - "object_count": Performance.get_monitor(Performance.OBJECT_COUNT), - "object_node_count": Performance.get_monitor(Performance.OBJECT_NODE_COUNT), - "object_orphan_node_count": Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT), - "render_total_objects": Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME), - "render_total_draw_calls": Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME) - }) - - -# --- Wait N Frames --- -func _cmd_wait(params: Dictionary) -> void: - var frames: int = int(params.get("frames", 1)) - for i in frames: - await get_tree().process_frame - _send_response({"success": true, "waited_frames": frames}) - - -# --- Helper: Convert Godot Variant to JSON-safe value --- -func _variant_to_json(value: Variant) -> Variant: - if value == null: - return null - if value is bool or value is int or value is float or value is String: - return value - if value is Vector2: - return {"x": value.x, "y": value.y} - if value is Vector3: - return {"x": value.x, "y": value.y, "z": value.z} - if value is Vector2i: - return {"x": value.x, "y": value.y} - if value is Vector3i: - return {"x": value.x, "y": value.y, "z": value.z} - if value is Color: - return {"r": value.r, "g": value.g, "b": value.b, "a": value.a} - if value is Quaternion: - return {"x": value.x, "y": value.y, "z": value.z, "w": value.w} - if value is Basis: - return { - "x": _variant_to_json(value.x), - "y": _variant_to_json(value.y), - "z": _variant_to_json(value.z) - } - if value is Transform3D: - return { - "basis": _variant_to_json(value.basis), - "origin": _variant_to_json(value.origin) - } - if value is Transform2D: - return { - "x": _variant_to_json(value.x), - "y": _variant_to_json(value.y), - "origin": _variant_to_json(value.origin) - } - if value is Rect2: - return {"position": _variant_to_json(value.position), "size": _variant_to_json(value.size)} - if value is AABB: - return {"position": _variant_to_json(value.position), "size": _variant_to_json(value.size)} - if value is NodePath: - return str(value) - if value is StringName: - return str(value) - # Packed arrays - serialize as JSON arrays instead of str() fallback - if value is PackedByteArray: - var arr: Array = [] - for item in value: - arr.append(item) - return arr - if value is PackedInt32Array or value is PackedInt64Array: - var arr: Array = [] - for item in value: - arr.append(item) - return arr - if value is PackedFloat32Array or value is PackedFloat64Array: - var arr: Array = [] - for item in value: - arr.append(item) - return arr - if value is PackedStringArray: - var arr: Array = [] - for item in value: - arr.append(item) - return arr - if value is PackedVector2Array: - var arr: Array = [] - for item in value: - arr.append({"x": item.x, "y": item.y}) - return arr - if value is PackedVector3Array: - var arr: Array = [] - for item in value: - arr.append({"x": item.x, "y": item.y, "z": item.z}) - return arr - if value is PackedColorArray: - var arr: Array = [] - for item in value: - arr.append({"r": item.r, "g": item.g, "b": item.b, "a": item.a}) - return arr - if value is Array: - var arr: Array = [] - for item in value: - arr.append(_variant_to_json(item)) - return arr - if value is Dictionary: - var dict: Dictionary = {} - for key in value: - dict[str(key)] = _variant_to_json(value[key]) - return dict - if value is Object: - if value is Node: - return {"_type": "Node", "class": value.get_class(), "name": (value as Node).name, "path": str((value as Node).get_path())} - if value is Resource: - return {"_type": "Resource", "class": value.get_class(), "path": (value as Resource).resource_path} - return {"_type": "Object", "class": value.get_class(), "id": value.get_instance_id()} - # Fallback: convert to string - return str(value) - - -# --- Helper: Convert JSON value back to Godot Variant --- -func _json_to_variant(value: Variant, type_hint: String = "") -> Variant: - if value == null: - return null - if value is Dictionary: - var dict: Dictionary = value - # Explicit type hints take priority - match type_hint: - "Vector2": - return Vector2(float(dict.get("x", 0)), float(dict.get("y", 0))) - "Vector2i": - return Vector2i(int(dict.get("x", 0)), int(dict.get("y", 0))) - "Vector3": - return Vector3(float(dict.get("x", 0)), float(dict.get("y", 0)), float(dict.get("z", 0))) - "Vector3i": - return Vector3i(int(dict.get("x", 0)), int(dict.get("y", 0)), int(dict.get("z", 0))) - "Color": - return Color(float(dict.get("r", 0)), float(dict.get("g", 0)), float(dict.get("b", 0)), float(dict.get("a", 1))) - "Quaternion": - return Quaternion(float(dict.get("x", 0)), float(dict.get("y", 0)), float(dict.get("z", 0)), float(dict.get("w", 1))) - "Rect2": - var pos: Dictionary = dict.get("position", {"x": 0, "y": 0}) - var sz: Dictionary = dict.get("size", {"x": 0, "y": 0}) - return Rect2(float(pos.get("x", 0)), float(pos.get("y", 0)), float(sz.get("x", 0)), float(sz.get("y", 0))) - "AABB": - var aabb_pos: Dictionary = dict.get("position", {"x": 0, "y": 0, "z": 0}) - var aabb_sz: Dictionary = dict.get("size", {"x": 0, "y": 0, "z": 0}) - return AABB( - Vector3(float(aabb_pos.get("x", 0)), float(aabb_pos.get("y", 0)), float(aabb_pos.get("z", 0))), - Vector3(float(aabb_sz.get("x", 0)), float(aabb_sz.get("y", 0)), float(aabb_sz.get("z", 0))) - ) - "Basis": - var bx: Dictionary = dict.get("x", {"x": 1, "y": 0, "z": 0}) - var by: Dictionary = dict.get("y", {"x": 0, "y": 1, "z": 0}) - var bz: Dictionary = dict.get("z", {"x": 0, "y": 0, "z": 1}) - return Basis( - Vector3(float(bx.get("x", 0)), float(bx.get("y", 0)), float(bx.get("z", 0))), - Vector3(float(by.get("x", 0)), float(by.get("y", 0)), float(by.get("z", 0))), - Vector3(float(bz.get("x", 0)), float(bz.get("y", 0)), float(bz.get("z", 0))) - ) - "Transform3D": - var basis_dict: Dictionary = dict.get("basis", {}) - var origin_dict: Dictionary = dict.get("origin", {"x": 0, "y": 0, "z": 0}) - var basis: Basis = _json_to_variant(basis_dict, "Basis") if basis_dict.size() > 0 else Basis.IDENTITY - var origin: Vector3 = Vector3(float(origin_dict.get("x", 0)), float(origin_dict.get("y", 0)), float(origin_dict.get("z", 0))) - return Transform3D(basis, origin) - "Transform2D": - var tx: Dictionary = dict.get("x", {"x": 1, "y": 0}) - var ty: Dictionary = dict.get("y", {"x": 0, "y": 1}) - var t_origin: Dictionary = dict.get("origin", {"x": 0, "y": 0}) - return Transform2D( - Vector2(float(tx.get("x", 0)), float(tx.get("y", 0))), - Vector2(float(ty.get("x", 0)), float(ty.get("y", 0))), - Vector2(float(t_origin.get("x", 0)), float(t_origin.get("y", 0))) - ) - # Auto-detect from dict keys - if dict.has("basis") and dict.has("origin"): - return _json_to_variant(dict, "Transform3D") - if dict.has("r") and dict.has("g") and dict.has("b"): - return Color(float(dict.get("r", 0)), float(dict.get("g", 0)), float(dict.get("b", 0)), float(dict.get("a", 1))) - if dict.has("x") and dict.has("y") and dict.has("z") and dict.has("w"): - return Quaternion(float(dict.get("x", 0)), float(dict.get("y", 0)), float(dict.get("z", 0)), float(dict.get("w", 1))) - if dict.has("position") and dict.has("size"): - var pos_dict: Dictionary = dict["position"] - var size_dict: Dictionary = dict["size"] - if pos_dict.has("z") or size_dict.has("z"): - return _json_to_variant(dict, "AABB") - return _json_to_variant(dict, "Rect2") - if dict.has("x") and dict.has("y") and dict.has("z"): - return Vector3(float(dict.get("x", 0)), float(dict.get("y", 0)), float(dict.get("z", 0))) - if dict.has("x") and dict.has("y") and dict.size() == 2: - return Vector2(float(dict.get("x", 0)), float(dict.get("y", 0))) - return value - return value - - -# --- Helper: Convert JSON value using node's property type info --- -func _json_to_variant_for_property(node: Node, property: String, value: Variant) -> Variant: - for prop in node.get_property_list(): - if prop["name"] == property: - var type_id: int = prop.get("type", 0) - match type_id: - TYPE_VECTOR2: - return _json_to_variant(value, "Vector2") - TYPE_VECTOR2I: - return _json_to_variant(value, "Vector2i") - TYPE_VECTOR3: - return _json_to_variant(value, "Vector3") - TYPE_VECTOR3I: - return _json_to_variant(value, "Vector3i") - TYPE_COLOR: - return _json_to_variant(value, "Color") - TYPE_QUATERNION: - return _json_to_variant(value, "Quaternion") - TYPE_RECT2: - return _json_to_variant(value, "Rect2") - TYPE_AABB: - return _json_to_variant(value, "AABB") - TYPE_BASIS: - return _json_to_variant(value, "Basis") - TYPE_TRANSFORM3D: - return _json_to_variant(value, "Transform3D") - TYPE_TRANSFORM2D: - return _json_to_variant(value, "Transform2D") - TYPE_BOOL: - if value is String: - return value.to_lower() == "true" - return bool(value) - TYPE_INT: - return int(value) - TYPE_FLOAT: - return float(value) - break - # No type info found, use raw value or auto-detect - return _json_to_variant(value) - - -# --- Connect Signal --- -func _cmd_connect_signal(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var signal_name: String = params.get("signal_name", "") - var target_path: String = params.get("target_path", "") - var method_name: String = params.get("method", "") - if node_path.is_empty() or signal_name.is_empty() or target_path.is_empty() or method_name.is_empty(): - _send_response({"error": "node_path, signal_name, target_path, and method are required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Source node not found: %s" % node_path}) - return - - var target: Node = get_tree().root.get_node_or_null(target_path) - if target == null: - _send_response({"error": "Target node not found: %s" % target_path}) - return - - if not node.has_signal(signal_name): - _send_response({"error": "Signal '%s' not found on node %s" % [signal_name, node_path]}) - return - - if not target.has_method(method_name): - _send_response({"error": "Method '%s' not found on target %s" % [method_name, target_path]}) - return - - if node.is_connected(signal_name, Callable(target, method_name)): - _send_response({"error": "Signal already connected"}) - return - - node.connect(signal_name, Callable(target, method_name)) - _send_response({"success": true, "signal": signal_name, "from": node_path, "to": target_path, "method": method_name}) - - -# --- Disconnect Signal --- -func _cmd_disconnect_signal(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var signal_name: String = params.get("signal_name", "") - var target_path: String = params.get("target_path", "") - var method_name: String = params.get("method", "") - if node_path.is_empty() or signal_name.is_empty() or target_path.is_empty() or method_name.is_empty(): - _send_response({"error": "node_path, signal_name, target_path, and method are required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Source node not found: %s" % node_path}) - return - - var target: Node = get_tree().root.get_node_or_null(target_path) - if target == null: - _send_response({"error": "Target node not found: %s" % target_path}) - return - - var callable: Callable = Callable(target, method_name) - if not node.is_connected(signal_name, callable): - _send_response({"error": "Signal is not connected"}) - return - - node.disconnect(signal_name, callable) - _send_response({"success": true, "disconnected": signal_name, "from": node_path, "to": target_path, "method": method_name}) - - -# --- Emit Signal --- -func _cmd_emit_signal(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var signal_name: String = params.get("signal_name", "") - if node_path.is_empty() or signal_name.is_empty(): - _send_response({"error": "node_path and signal_name are required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - if not node.has_signal(signal_name): - _send_response({"error": "Signal '%s' not found on node %s" % [signal_name, node_path]}) - return - - var args: Array = params.get("args", []) - var call_args: Array = [signal_name] - call_args.append_array(args) - node.callv("emit_signal", call_args) - _send_response({"success": true, "emitted": signal_name, "node": node_path, "arg_count": args.size()}) - - -# --- Play Animation --- -func _cmd_play_animation(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - if node_path.is_empty(): - _send_response({"error": "node_path is required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - if not node is AnimationPlayer: - _send_response({"error": "Node is not an AnimationPlayer: %s (is %s)" % [node_path, node.get_class()]}) - return - - var anim_player: AnimationPlayer = node as AnimationPlayer - var action: String = params.get("action", "play") - - match action: - "play": - var animation: String = params.get("animation", "") - if animation.is_empty(): - _send_response({"error": "animation name is required for play action"}) - return - if not anim_player.has_animation(animation): - _send_response({"error": "Animation '%s' not found. Available: %s" % [animation, str(anim_player.get_animation_list())]}) - return - anim_player.play(animation) - _send_response({"success": true, "action": "play", "animation": animation}) - "stop": - anim_player.stop() - _send_response({"success": true, "action": "stop"}) - "pause": - anim_player.pause() - _send_response({"success": true, "action": "pause"}) - "get_list": - var anims: Array = [] - for anim_name in anim_player.get_animation_list(): - anims.append(str(anim_name)) - _send_response({"success": true, "animations": anims, "current": anim_player.current_animation, "playing": anim_player.is_playing()}) - _: - _send_response({"error": "Unknown animation action: %s. Use play, stop, pause, or get_list" % action}) - - -# --- Tween Property --- -func _cmd_tween_property(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var property: String = params.get("property", "") - if node_path.is_empty() or property.is_empty(): - _send_response({"error": "node_path and property are required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - var final_value: Variant = _json_to_variant_for_property(node, property, params.get("final_value", null)) - var duration: float = float(params.get("duration", 1.0)) - var trans_type: int = int(params.get("trans_type", 0)) # Tween.TRANS_LINEAR - var ease_type: int = int(params.get("ease_type", 2)) # Tween.EASE_IN_OUT - - var tween: Tween = create_tween() - tween.tween_property(node, property, final_value, duration).set_trans(trans_type).set_ease(ease_type) - _send_response({"success": true, "node": node_path, "property": property, "duration": duration}) - - -# --- Get Nodes In Group --- -func _cmd_get_nodes_in_group(params: Dictionary) -> void: - var group_name: String = params.get("group", "") - if group_name.is_empty(): - _send_response({"error": "group is required"}) - return - - var nodes: Array = get_tree().get_nodes_in_group(group_name) - var result: Array = [] - for node in nodes: - result.append({ - "name": node.name, - "type": node.get_class(), - "path": str(node.get_path()) - }) - _send_response({"success": true, "group": group_name, "count": result.size(), "nodes": result}) - - -# --- Find Nodes By Class --- -func _cmd_find_nodes_by_class(params: Dictionary) -> void: - var class_filter: String = params.get("class_name", "") - if class_filter.is_empty(): - _send_response({"error": "class_name is required"}) - return - - var root_path: String = params.get("root_path", "/root") - var root_node: Node = get_tree().root.get_node_or_null(root_path) - if root_node == null: - _send_response({"error": "Root node not found: %s" % root_path}) - return - - var found: Array = [] - _find_by_class_recursive(root_node, class_filter, found) - _send_response({"success": true, "class_name": class_filter, "count": found.size(), "nodes": found}) - - -func _find_by_class_recursive(node: Node, class_filter: String, results: Array) -> void: - if node.get_class() == class_filter or node.is_class(class_filter): - results.append({ - "name": node.name, - "type": node.get_class(), - "path": str(node.get_path()) - }) - for child in node.get_children(): - _find_by_class_recursive(child, class_filter, results) - - -# --- Reparent Node --- -func _cmd_reparent_node(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var new_parent_path: String = params.get("new_parent_path", "") - if node_path.is_empty() or new_parent_path.is_empty(): - _send_response({"error": "node_path and new_parent_path are required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - var new_parent: Node = get_tree().root.get_node_or_null(new_parent_path) - if new_parent == null: - _send_response({"error": "New parent not found: %s" % new_parent_path}) - return - - var keep_global: bool = params.get("keep_global_transform", true) - node.reparent(new_parent, keep_global) - _send_response({"success": true, "node": node.name, "new_parent": new_parent_path, "new_path": str(node.get_path())}) - - -# --- Key Hold (no auto-release) --- -func _cmd_key_hold(params: Dictionary) -> void: - var action: String = params.get("action", "") - var key: String = params.get("key", "") - - if action.length() > 0: - Input.action_press(action) - _held_keys["action:" + action] = true - _send_response({"success": true, "held": action, "type": "action"}) - return - - if key.length() > 0: - var keycode: int = _string_to_keycode(key) - if keycode == KEY_NONE: - _send_response({"error": "Unknown key: %s" % key}) - return - var event: InputEventKey = InputEventKey.new() - event.keycode = keycode as Key - event.physical_keycode = keycode as Key - event.pressed = true - Input.parse_input_event(event) - _held_keys["key:" + key.to_upper()] = keycode - _send_response({"success": true, "held": key, "type": "key"}) - return - - _send_response({"error": "Must provide 'key' or 'action' parameter"}) - - -# --- Key Release --- -func _cmd_key_release(params: Dictionary) -> void: - var action: String = params.get("action", "") - var key: String = params.get("key", "") - - if action.length() > 0: - Input.action_release(action) - _held_keys.erase("action:" + action) - _send_response({"success": true, "released": action, "type": "action"}) - return - - if key.length() > 0: - var keycode: int = _string_to_keycode(key) - if keycode == KEY_NONE: - _send_response({"error": "Unknown key: %s" % key}) - return - var event: InputEventKey = InputEventKey.new() - event.keycode = keycode as Key - event.physical_keycode = keycode as Key - event.pressed = false - Input.parse_input_event(event) - _held_keys.erase("key:" + key.to_upper()) - _send_response({"success": true, "released": key, "type": "key"}) - return - - _send_response({"error": "Must provide 'key' or 'action' parameter"}) - - -# --- Scroll --- -func _cmd_scroll(params: Dictionary) -> void: - var x: float = float(params.get("x", 0)) - var y: float = float(params.get("y", 0)) - var direction: String = params.get("direction", "up") - var amount: int = int(params.get("amount", 1)) - - var button_index: int = MOUSE_BUTTON_WHEEL_UP - match direction: - "down": - button_index = MOUSE_BUTTON_WHEEL_DOWN - "left": - button_index = MOUSE_BUTTON_WHEEL_LEFT - "right": - button_index = MOUSE_BUTTON_WHEEL_RIGHT - - for i in amount: - var press_event: InputEventMouseButton = InputEventMouseButton.new() - press_event.position = Vector2(x, y) - press_event.global_position = Vector2(x, y) - press_event.button_index = button_index as MouseButton - press_event.pressed = true - press_event.factor = 1.0 - Input.parse_input_event(press_event) - - var release_event: InputEventMouseButton = InputEventMouseButton.new() - release_event.position = Vector2(x, y) - release_event.global_position = Vector2(x, y) - release_event.button_index = button_index as MouseButton - release_event.pressed = false - Input.parse_input_event(release_event) - - _send_response({"success": true, "direction": direction, "amount": amount, "position": {"x": x, "y": y}}) - - -# --- Mouse Drag --- -func _cmd_mouse_drag(params: Dictionary) -> void: - var from_x: float = float(params.get("from_x", 0)) - var from_y: float = float(params.get("from_y", 0)) - var to_x: float = float(params.get("to_x", 0)) - var to_y: float = float(params.get("to_y", 0)) - var button: int = int(params.get("button", MOUSE_BUTTON_LEFT)) - var steps: int = int(params.get("steps", 10)) - if steps < 1: - steps = 1 - - var from_pos: Vector2 = Vector2(from_x, from_y) - var to_pos: Vector2 = Vector2(to_x, to_y) - - # Press at start position - var press_event: InputEventMouseButton = InputEventMouseButton.new() - press_event.position = from_pos - press_event.global_position = from_pos - press_event.button_index = button as MouseButton - press_event.pressed = true - Input.parse_input_event(press_event) - - # Lerp position over steps frames - for i in steps: - await get_tree().process_frame - var t: float = float(i + 1) / float(steps) - var current_pos: Vector2 = from_pos.lerp(to_pos, t) - var move_event: InputEventMouseMotion = InputEventMouseMotion.new() - move_event.position = current_pos - move_event.global_position = current_pos - move_event.relative = (to_pos - from_pos) / float(steps) - move_event.button_mask = MOUSE_BUTTON_MASK_LEFT if button == MOUSE_BUTTON_LEFT else 0 - Input.parse_input_event(move_event) - - # Release at end position - var release_event: InputEventMouseButton = InputEventMouseButton.new() - release_event.position = to_pos - release_event.global_position = to_pos - release_event.button_index = button as MouseButton - release_event.pressed = false - Input.parse_input_event(release_event) - - _send_response({"success": true, "from": {"x": from_x, "y": from_y}, "to": {"x": to_x, "y": to_y}, "steps": steps}) - - -# --- Gamepad --- -func _cmd_gamepad(params: Dictionary) -> void: - var input_type: String = params.get("type", "button") - var index: int = int(params.get("index", 0)) - var value: float = float(params.get("value", 0)) - var device: int = int(params.get("device", 0)) - - if input_type == "button": - var event: InputEventJoypadButton = InputEventJoypadButton.new() - event.device = device - event.button_index = index as JoyButton - event.pressed = value > 0.5 - event.pressure = value - Input.parse_input_event(event) - _send_response({"success": true, "type": "button", "index": index, "pressed": event.pressed, "device": device}) - elif input_type == "axis": - var event: InputEventJoypadMotion = InputEventJoypadMotion.new() - event.device = device - event.axis = index as JoyAxis - event.axis_value = value - Input.parse_input_event(event) - _send_response({"success": true, "type": "axis", "index": index, "value": value, "device": device}) - else: - _send_response({"error": "Invalid type: %s. Use 'button' or 'axis'" % input_type}) - - -# --- Get Camera --- -func _cmd_get_camera() -> void: - var result: Dictionary = {"success": true} - - var cam2d: Camera2D = get_viewport().get_camera_2d() - if cam2d != null: - result["camera_2d"] = { - "position": {"x": cam2d.global_position.x, "y": cam2d.global_position.y}, - "rotation": cam2d.global_rotation, - "zoom": {"x": cam2d.zoom.x, "y": cam2d.zoom.y}, - "path": str(cam2d.get_path()) - } - - var cam3d: Camera3D = get_viewport().get_camera_3d() - if cam3d != null: - result["camera_3d"] = { - "position": {"x": cam3d.global_position.x, "y": cam3d.global_position.y, "z": cam3d.global_position.z}, - "rotation": {"x": rad_to_deg(cam3d.global_rotation.x), "y": rad_to_deg(cam3d.global_rotation.y), "z": rad_to_deg(cam3d.global_rotation.z)}, - "fov": cam3d.fov, - "path": str(cam3d.get_path()) - } - - if cam2d == null and cam3d == null: - result["error"] = "No active camera found" - result["success"] = false - - _send_response(result) - - -# --- Set Camera --- -func _cmd_set_camera(params: Dictionary) -> void: - var cam2d: Camera2D = get_viewport().get_camera_2d() - var cam3d: Camera3D = get_viewport().get_camera_3d() - - if cam2d == null and cam3d == null: - _send_response({"error": "No active camera found"}) - return - - if cam2d != null: - if params.has("position"): - var pos: Dictionary = params["position"] - cam2d.global_position = Vector2(float(pos.get("x", cam2d.global_position.x)), float(pos.get("y", cam2d.global_position.y))) - if params.has("rotation"): - var rot: Dictionary = params["rotation"] - cam2d.global_rotation = deg_to_rad(float(rot.get("z", rad_to_deg(cam2d.global_rotation)))) - if params.has("zoom"): - var z: Dictionary = params["zoom"] - cam2d.zoom = Vector2(float(z.get("x", cam2d.zoom.x)), float(z.get("y", cam2d.zoom.y))) - _send_response({"success": true, "camera": "2d", "position": _variant_to_json(cam2d.global_position), "zoom": _variant_to_json(cam2d.zoom)}) - return - - if cam3d != null: - if params.has("position"): - var pos: Dictionary = params["position"] - cam3d.global_position = Vector3(float(pos.get("x", cam3d.global_position.x)), float(pos.get("y", cam3d.global_position.y)), float(pos.get("z", cam3d.global_position.z))) - if params.has("rotation"): - var rot: Dictionary = params["rotation"] - cam3d.global_rotation = Vector3(deg_to_rad(float(rot.get("x", rad_to_deg(cam3d.global_rotation.x)))), deg_to_rad(float(rot.get("y", rad_to_deg(cam3d.global_rotation.y)))), deg_to_rad(float(rot.get("z", rad_to_deg(cam3d.global_rotation.z))))) - if params.has("fov"): - cam3d.fov = float(params["fov"]) - _send_response({"success": true, "camera": "3d", "position": _variant_to_json(cam3d.global_position), "rotation": _variant_to_json(cam3d.global_rotation)}) - return - - -# --- Raycast --- -func _cmd_raycast(params: Dictionary) -> void: - var from_dict: Dictionary = params.get("from", {}) - var to_dict: Dictionary = params.get("to", {}) - var collision_mask: int = int(params.get("collision_mask", 0xFFFFFFFF)) - - # Determine 2D vs 3D based on whether z is present - var is_3d: bool = from_dict.has("z") or to_dict.has("z") - - if is_3d: - var from_pos: Vector3 = Vector3(float(from_dict.get("x", 0)), float(from_dict.get("y", 0)), float(from_dict.get("z", 0))) - var to_pos: Vector3 = Vector3(float(to_dict.get("x", 0)), float(to_dict.get("y", 0)), float(to_dict.get("z", 0))) - - # Wait a frame to ensure physics state is available - await get_tree().process_frame - - var space_state: PhysicsDirectSpaceState3D = get_viewport().world_3d.direct_space_state - var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(from_pos, to_pos, collision_mask) - var result: Dictionary = space_state.intersect_ray(query) - - if result.is_empty(): - _send_response({"success": true, "hit": false, "mode": "3d"}) - else: - _send_response({ - "success": true, "hit": true, "mode": "3d", - "position": _variant_to_json(result["position"]), - "normal": _variant_to_json(result["normal"]), - "collider_path": str(result["collider"].get_path()) if result.has("collider") and result["collider"] is Node else "", - "collider_class": result["collider"].get_class() if result.has("collider") else "", - }) - else: - var from_pos: Vector2 = Vector2(float(from_dict.get("x", 0)), float(from_dict.get("y", 0))) - var to_pos: Vector2 = Vector2(float(to_dict.get("x", 0)), float(to_dict.get("y", 0))) - - await get_tree().process_frame - - var space_state: PhysicsDirectSpaceState2D = get_viewport().world_2d.direct_space_state - var query: PhysicsRayQueryParameters2D = PhysicsRayQueryParameters2D.create(from_pos, to_pos, collision_mask) - var result: Dictionary = space_state.intersect_ray(query) - - if result.is_empty(): - _send_response({"success": true, "hit": false, "mode": "2d"}) - else: - _send_response({ - "success": true, "hit": true, "mode": "2d", - "position": _variant_to_json(result["position"]), - "normal": _variant_to_json(result["normal"]), - "collider_path": str(result["collider"].get_path()) if result.has("collider") and result["collider"] is Node else "", - "collider_class": result["collider"].get_class() if result.has("collider") else "", - }) - - -# --- Get Audio --- -func _cmd_get_audio() -> void: - var buses: Array = [] - for i in AudioServer.bus_count: - buses.append({ - "name": AudioServer.get_bus_name(i), - "volume_db": AudioServer.get_bus_volume_db(i), - "mute": AudioServer.is_bus_mute(i), - "solo": AudioServer.is_bus_solo(i), - }) - - var players: Array = [] - _find_audio_players(get_tree().root, players) - - _send_response({"success": true, "buses": buses, "players": players}) - - -func _find_audio_players(node: Node, results: Array) -> void: - if node is AudioStreamPlayer: - var p: AudioStreamPlayer = node as AudioStreamPlayer - results.append({"path": str(p.get_path()), "type": "AudioStreamPlayer", "playing": p.playing, "bus": p.bus}) - elif node is AudioStreamPlayer2D: - var p: AudioStreamPlayer2D = node as AudioStreamPlayer2D - results.append({"path": str(p.get_path()), "type": "AudioStreamPlayer2D", "playing": p.playing, "bus": p.bus}) - elif node is AudioStreamPlayer3D: - var p: AudioStreamPlayer3D = node as AudioStreamPlayer3D - results.append({"path": str(p.get_path()), "type": "AudioStreamPlayer3D", "playing": p.playing, "bus": p.bus}) - for child in node.get_children(): - _find_audio_players(child, results) - - -# --- Spawn Node --- -func _cmd_spawn_node(params: Dictionary) -> void: - var type_name: String = params.get("type", "") - var node_name: String = params.get("name", "") - var parent_path: String = params.get("parent_path", "/root") - - if type_name.is_empty(): - _send_response({"error": "type is required"}) - return - - if not ClassDB.class_exists(type_name): - _send_response({"error": "Unknown class: %s" % type_name}) - return - - if not ClassDB.is_parent_class(type_name, "Node") and type_name != "Node": - _send_response({"error": "Class '%s' is not a Node type" % type_name}) - return - - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent node not found: %s" % parent_path}) - return - - var instance: Node = ClassDB.instantiate(type_name) as Node - if instance == null: - _send_response({"error": "Failed to instantiate: %s" % type_name}) - return - - if node_name.length() > 0: - instance.name = node_name - - # Apply properties if provided - var properties: Dictionary = params.get("properties", {}) - for prop_name in properties: - var raw_value: Variant = properties[prop_name] - var value: Variant = _json_to_variant_for_property(instance, prop_name, raw_value) - instance.set(prop_name, value) - - parent.add_child(instance) - _send_response({"success": true, "name": instance.name, "type": type_name, "path": str(instance.get_path())}) - - -# --- Set Shader Parameter --- -func _cmd_set_shader_param(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var param_name: String = params.get("param_name", "") - if node_path.is_empty() or param_name.is_empty(): - _send_response({"error": "node_path and param_name are required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - var material: Material = null - # Try material_override first (MeshInstance3D/2D) - if node.get("material_override") != null: - material = node.get("material_override") - # Try surface override material (MeshInstance3D) - elif node.has_method("get_surface_override_material"): - material = node.get_surface_override_material(0) - # Try material property (CanvasItem, e.g. Sprite2D) - elif node.get("material") != null: - material = node.get("material") - - if material == null or not material is ShaderMaterial: - _send_response({"error": "No ShaderMaterial found on node: %s" % node_path}) - return - - var shader_mat: ShaderMaterial = material as ShaderMaterial - var raw_value: Variant = params.get("value", null) - var type_hint: String = params.get("type_hint", "") - var value: Variant = _json_to_variant(raw_value, type_hint) - shader_mat.set_shader_parameter(param_name, value) - _send_response({"success": true, "node_path": node_path, "param_name": param_name, "value": _variant_to_json(shader_mat.get_shader_parameter(param_name))}) - - -# --- Audio Play --- -func _cmd_audio_play(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var action: String = params.get("action", "play") - if node_path.is_empty(): - _send_response({"error": "node_path is required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - if not (node is AudioStreamPlayer or node is AudioStreamPlayer2D or node is AudioStreamPlayer3D): - _send_response({"error": "Node is not an AudioStreamPlayer: %s (is %s)" % [node_path, node.get_class()]}) - return - - # Optionally load a new stream - if params.has("stream"): - var stream_path: String = params["stream"] - var stream: AudioStream = load(stream_path) as AudioStream - if stream == null: - _send_response({"error": "Failed to load audio stream: %s" % stream_path}) - return - node.set("stream", stream) - - # Set optional properties - if params.has("volume"): - var linear_vol: float = float(params["volume"]) - node.set("volume_db", linear_to_db(clampf(linear_vol, 0.0, 1.0))) - if params.has("pitch"): - node.set("pitch_scale", float(params["pitch"])) - if params.has("bus"): - node.set("bus", params["bus"]) - - match action: - "play": - var from_pos: float = float(params.get("from_position", 0.0)) - node.call("play", from_pos) - _send_response({"success": true, "action": "play", "node_path": node_path}) - "stop": - node.call("stop") - _send_response({"success": true, "action": "stop", "node_path": node_path}) - "pause": - node.set("stream_paused", true) - _send_response({"success": true, "action": "pause", "node_path": node_path}) - "resume": - node.set("stream_paused", false) - _send_response({"success": true, "action": "resume", "node_path": node_path}) - _: - _send_response({"error": "Unknown audio action: %s. Use play, stop, pause, or resume" % action}) - - -# --- Audio Bus --- -func _cmd_audio_bus(params: Dictionary) -> void: - var bus_name: String = params.get("bus_name", "Master") - var bus_idx: int = AudioServer.get_bus_index(bus_name) - if bus_idx == -1: - _send_response({"error": "Audio bus not found: %s" % bus_name}) - return - - if params.has("volume"): - var linear_vol: float = float(params["volume"]) - AudioServer.set_bus_volume_db(bus_idx, linear_to_db(clampf(linear_vol, 0.0, 1.0))) - if params.has("mute"): - AudioServer.set_bus_mute(bus_idx, bool(params["mute"])) - if params.has("solo"): - AudioServer.set_bus_solo(bus_idx, bool(params["solo"])) - - _send_response({ - "success": true, - "bus_name": bus_name, - "volume_db": AudioServer.get_bus_volume_db(bus_idx), - "mute": AudioServer.is_bus_mute(bus_idx), - "solo": AudioServer.is_bus_solo(bus_idx) - }) - - -# --- Navigate Path --- -func _cmd_navigate_path(params: Dictionary) -> void: - var start_dict: Dictionary = params.get("start", {}) - var end_dict: Dictionary = params.get("end", {}) - var optimize: bool = params.get("optimize", true) - - if start_dict.is_empty() or end_dict.is_empty(): - _send_response({"error": "start and end are required"}) - return - - # Wait a frame to ensure navigation map is ready - await get_tree().process_frame - - var is_3d: bool = start_dict.has("z") or end_dict.has("z") - - if is_3d: - var start_pos: Vector3 = Vector3(float(start_dict.get("x", 0)), float(start_dict.get("y", 0)), float(start_dict.get("z", 0))) - var end_pos: Vector3 = Vector3(float(end_dict.get("x", 0)), float(end_dict.get("y", 0)), float(end_dict.get("z", 0))) - var map_rid: RID = get_tree().root.get_world_3d().get_navigation_map() - var path: PackedVector3Array = NavigationServer3D.map_get_path(map_rid, start_pos, end_pos, optimize) - var total_length: float = 0.0 - for i in range(1, path.size()): - total_length += path[i - 1].distance_to(path[i]) - _send_response({"success": true, "mode": "3d", "path": _variant_to_json(path), "point_count": path.size(), "total_length": total_length}) - else: - var start_pos: Vector2 = Vector2(float(start_dict.get("x", 0)), float(start_dict.get("y", 0))) - var end_pos: Vector2 = Vector2(float(end_dict.get("x", 0)), float(end_dict.get("y", 0))) - var map_rid: RID = get_tree().root.get_world_2d().get_navigation_map() - var path: PackedVector2Array = NavigationServer2D.map_get_path(map_rid, start_pos, end_pos, optimize) - var total_length: float = 0.0 - for i in range(1, path.size()): - total_length += path[i - 1].distance_to(path[i]) - _send_response({"success": true, "mode": "2d", "path": _variant_to_json(path), "point_count": path.size(), "total_length": total_length}) - - -# --- TileMap --- -func _cmd_tilemap(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var action: String = params.get("action", "get_cell") - if node_path.is_empty(): - _send_response({"error": "node_path is required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - if not node is TileMapLayer: - _send_response({"error": "Node is not a TileMapLayer: %s (is %s)" % [node_path, node.get_class()]}) - return - - var tilemap: TileMapLayer = node as TileMapLayer - - match action: - "set_cells": - var cells: Array = params.get("cells", []) - var count: int = 0 - for cell in cells: - var pos: Vector2i = Vector2i(int(cell.get("x", 0)), int(cell.get("y", 0))) - var source_id: int = int(cell.get("source_id", 0)) - var atlas_coords: Vector2i = Vector2i(int(cell.get("atlas_x", 0)), int(cell.get("atlas_y", 0))) - var alt_tile: int = int(cell.get("alt_tile", 0)) - tilemap.set_cell(pos, source_id, atlas_coords, alt_tile) - count += 1 - _send_response({"success": true, "action": "set_cells", "count": count}) - "get_cell": - var x: int = int(params.get("x", 0)) - var y: int = int(params.get("y", 0)) - var pos: Vector2i = Vector2i(x, y) - _send_response({ - "success": true, "action": "get_cell", - "x": x, "y": y, - "source_id": tilemap.get_cell_source_id(pos), - "atlas_coords": _variant_to_json(tilemap.get_cell_atlas_coords(pos)), - "alt_tile": tilemap.get_cell_alternative_tile(pos) - }) - "erase_cells": - var cells: Array = params.get("cells", []) - var count: int = 0 - for cell in cells: - tilemap.erase_cell(Vector2i(int(cell.get("x", 0)), int(cell.get("y", 0)))) - count += 1 - _send_response({"success": true, "action": "erase_cells", "count": count}) - "get_used_cells": - var source_filter: int = int(params.get("source_id", -1)) - var used: Array - if source_filter >= 0: - used = tilemap.get_used_cells_by_id(source_filter) - else: - used = tilemap.get_used_cells() - _send_response({"success": true, "action": "get_used_cells", "cells": _variant_to_json(used), "count": used.size()}) - _: - _send_response({"error": "Unknown tilemap action: %s. Use set_cells, get_cell, erase_cells, or get_used_cells" % action}) - - -# --- Add Collision Shape --- -func _cmd_add_collision(params: Dictionary) -> void: - var parent_path: String = params.get("parent_path", "") - var shape_type: String = params.get("shape_type", "") - if parent_path.is_empty() or shape_type.is_empty(): - _send_response({"error": "parent_path and shape_type are required"}) - return - - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent node not found: %s" % parent_path}) - return - - var is_3d: bool = parent.get_class().ends_with("3D") or parent is PhysicsBody3D or parent is Area3D - var shape_params: Dictionary = params.get("shape_params", {}) - var shape: Resource = null - - if is_3d: - match shape_type: - "box": - var s: BoxShape3D = BoxShape3D.new() - s.size = Vector3(float(shape_params.get("size_x", 1)), float(shape_params.get("size_y", 1)), float(shape_params.get("size_z", 1))) - shape = s - "sphere": - var s: SphereShape3D = SphereShape3D.new() - s.radius = float(shape_params.get("radius", 0.5)) - shape = s - "capsule": - var s: CapsuleShape3D = CapsuleShape3D.new() - s.radius = float(shape_params.get("radius", 0.5)) - s.height = float(shape_params.get("height", 2.0)) - shape = s - "cylinder": - var s: CylinderShape3D = CylinderShape3D.new() - s.radius = float(shape_params.get("radius", 0.5)) - s.height = float(shape_params.get("height", 2.0)) - shape = s - "ray": - var s: SeparationRayShape3D = SeparationRayShape3D.new() - s.length = float(shape_params.get("length", 1.0)) - shape = s - _: - _send_response({"error": "Unknown 3D shape type: %s. Use box, sphere, capsule, cylinder, or ray" % shape_type}) - return - var col_shape: CollisionShape3D = CollisionShape3D.new() - col_shape.shape = shape as Shape3D - if params.has("disabled"): - col_shape.disabled = bool(params["disabled"]) - parent.add_child(col_shape) - col_shape.owner = get_tree().edited_scene_root if get_tree().edited_scene_root else get_tree().root - if params.has("collision_layer"): - parent.set("collision_layer", int(params["collision_layer"])) - if params.has("collision_mask"): - parent.set("collision_mask", int(params["collision_mask"])) - _send_response({"success": true, "name": col_shape.name, "path": str(col_shape.get_path()), "shape_type": shape_type, "mode": "3d"}) - else: - match shape_type: - "box": - var s: RectangleShape2D = RectangleShape2D.new() - s.size = Vector2(float(shape_params.get("size_x", 1)), float(shape_params.get("size_y", 1))) - shape = s - "circle": - var s: CircleShape2D = CircleShape2D.new() - s.radius = float(shape_params.get("radius", 0.5)) - shape = s - "capsule": - var s: CapsuleShape2D = CapsuleShape2D.new() - s.radius = float(shape_params.get("radius", 0.5)) - s.height = float(shape_params.get("height", 2.0)) - shape = s - "segment": - var s: SegmentShape2D = SegmentShape2D.new() - s.a = Vector2(float(shape_params.get("a_x", 0)), float(shape_params.get("a_y", 0))) - s.b = Vector2(float(shape_params.get("b_x", 1)), float(shape_params.get("b_y", 0))) - shape = s - _: - _send_response({"error": "Unknown 2D shape type: %s. Use box, circle, capsule, or segment" % shape_type}) - return - var col_shape: CollisionShape2D = CollisionShape2D.new() - col_shape.shape = shape as Shape2D - if params.has("disabled"): - col_shape.disabled = bool(params["disabled"]) - parent.add_child(col_shape) - col_shape.owner = get_tree().edited_scene_root if get_tree().edited_scene_root else get_tree().root - if params.has("collision_layer"): - parent.set("collision_layer", int(params["collision_layer"])) - if params.has("collision_mask"): - parent.set("collision_mask", int(params["collision_mask"])) - _send_response({"success": true, "name": col_shape.name, "path": str(col_shape.get_path()), "shape_type": shape_type, "mode": "2d"}) - - -# --- Environment / Post-Processing --- -func _cmd_environment(params: Dictionary) -> void: - var action: String = params.get("action", "set") - - # Find existing WorldEnvironment or Camera3D environment - var env: Environment = null - var world_env: Node = null - - # Search for WorldEnvironment node - var found: Array = [] - _find_by_class_recursive(get_tree().root, "WorldEnvironment", found) - if found.size() > 0: - world_env = get_tree().root.get_node_or_null(found[0]["path"]) - if world_env != null: - env = world_env.get("environment") as Environment - - # Fallback: check Camera3D - if env == null: - var cam3d: Camera3D = get_viewport().get_camera_3d() - if cam3d != null and cam3d.get("environment") != null: - env = cam3d.get("environment") as Environment - - if action == "get": - if env == null: - _send_response({"error": "No Environment resource found"}) - return - _send_response(_get_environment_state(env)) - return - - # action == "set": create if needed - if env == null: - env = Environment.new() - var we: WorldEnvironment = WorldEnvironment.new() - we.environment = env - get_tree().root.add_child(we) - world_env = we - - # Apply settings - if params.has("background_mode"): - env.background_mode = int(params["background_mode"]) as Environment.BGMode - if params.has("background_color"): - var c: Dictionary = params["background_color"] - env.background_color = Color(float(c.get("r", 0)), float(c.get("g", 0)), float(c.get("b", 0)), float(c.get("a", 1))) - if params.has("ambient_light_color"): - var c: Dictionary = params["ambient_light_color"] - env.ambient_light_color = Color(float(c.get("r", 0)), float(c.get("g", 0)), float(c.get("b", 0)), float(c.get("a", 1))) - if params.has("ambient_light_energy"): - env.ambient_light_energy = float(params["ambient_light_energy"]) - if params.has("fog_enabled"): - env.fog_enabled = bool(params["fog_enabled"]) - if params.has("fog_density"): - env.fog_density = float(params["fog_density"]) - if params.has("fog_light_color"): - var c: Dictionary = params["fog_light_color"] - env.fog_light_color = Color(float(c.get("r", 0)), float(c.get("g", 0)), float(c.get("b", 0)), float(c.get("a", 1))) - if params.has("glow_enabled"): - env.glow_enabled = bool(params["glow_enabled"]) - if params.has("glow_intensity"): - env.glow_intensity = float(params["glow_intensity"]) - if params.has("glow_bloom"): - env.glow_bloom = float(params["glow_bloom"]) - if params.has("tonemap_mode"): - env.tonemap_mode = int(params["tonemap_mode"]) as Environment.ToneMapper - if params.has("ssao_enabled"): - env.ssao_enabled = bool(params["ssao_enabled"]) - if params.has("ssao_radius"): - env.ssao_radius = float(params["ssao_radius"]) - if params.has("ssao_intensity"): - env.ssao_intensity = float(params["ssao_intensity"]) - if params.has("ssr_enabled"): - env.ssr_enabled = bool(params["ssr_enabled"]) - if params.has("brightness"): - env.adjustment_enabled = true - env.adjustment_brightness = float(params["brightness"]) - if params.has("contrast"): - env.adjustment_enabled = true - env.adjustment_contrast = float(params["contrast"]) - if params.has("saturation"): - env.adjustment_enabled = true - env.adjustment_saturation = float(params["saturation"]) - - _send_response(_get_environment_state(env)) - - -func _get_environment_state(env: Environment) -> Dictionary: - return { - "success": true, - "background_mode": env.background_mode, - "background_color": _variant_to_json(env.background_color), - "ambient_light_color": _variant_to_json(env.ambient_light_color), - "ambient_light_energy": env.ambient_light_energy, - "fog_enabled": env.fog_enabled, - "fog_density": env.fog_density, - "fog_light_color": _variant_to_json(env.fog_light_color), - "glow_enabled": env.glow_enabled, - "glow_intensity": env.glow_intensity, - "glow_bloom": env.glow_bloom, - "tonemap_mode": env.tonemap_mode, - "ssao_enabled": env.ssao_enabled, - "ssao_radius": env.ssao_radius, - "ssao_intensity": env.ssao_intensity, - "ssr_enabled": env.ssr_enabled, - "brightness": env.adjustment_brightness, - "contrast": env.adjustment_contrast, - "saturation": env.adjustment_saturation - } - - -# --- Manage Group --- -func _cmd_manage_group(params: Dictionary) -> void: - var action: String = params.get("action", "") - var group_name: String = params.get("group", "") - - if action == "clear_group": - if group_name.is_empty(): - _send_response({"error": "group is required for clear_group"}) - return - var nodes: Array = get_tree().get_nodes_in_group(group_name) - for node in nodes: - node.remove_from_group(group_name) - _send_response({"success": true, "action": "clear_group", "group": group_name, "removed_count": nodes.size()}) - return - - var node_path: String = params.get("node_path", "") - if node_path.is_empty(): - _send_response({"error": "node_path is required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - match action: - "add": - if group_name.is_empty(): - _send_response({"error": "group is required for add"}) - return - node.add_to_group(group_name) - _send_response({"success": true, "action": "add", "node_path": node_path, "group": group_name}) - "remove": - if group_name.is_empty(): - _send_response({"error": "group is required for remove"}) - return - node.remove_from_group(group_name) - _send_response({"success": true, "action": "remove", "node_path": node_path, "group": group_name}) - "get_groups": - var groups: Array = [] - for g in node.get_groups(): - groups.append(str(g)) - _send_response({"success": true, "action": "get_groups", "node_path": node_path, "groups": groups}) - _: - _send_response({"error": "Unknown group action: %s. Use add, remove, get_groups, or clear_group" % action}) - - -# --- Create Timer --- -func _cmd_create_timer(params: Dictionary) -> void: - var parent_path: String = params.get("parent_path", "/root") - var wait_time: float = float(params.get("wait_time", 1.0)) - var one_shot: bool = params.get("one_shot", false) - var autostart: bool = params.get("autostart", false) - - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent node not found: %s" % parent_path}) - return - - var timer: Timer = Timer.new() - timer.wait_time = wait_time - timer.one_shot = one_shot - timer.autostart = autostart - if params.has("name") and params["name"] is String and not (params["name"] as String).is_empty(): - timer.name = params["name"] - parent.add_child(timer) - if autostart: - timer.start() - _send_response({"success": true, "path": str(timer.get_path()), "name": timer.name, "wait_time": timer.wait_time, "one_shot": timer.one_shot, "autostart": autostart}) - - -# --- Set Particles --- -func _cmd_set_particles(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - if node_path.is_empty(): - _send_response({"error": "node_path is required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - if not (node is GPUParticles2D or node is GPUParticles3D): - _send_response({"error": "Node is not a GPUParticles node: %s (is %s)" % [node_path, node.get_class()]}) - return - - # Set direct particle properties - if params.has("emitting"): - node.set("emitting", bool(params["emitting"])) - if params.has("amount"): - node.set("amount", int(params["amount"])) - if params.has("lifetime"): - node.set("lifetime", float(params["lifetime"])) - if params.has("one_shot"): - node.set("one_shot", bool(params["one_shot"])) - if params.has("speed_scale"): - node.set("speed_scale", float(params["speed_scale"])) - if params.has("explosiveness"): - node.set("explosiveness", float(params["explosiveness"])) - if params.has("randomness"): - node.set("randomness", float(params["randomness"])) - - # Configure process material - if params.has("process_material"): - var mat_params: Dictionary = params["process_material"] - var mat: ParticleProcessMaterial = node.get("process_material") as ParticleProcessMaterial - if mat == null: - mat = ParticleProcessMaterial.new() - node.set("process_material", mat) - if mat_params.has("direction"): - var d: Dictionary = mat_params["direction"] - mat.direction = Vector3(float(d.get("x", 0)), float(d.get("y", -1)), float(d.get("z", 0))) - if mat_params.has("spread"): - mat.spread = float(mat_params["spread"]) - if mat_params.has("gravity"): - var g: Dictionary = mat_params["gravity"] - mat.gravity = Vector3(float(g.get("x", 0)), float(g.get("y", -9.8)), float(g.get("z", 0))) - if mat_params.has("initial_velocity_min"): - mat.initial_velocity_min = float(mat_params["initial_velocity_min"]) - if mat_params.has("initial_velocity_max"): - mat.initial_velocity_max = float(mat_params["initial_velocity_max"]) - if mat_params.has("color"): - var c: Dictionary = mat_params["color"] - mat.color = Color(float(c.get("r", 1)), float(c.get("g", 1)), float(c.get("b", 1)), float(c.get("a", 1))) - if mat_params.has("scale_min"): - mat.scale_min = float(mat_params["scale_min"]) - if mat_params.has("scale_max"): - mat.scale_max = float(mat_params["scale_max"]) - - _send_response({ - "success": true, "node_path": node_path, - "emitting": node.get("emitting"), "amount": node.get("amount"), - "lifetime": node.get("lifetime"), "one_shot": node.get("one_shot"), - "speed_scale": node.get("speed_scale") - }) - - -# --- Create Animation --- -func _cmd_create_animation(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var anim_name: String = params.get("animation_name", "") - if node_path.is_empty() or anim_name.is_empty(): - _send_response({"error": "node_path and animation_name are required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - if not node is AnimationPlayer: - _send_response({"error": "Node is not an AnimationPlayer: %s (is %s)" % [node_path, node.get_class()]}) - return - - var anim_player: AnimationPlayer = node as AnimationPlayer - var anim: Animation = Animation.new() - anim.length = float(params.get("length", 1.0)) - var loop_mode: int = int(params.get("loop_mode", 0)) - anim.loop_mode = loop_mode as Animation.LoopMode - - var tracks: Array = params.get("tracks", []) - var track_count: int = 0 - for track_data in tracks: - var track_type_str: String = track_data.get("type", "value") - var track_path: String = track_data.get("path", "") - if track_path.is_empty(): - continue - - var track_type: int = Animation.TYPE_VALUE - match track_type_str: - "value": - track_type = Animation.TYPE_VALUE - "method": - track_type = Animation.TYPE_METHOD - "bezier": - track_type = Animation.TYPE_BEZIER - "audio": - track_type = Animation.TYPE_AUDIO - - var idx: int = anim.add_track(track_type) - anim.track_set_path(idx, NodePath(track_path)) - - var keys: Array = track_data.get("keys", []) - for key_data in keys: - var time: float = float(key_data.get("time", 0.0)) - match track_type: - Animation.TYPE_VALUE: - var value: Variant = _json_to_variant(key_data.get("value", null), key_data.get("type_hint", "")) - anim.track_insert_key(idx, time, value) - if key_data.has("transition"): - var key_idx: int = anim.track_find_key(idx, time, Animation.FIND_MODE_APPROX) - if key_idx >= 0: - anim.track_set_key_transition(idx, key_idx, float(key_data["transition"])) - Animation.TYPE_METHOD: - var method_name: String = key_data.get("method", "") - var args: Array = key_data.get("args", []) - anim.track_insert_key(idx, time, {"method": method_name, "args": args}) - Animation.TYPE_BEZIER: - var value: float = float(key_data.get("value", 0.0)) - anim.bezier_track_insert_key(idx, time, value) - Animation.TYPE_AUDIO: - var stream_path: String = key_data.get("stream", "") - if not stream_path.is_empty(): - var stream: AudioStream = load(stream_path) as AudioStream - if stream != null: - anim.audio_track_insert_key(idx, time, stream) - track_count += 1 - - # Add to library (use default "" library if it exists, otherwise create it) - var lib_name: String = params.get("library", "") - var lib: AnimationLibrary = null - if anim_player.has_animation_library(lib_name): - lib = anim_player.get_animation_library(lib_name) - else: - lib = AnimationLibrary.new() - anim_player.add_animation_library(lib_name, lib) - lib.add_animation(anim_name, anim) - - _send_response({"success": true, "animation_name": anim_name, "length": anim.length, "loop_mode": loop_mode, "track_count": track_count}) - - -# --- Serialize State --- -func _cmd_serialize_state(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "/root") - var action: String = params.get("action", "save") - var max_depth: int = int(params.get("max_depth", 5)) - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - match action: - "save": - var state: Dictionary = _serialize_node(node, max_depth, 0) - _send_response({"success": true, "action": "save", "state": state}) - "load": - var data: Dictionary = params.get("data", {}) - if data.is_empty(): - _send_response({"error": "data is required for load action"}) - return - var count: int = _deserialize_node(node, data) - _send_response({"success": true, "action": "load", "restored_count": count}) - _: - _send_response({"error": "Unknown serialize action: %s. Use save or load" % action}) - - -func _serialize_node(node: Node, max_depth: int, depth: int) -> Dictionary: - var result: Dictionary = { - "class": node.get_class(), - "name": node.name, - "path": str(node.get_path()), - } - # Capture editor-visible properties - var props: Dictionary = {} - for prop in node.get_property_list(): - var prop_dict: Dictionary = prop - if prop_dict.get("usage", 0) & PROPERTY_USAGE_STORAGE: - var prop_name: String = prop_dict.get("name", "") - if prop_name.is_empty() or prop_name.begins_with("_"): - continue - props[prop_name] = _variant_to_json(node.get(prop_name)) - result["properties"] = props - - if depth < max_depth: - var children: Array = [] - for child in node.get_children(): - # Skip the MCP interaction server itself - if child == self: - continue - children.append(_serialize_node(child, max_depth, depth + 1)) - result["children"] = children - - return result - - -func _deserialize_node(node: Node, data: Dictionary) -> int: - var count: int = 0 - # Restore properties - var props: Dictionary = data.get("properties", {}) - for prop_name in props: - var value: Variant = _json_to_variant_for_property(node, prop_name, props[prop_name]) - node.set(prop_name, value) - count += 1 - - # Restore children - var children_data: Array = data.get("children", []) - for child_data in children_data: - var child_name: String = child_data.get("name", "") - var child: Node = null - for c in node.get_children(): - if c.name == child_name: - child = c - break - if child != null: - count += _deserialize_node(child, child_data) - return count - - -# --- Physics Body --- -func _cmd_physics_body(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - if node_path.is_empty(): - _send_response({"error": "node_path is required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - if not (node is PhysicsBody2D or node is PhysicsBody3D): - _send_response({"error": "Node is not a PhysicsBody: %s (is %s)" % [node_path, node.get_class()]}) - return - - # Set common physics properties - if params.has("gravity_scale") and node.get("gravity_scale") != null: - node.set("gravity_scale", float(params["gravity_scale"])) - if params.has("mass") and node.get("mass") != null: - node.set("mass", float(params["mass"])) - if params.has("freeze") and node.get("freeze") != null: - node.set("freeze", bool(params["freeze"])) - if params.has("sleeping") and node.get("sleeping") != null: - node.set("sleeping", bool(params["sleeping"])) - if params.has("linear_damp") and node.get("linear_damp") != null: - node.set("linear_damp", float(params["linear_damp"])) - if params.has("angular_damp") and node.get("angular_damp") != null: - node.set("angular_damp", float(params["angular_damp"])) - - # Velocity (2D vs 3D) - if params.has("linear_velocity"): - var lv: Dictionary = params["linear_velocity"] - if node is PhysicsBody3D: - node.set("linear_velocity", Vector3(float(lv.get("x", 0)), float(lv.get("y", 0)), float(lv.get("z", 0)))) - else: - node.set("linear_velocity", Vector2(float(lv.get("x", 0)), float(lv.get("y", 0)))) - if params.has("angular_velocity"): - var av: Variant = params["angular_velocity"] - if node is PhysicsBody3D and av is Dictionary: - node.set("angular_velocity", Vector3(float(av.get("x", 0)), float(av.get("y", 0)), float(av.get("z", 0)))) - else: - node.set("angular_velocity", float(av)) - - # Physics material (friction, bounce) - if params.has("friction") or params.has("bounce"): - var phys_mat: PhysicsMaterial = node.get("physics_material_override") as PhysicsMaterial - if phys_mat == null: - phys_mat = PhysicsMaterial.new() - node.set("physics_material_override", phys_mat) - if params.has("friction"): - phys_mat.friction = float(params["friction"]) - if params.has("bounce"): - phys_mat.bounce = float(params["bounce"]) - - # Build response - var result: Dictionary = {"success": true, "node_path": node_path, "class": node.get_class()} - if node.get("mass") != null: - result["mass"] = node.get("mass") - if node.get("gravity_scale") != null: - result["gravity_scale"] = node.get("gravity_scale") - if node.get("linear_velocity") != null: - result["linear_velocity"] = _variant_to_json(node.get("linear_velocity")) - if node.get("angular_velocity") != null: - result["angular_velocity"] = _variant_to_json(node.get("angular_velocity")) - _send_response(result) - - -# --- Create Joint --- -func _cmd_create_joint(params: Dictionary) -> void: - var parent_path: String = params.get("parent_path", "") - var joint_type: String = params.get("joint_type", "") - if parent_path.is_empty() or joint_type.is_empty(): - _send_response({"error": "parent_path and joint_type are required"}) - return - - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent node not found: %s" % parent_path}) - return - - var node_a: String = params.get("node_a_path", "") - var node_b: String = params.get("node_b_path", "") - var joint: Node = null - - match joint_type: - "pin_2d": - var j: PinJoint2D = PinJoint2D.new() - if not node_a.is_empty(): - j.node_a = NodePath(node_a) - if not node_b.is_empty(): - j.node_b = NodePath(node_b) - if params.has("softness"): - j.softness = float(params["softness"]) - joint = j - "spring_2d": - var j: DampedSpringJoint2D = DampedSpringJoint2D.new() - if not node_a.is_empty(): - j.node_a = NodePath(node_a) - if not node_b.is_empty(): - j.node_b = NodePath(node_b) - if params.has("length"): - j.length = float(params["length"]) - if params.has("rest_length"): - j.rest_length = float(params["rest_length"]) - if params.has("stiffness"): - j.stiffness = float(params["stiffness"]) - if params.has("damping"): - j.damping = float(params["damping"]) - joint = j - "groove_2d": - var j: GrooveJoint2D = GrooveJoint2D.new() - if not node_a.is_empty(): - j.node_a = NodePath(node_a) - if not node_b.is_empty(): - j.node_b = NodePath(node_b) - if params.has("length"): - j.length = float(params["length"]) - if params.has("initial_offset"): - j.initial_offset = float(params["initial_offset"]) - joint = j - "pin_3d": - var j: PinJoint3D = PinJoint3D.new() - if not node_a.is_empty(): - j.node_a = NodePath(node_a) - if not node_b.is_empty(): - j.node_b = NodePath(node_b) - joint = j - "hinge_3d": - var j: HingeJoint3D = HingeJoint3D.new() - if not node_a.is_empty(): - j.node_a = NodePath(node_a) - if not node_b.is_empty(): - j.node_b = NodePath(node_b) - joint = j - "cone_3d": - var j: ConeTwistJoint3D = ConeTwistJoint3D.new() - if not node_a.is_empty(): - j.node_a = NodePath(node_a) - if not node_b.is_empty(): - j.node_b = NodePath(node_b) - joint = j - "slider_3d": - var j: SliderJoint3D = SliderJoint3D.new() - if not node_a.is_empty(): - j.node_a = NodePath(node_a) - if not node_b.is_empty(): - j.node_b = NodePath(node_b) - joint = j - _: - _send_response({"error": "Unknown joint type: %s. Use pin_2d, spring_2d, groove_2d, pin_3d, hinge_3d, cone_3d, or slider_3d" % joint_type}) - return - - parent.add_child(joint) - _send_response({"success": true, "joint_type": joint_type, "name": joint.name, "path": str(joint.get_path())}) - - -# --- Bone Pose --- -func _cmd_bone_pose(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var action: String = params.get("action", "list") - if node_path.is_empty(): - _send_response({"error": "node_path is required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - if not node is Skeleton3D: - _send_response({"error": "Node is not a Skeleton3D: %s (is %s)" % [node_path, node.get_class()]}) - return - - var skel: Skeleton3D = node as Skeleton3D - - match action: - "list": - var bones: Array = [] - for i in skel.get_bone_count(): - bones.append({"index": i, "name": skel.get_bone_name(i), "parent": skel.get_bone_parent(i)}) - _send_response({"success": true, "action": "list", "bone_count": skel.get_bone_count(), "bones": bones}) - "get": - var bone_idx: int = _resolve_bone_index(skel, params) - if bone_idx < 0: - _send_response({"error": "Bone not found"}) - return - _send_response({ - "success": true, "action": "get", "bone_index": bone_idx, - "bone_name": skel.get_bone_name(bone_idx), - "position": _variant_to_json(skel.get_bone_pose_position(bone_idx)), - "rotation": _variant_to_json(skel.get_bone_pose_rotation(bone_idx)), - "scale": _variant_to_json(skel.get_bone_pose_scale(bone_idx)) - }) - "set": - var bone_idx: int = _resolve_bone_index(skel, params) - if bone_idx < 0: - _send_response({"error": "Bone not found"}) - return - if params.has("position"): - var p: Dictionary = params["position"] - skel.set_bone_pose_position(bone_idx, Vector3(float(p.get("x", 0)), float(p.get("y", 0)), float(p.get("z", 0)))) - if params.has("rotation"): - var r: Dictionary = params["rotation"] - skel.set_bone_pose_rotation(bone_idx, Quaternion(float(r.get("x", 0)), float(r.get("y", 0)), float(r.get("z", 0)), float(r.get("w", 1)))) - if params.has("scale"): - var s: Dictionary = params["scale"] - skel.set_bone_pose_scale(bone_idx, Vector3(float(s.get("x", 1)), float(s.get("y", 1)), float(s.get("z", 1)))) - _send_response({"success": true, "action": "set", "bone_index": bone_idx, "bone_name": skel.get_bone_name(bone_idx)}) - _: - _send_response({"error": "Unknown bone action: %s. Use list, get, or set" % action}) - - -func _resolve_bone_index(skel: Skeleton3D, params: Dictionary) -> int: - if params.has("bone_index"): - return int(params["bone_index"]) - if params.has("bone_name"): - return skel.find_bone(params["bone_name"]) - return -1 - - -# --- UI Theme --- -func _cmd_ui_theme(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - if node_path.is_empty(): - _send_response({"error": "node_path is required"}) - return - - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - - if not node is Control: - _send_response({"error": "Node is not a Control: %s (is %s)" % [node_path, node.get_class()]}) - return - - var ctrl: Control = node as Control - var overrides: Dictionary = params.get("overrides", {}) - var applied: Array = [] - - # Color overrides - var colors: Dictionary = overrides.get("colors", {}) - for name in colors: - var c: Dictionary = colors[name] - ctrl.add_theme_color_override(name, Color(float(c.get("r", 0)), float(c.get("g", 0)), float(c.get("b", 0)), float(c.get("a", 1)))) - applied.append("color:" + name) - - # Constant overrides - var constants: Dictionary = overrides.get("constants", {}) - for name in constants: - ctrl.add_theme_constant_override(name, int(constants[name])) - applied.append("constant:" + name) - - # Font size overrides - var font_sizes: Dictionary = overrides.get("font_sizes", {}) - for name in font_sizes: - ctrl.add_theme_font_size_override(name, int(font_sizes[name])) - applied.append("font_size:" + name) - - _send_response({"success": true, "node_path": node_path, "applied": applied}) - - -# --- Viewport --- -func _cmd_viewport(params: Dictionary) -> void: - var action: String = params.get("action", "create") - - match action: - "create": - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent node not found: %s" % parent_path}) - return - var viewport: SubViewport = SubViewport.new() - if params.has("width") and params.has("height"): - viewport.size = Vector2i(int(params["width"]), int(params["height"])) - if params.has("transparent_bg"): - viewport.transparent_bg = bool(params["transparent_bg"]) - if params.has("msaa"): - viewport.msaa_2d = int(params["msaa"]) as Viewport.MSAA - viewport.msaa_3d = int(params["msaa"]) as Viewport.MSAA - if params.has("name") and params["name"] is String and not (params["name"] as String).is_empty(): - viewport.name = params["name"] - var container: SubViewportContainer = SubViewportContainer.new() - container.add_child(viewport) - parent.add_child(container) - _send_response({"success": true, "action": "create", "viewport_path": str(viewport.get_path()), "container_path": str(container.get_path()), "size": _variant_to_json(viewport.size)}) - "configure": - var node_path: String = params.get("node_path", "") - if node_path.is_empty(): - _send_response({"error": "node_path is required for configure"}) - return - var vp: Node = get_tree().root.get_node_or_null(node_path) - if vp == null or not vp is SubViewport: - _send_response({"error": "SubViewport not found: %s" % node_path}) - return - var sv: SubViewport = vp as SubViewport - if params.has("width") and params.has("height"): - sv.size = Vector2i(int(params["width"]), int(params["height"])) - if params.has("transparent_bg"): - sv.transparent_bg = bool(params["transparent_bg"]) - if params.has("msaa"): - sv.msaa_2d = int(params["msaa"]) as Viewport.MSAA - sv.msaa_3d = int(params["msaa"]) as Viewport.MSAA - _send_response({"success": true, "action": "configure", "size": _variant_to_json(sv.size), "transparent_bg": sv.transparent_bg}) - "get": - var node_path: String = params.get("node_path", "") - if node_path.is_empty(): - _send_response({"error": "node_path is required for get"}) - return - var vp: Node = get_tree().root.get_node_or_null(node_path) - if vp == null or not vp is SubViewport: - _send_response({"error": "SubViewport not found: %s" % node_path}) - return - var sv: SubViewport = vp as SubViewport - _send_response({"success": true, "action": "get", "size": _variant_to_json(sv.size), "transparent_bg": sv.transparent_bg, "msaa_2d": sv.msaa_2d, "msaa_3d": sv.msaa_3d}) - _: - _send_response({"error": "Unknown viewport action: %s. Use create, configure, or get" % action}) - - -# --- Debug Draw --- -var _debug_draw_node: Node = null -var _debug_meshes: Array = [] - -func _cmd_debug_draw(params: Dictionary) -> void: - var action: String = params.get("action", "line") - var color_dict: Dictionary = params.get("color", {"r": 1.0, "g": 0.0, "b": 0.0}) - var color: Color = Color(float(color_dict.get("r", 1)), float(color_dict.get("g", 0)), float(color_dict.get("b", 0)), float(color_dict.get("a", 1))) - var duration: int = int(params.get("duration", 0)) - - if action == "clear": - _clear_debug_draw() - _send_response({"success": true, "action": "clear"}) - return - - # Ensure we have a debug draw parent - if _debug_draw_node == null or not is_instance_valid(_debug_draw_node): - _debug_draw_node = Node3D.new() - _debug_draw_node.name = "_McpDebugDraw" - get_tree().root.add_child(_debug_draw_node) - - var mat: StandardMaterial3D = StandardMaterial3D.new() - mat.albedo_color = color - mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED - mat.no_depth_test = true - mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA if color.a < 1.0 else BaseMaterial3D.TRANSPARENCY_DISABLED - - match action: - "line": - var from_dict: Dictionary = params.get("from", {}) - var to_dict: Dictionary = params.get("to", {}) - var from_pos: Vector3 = Vector3(float(from_dict.get("x", 0)), float(from_dict.get("y", 0)), float(from_dict.get("z", 0))) - var to_pos: Vector3 = Vector3(float(to_dict.get("x", 0)), float(to_dict.get("y", 0)), float(to_dict.get("z", 0))) - var im: ImmediateMesh = ImmediateMesh.new() - im.surface_begin(Mesh.PRIMITIVE_LINES, mat) - im.surface_add_vertex(from_pos) - im.surface_add_vertex(to_pos) - im.surface_end() - var mi: MeshInstance3D = MeshInstance3D.new() - mi.mesh = im - _debug_draw_node.add_child(mi) - _debug_meshes.append({"node": mi, "frames_left": duration}) - _send_response({"success": true, "action": "line"}) - "sphere": - var center_dict: Dictionary = params.get("center", {}) - var center: Vector3 = Vector3(float(center_dict.get("x", 0)), float(center_dict.get("y", 0)), float(center_dict.get("z", 0))) - var radius: float = float(params.get("radius", 0.5)) - var sphere_mesh: SphereMesh = SphereMesh.new() - sphere_mesh.radius = radius - sphere_mesh.height = radius * 2.0 - sphere_mesh.material = mat - var mi: MeshInstance3D = MeshInstance3D.new() - mi.mesh = sphere_mesh - mi.global_position = center - _debug_draw_node.add_child(mi) - _debug_meshes.append({"node": mi, "frames_left": duration}) - _send_response({"success": true, "action": "sphere"}) - "box": - var center_dict: Dictionary = params.get("center", {}) - var center: Vector3 = Vector3(float(center_dict.get("x", 0)), float(center_dict.get("y", 0)), float(center_dict.get("z", 0))) - var size_dict: Dictionary = params.get("size", {"x": 1, "y": 1, "z": 1}) - var box_size: Vector3 = Vector3(float(size_dict.get("x", 1)), float(size_dict.get("y", 1)), float(size_dict.get("z", 1))) - var box_mesh: BoxMesh = BoxMesh.new() - box_mesh.size = box_size - box_mesh.material = mat - var mi: MeshInstance3D = MeshInstance3D.new() - mi.mesh = box_mesh - mi.global_position = center - _debug_draw_node.add_child(mi) - _debug_meshes.append({"node": mi, "frames_left": duration}) - _send_response({"success": true, "action": "box"}) - _: - _send_response({"error": "Unknown debug draw action: %s. Use line, sphere, box, or clear" % action}) - - -func _clear_debug_draw() -> void: - for entry in _debug_meshes: - if is_instance_valid(entry["node"]): - entry["node"].queue_free() - _debug_meshes.clear() - if _debug_draw_node != null and is_instance_valid(_debug_draw_node): - _debug_draw_node.queue_free() - _debug_draw_node = null - - -# ========================================================================== -# Batch 1: Networking + Input + System + Signals + Script -# ========================================================================== - -func _cmd_http_request(params: Dictionary) -> void: - var url: String = params.get("url", "") - if url.is_empty(): - _send_response({"error": "url is required"}) - return - var method_str: String = params.get("method", "GET").to_upper() - var http: HTTPRequest = HTTPRequest.new() - http.timeout = float(params.get("timeout", 30)) - add_child(http) - var headers: PackedStringArray = PackedStringArray() - if params.has("headers"): - var h: Dictionary = params["headers"] - for k in h: - headers.append("%s: %s" % [k, str(h[k])]) - var method_enum: int = HTTPClient.METHOD_GET - match method_str: - "POST": method_enum = HTTPClient.METHOD_POST - "PUT": method_enum = HTTPClient.METHOD_PUT - "DELETE": method_enum = HTTPClient.METHOD_DELETE - var body: String = params.get("body", "") - var err: int = http.request(url, headers, method_enum, body) - if err != OK: - http.queue_free() - _send_response({"error": "HTTP request failed to start: %d" % err}) - return - var result: Array = await http.request_completed - http.queue_free() - _send_response({"success": true, "status_code": result[1], "body": result[3].get_string_from_utf8()}) - - -var _websocket: WebSocketPeer = null - -func _cmd_websocket(params: Dictionary) -> void: - var action: String = params.get("action", "") - match action: - "connect": - var url: String = params.get("url", "") - if url.is_empty(): - _send_response({"error": "url is required for connect"}) - return - _websocket = WebSocketPeer.new() - var err: int = _websocket.connect_to_url(url) - if err != OK: - _send_response({"error": "WebSocket connect failed: %d" % err}) - _websocket = null - return - _send_response({"success": true, "action": "connect", "url": url}) - "disconnect": - if _websocket != null: - _websocket.close() - _websocket = null - _send_response({"success": true, "action": "disconnect"}) - "send": - if _websocket == null: - _send_response({"error": "No WebSocket connection"}) - return - _websocket.poll() - var msg: String = params.get("message", "") - _websocket.send_text(msg) - _send_response({"success": true, "action": "send"}) - "status": - if _websocket == null: - _send_response({"success": true, "status": "disconnected"}) - return - _websocket.poll() - _send_response({"success": true, "status": _websocket.get_ready_state()}) - _: - _send_response({"error": "Unknown websocket action: %s" % action}) - - -func _cmd_multiplayer(params: Dictionary) -> void: - var action: String = params.get("action", "") - match action: - "create_server": - var peer: ENetMultiplayerPeer = ENetMultiplayerPeer.new() - var port: int = int(params.get("port", 7000)) - var max_cl: int = int(params.get("max_clients", 32)) - var err: int = peer.create_server(port, max_cl) - if err != OK: - _send_response({"error": "Failed to create server: %d" % err}) - return - multiplayer.multiplayer_peer = peer - _send_response({"success": true, "action": "create_server", "port": port}) - "create_client": - var peer: ENetMultiplayerPeer = ENetMultiplayerPeer.new() - var address: String = params.get("address", "127.0.0.1") - var port: int = int(params.get("port", 7000)) - var err: int = peer.create_client(address, port) - if err != OK: - _send_response({"error": "Failed to create client: %d" % err}) - return - multiplayer.multiplayer_peer = peer - _send_response({"success": true, "action": "create_client", "address": address, "port": port}) - "disconnect": - multiplayer.multiplayer_peer = null - _send_response({"success": true, "action": "disconnect"}) - "status": - var peer = multiplayer.multiplayer_peer - if peer == null: - _send_response({"success": true, "connected": false}) - return - _send_response({"success": true, "connected": true, "unique_id": multiplayer.get_unique_id(), "is_server": multiplayer.is_server()}) - _: - _send_response({"error": "Unknown multiplayer action: %s" % action}) - - -func _cmd_rpc(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - var action: String = params.get("action", "call") - var method: String = params.get("method", "") - if action == "call": - var args: Array = params.get("args", []) - node.rpc(method, args) - _send_response({"success": true, "action": "call", "method": method}) - else: - _send_response({"success": true, "action": action, "method": method}) - - -func _cmd_touch(params: Dictionary) -> void: - var action: String = params.get("action", "press") - var x: float = float(params.get("x", 0)) - var y: float = float(params.get("y", 0)) - var idx: int = int(params.get("index", 0)) - match action: - "press": - var ev: InputEventScreenTouch = InputEventScreenTouch.new() - ev.index = idx - ev.position = Vector2(x, y) - ev.pressed = true - Input.parse_input_event(ev) - await get_tree().process_frame - _send_response({"success": true, "action": "press", "x": x, "y": y}) - "release": - var ev: InputEventScreenTouch = InputEventScreenTouch.new() - ev.index = idx - ev.position = Vector2(x, y) - ev.pressed = false - Input.parse_input_event(ev) - await get_tree().process_frame - _send_response({"success": true, "action": "release", "x": x, "y": y}) - "drag": - var to_x: float = float(params.get("to_x", x)) - var to_y: float = float(params.get("to_y", y)) - var steps: int = int(params.get("steps", 10)) - var press_ev: InputEventScreenTouch = InputEventScreenTouch.new() - press_ev.index = idx - press_ev.position = Vector2(x, y) - press_ev.pressed = true - Input.parse_input_event(press_ev) - for i in range(steps): - var t: float = float(i + 1) / float(steps) - var drag_ev: InputEventScreenDrag = InputEventScreenDrag.new() - drag_ev.index = idx - drag_ev.position = Vector2(lerp(x, to_x, t), lerp(y, to_y, t)) - Input.parse_input_event(drag_ev) - await get_tree().process_frame - var rel_ev: InputEventScreenTouch = InputEventScreenTouch.new() - rel_ev.index = idx - rel_ev.position = Vector2(to_x, to_y) - rel_ev.pressed = false - Input.parse_input_event(rel_ev) - await get_tree().process_frame - _send_response({"success": true, "action": "drag", "from": {"x": x, "y": y}, "to": {"x": to_x, "y": to_y}}) - _: - _send_response({"error": "Unknown touch action: %s" % action}) - - -func _cmd_input_state(params: Dictionary) -> void: - var action: String = params.get("action", "query") - match action: - "query": - var mouse_pos: Vector2 = get_viewport().get_mouse_position() - var joypads: Array = Input.get_connected_joypads() - _send_response({"success": true, "mouse_position": {"x": mouse_pos.x, "y": mouse_pos.y}, "connected_joypads": joypads.size()}) - "warp_mouse": - var pos: Vector2 = Vector2(float(params.get("x", 0)), float(params.get("y", 0))) - Input.warp_mouse(pos) - _send_response({"success": true, "action": "warp_mouse", "position": {"x": pos.x, "y": pos.y}}) - "set_mouse_mode": - var mode_str: String = params.get("mouse_mode", "visible") - var mode_val: int = Input.MOUSE_MODE_VISIBLE - match mode_str: - "hidden": mode_val = Input.MOUSE_MODE_HIDDEN - "captured": mode_val = Input.MOUSE_MODE_CAPTURED - "confined": mode_val = Input.MOUSE_MODE_CONFINED - Input.mouse_mode = mode_val - _send_response({"success": true, "action": "set_mouse_mode", "mode": mode_str}) - _: - _send_response({"error": "Unknown input_state action: %s" % action}) - - -func _cmd_input_action(params: Dictionary) -> void: - var action: String = params.get("action", "") - match action: - "set_strength": - var action_name: String = params.get("action_name", "") - var strength: float = float(params.get("strength", 1.0)) - Input.action_press(action_name, strength) - _send_response({"success": true, "action": "set_strength", "action_name": action_name, "strength": strength}) - "add_action": - var action_name: String = params.get("action_name", "") - if not InputMap.has_action(action_name): - InputMap.add_action(action_name) - if params.has("key"): - var ev: InputEventKey = InputEventKey.new() - ev.keycode = OS.find_keycode_from_string(params["key"]) - InputMap.action_add_event(action_name, ev) - _send_response({"success": true, "action": "add_action", "action_name": action_name}) - "remove_action": - var action_name: String = params.get("action_name", "") - if InputMap.has_action(action_name): - InputMap.erase_action(action_name) - _send_response({"success": true, "action": "remove_action", "action_name": action_name}) - "list": - var actions: Array = InputMap.get_actions() - _send_response({"success": true, "actions": actions}) - _: - _send_response({"error": "Unknown input_action action: %s" % action}) - - -func _cmd_list_signals(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - var signals: Array = [] - for sig in node.get_signal_list(): - var connections: Array = [] - for conn in node.get_signal_connection_list(sig["name"]): - connections.append({"callable": str(conn["callable"]), "flags": conn["flags"]}) - signals.append({"name": sig["name"], "args": str(sig["args"]), "connections": connections}) - _send_response({"success": true, "node_path": node_path, "signals": signals}) - - -func _cmd_await_signal(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var signal_name: String = params.get("signal_name", "") - var timeout: float = float(params.get("timeout", 10)) - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - if not node.has_signal(signal_name): - _send_response({"error": "Signal not found: %s on %s" % [signal_name, node_path]}) - return - var timer: SceneTreeTimer = get_tree().create_timer(timeout) - var result: Array = [false, []] - var cb: Callable = func(): - result[0] = true - node.connect(signal_name, cb, CONNECT_ONE_SHOT) - while not result[0] and timer.time_left > 0: - await get_tree().process_frame - if node.is_connected(signal_name, cb): - node.disconnect(signal_name, cb) - if result[0]: - _send_response({"success": true, "signal_name": signal_name, "received": true}) - else: - _send_response({"success": true, "signal_name": signal_name, "received": false, "timeout": true}) - - -func _cmd_script(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - var action: String = params.get("action", "get_source") - match action: - "get_source": - var s = node.get_script() - if s == null: - _send_response({"success": true, "has_script": false}) - return - _send_response({"success": true, "has_script": true, "source": s.source_code if s is GDScript else "", "path": s.resource_path}) - "attach": - var source: String = params.get("source", "") - if source.is_empty(): - _send_response({"error": "source is required for attach"}) - return - var s: GDScript = GDScript.new() - s.source_code = source - var err: int = s.reload() - if err != OK: - _send_response({"error": "Script compile error: %d" % err}) - return - node.set_script(s) - _send_response({"success": true, "action": "attach", "node_path": node_path}) - "detach": - node.set_script(null) - _send_response({"success": true, "action": "detach", "node_path": node_path}) - _: - _send_response({"error": "Unknown script action: %s" % action}) - - -func _cmd_window(params: Dictionary) -> void: - var action: String = params.get("action", "get") - var win: Window = get_tree().root - if action == "get": - _send_response({"success": true, "size": {"x": win.size.x, "y": win.size.y}, "position": {"x": win.position.x, "y": win.position.y}, "fullscreen": win.mode == Window.MODE_FULLSCREEN, "borderless": win.borderless, "title": win.title}) - return - if params.has("width") and params.has("height"): - win.size = Vector2i(int(params["width"]), int(params["height"])) - if params.has("fullscreen"): - win.mode = Window.MODE_FULLSCREEN if bool(params["fullscreen"]) else Window.MODE_WINDOWED - if params.has("borderless"): - win.borderless = bool(params["borderless"]) - if params.has("title"): - win.title = str(params["title"]) - if params.has("position"): - var p: Dictionary = params["position"] - win.position = Vector2i(int(p.get("x", 0)), int(p.get("y", 0))) - if params.has("vsync"): - DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED if bool(params["vsync"]) else DisplayServer.VSYNC_DISABLED) - _send_response({"success": true, "action": "set", "size": {"x": win.size.x, "y": win.size.y}}) - - -func _cmd_os_info() -> void: - var screen_size: Vector2i = DisplayServer.screen_get_size() - _send_response({"success": true, "os_name": OS.get_name(), "locale": OS.get_locale(), "screen_size": {"x": screen_size.x, "y": screen_size.y}, "video_adapter": RenderingServer.get_video_adapter_name(), "processor_count": OS.get_processor_count()}) - - -func _cmd_time_scale(params: Dictionary) -> void: - var action: String = params.get("action", "get") - if action == "set": - Engine.time_scale = float(params.get("time_scale", 1.0)) - _send_response({"success": true, "time_scale": Engine.time_scale, "ticks_msec": Time.get_ticks_msec(), "fps": Engine.get_frames_per_second()}) - - -func _cmd_process_mode(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - var mode_str: String = params.get("mode", "inherit") - var mode_val: int = Node.PROCESS_MODE_INHERIT - match mode_str: - "pausable": mode_val = Node.PROCESS_MODE_PAUSABLE - "when_paused": mode_val = Node.PROCESS_MODE_WHEN_PAUSED - "always": mode_val = Node.PROCESS_MODE_ALWAYS - "disabled": mode_val = Node.PROCESS_MODE_DISABLED - node.process_mode = mode_val - _send_response({"success": true, "node_path": node_path, "mode": mode_str}) - - -func _cmd_world_settings(params: Dictionary) -> void: - var action: String = params.get("action", "get") - if action == "set": - if params.has("gravity"): - ProjectSettings.set_setting("physics/3d/default_gravity", float(params["gravity"])) - if params.has("physics_fps"): - Engine.physics_ticks_per_second = int(params["physics_fps"]) - _send_response({"success": true, "gravity": ProjectSettings.get_setting("physics/3d/default_gravity"), "physics_fps": Engine.physics_ticks_per_second}) - - -# ========================================================================== -# Batch 2: 3D Rendering + Lighting + Sky + Physics -# ========================================================================== - -func _cmd_csg(params: Dictionary) -> void: - var action: String = params.get("action", "create") - if action == "create": - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent not found: %s" % parent_path}) - return - var csg_type: String = params.get("csg_type", "box") - var node: CSGShape3D - match csg_type: - "box": node = CSGBox3D.new() - "sphere": node = CSGSphere3D.new() - "cylinder": node = CSGCylinder3D.new() - "mesh": node = CSGMesh3D.new() - "combiner": node = CSGCombiner3D.new() - _: - _send_response({"error": "Unknown CSG type: %s" % csg_type}) - return - if params.has("operation"): - match params["operation"]: - "union": node.operation = CSGShape3D.OPERATION_UNION - "intersection": node.operation = CSGShape3D.OPERATION_INTERSECTION - "subtraction": node.operation = CSGShape3D.OPERATION_SUBTRACTION - if params.has("name") and not (params["name"] as String).is_empty(): - node.name = params["name"] - parent.add_child(node) - node.owner = get_tree().edited_scene_root if get_tree().edited_scene_root else get_tree().root - _send_response({"success": true, "action": "create", "path": str(node.get_path()), "type": csg_type}) - elif action == "configure": - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is CSGShape3D: - _send_response({"error": "CSGShape3D not found: %s" % node_path}) - return - if params.has("operation"): - match params["operation"]: - "union": (node as CSGShape3D).operation = CSGShape3D.OPERATION_UNION - "intersection": (node as CSGShape3D).operation = CSGShape3D.OPERATION_INTERSECTION - "subtraction": (node as CSGShape3D).operation = CSGShape3D.OPERATION_SUBTRACTION - _send_response({"success": true, "action": "configure", "path": str(node.get_path())}) - else: - _send_response({"error": "Unknown csg action: %s" % action}) - - -func _cmd_multimesh(params: Dictionary) -> void: - var action: String = params.get("action", "create") - match action: - "create": - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent not found: %s" % parent_path}) - return - var mmi: MultiMeshInstance3D = MultiMeshInstance3D.new() - var mm: MultiMesh = MultiMesh.new() - mm.transform_format = MultiMesh.TRANSFORM_3D - mm.instance_count = int(params.get("count", 1)) - var mesh_type: String = params.get("mesh_type", "box") - match mesh_type: - "box": mm.mesh = BoxMesh.new() - "sphere": mm.mesh = SphereMesh.new() - "cylinder": mm.mesh = CylinderMesh.new() - _: mm.mesh = BoxMesh.new() - mmi.multimesh = mm - if params.has("name") and not (params["name"] as String).is_empty(): - mmi.name = params["name"] - parent.add_child(mmi) - _send_response({"success": true, "action": "create", "path": str(mmi.get_path()), "count": mm.instance_count}) - "set_instance": - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is MultiMeshInstance3D: - _send_response({"error": "MultiMeshInstance3D not found: %s" % node_path}) - return - var idx: int = int(params.get("index", 0)) - var tf: Dictionary = params.get("transform", {}) - var origin: Dictionary = tf.get("origin", {}) - var xform: Transform3D = Transform3D.IDENTITY - xform.origin = Vector3(float(origin.get("x", 0)), float(origin.get("y", 0)), float(origin.get("z", 0))) - (node as MultiMeshInstance3D).multimesh.set_instance_transform(idx, xform) - _send_response({"success": true, "action": "set_instance", "index": idx}) - "get_info": - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is MultiMeshInstance3D: - _send_response({"error": "MultiMeshInstance3D not found: %s" % node_path}) - return - var mm = (node as MultiMeshInstance3D).multimesh - _send_response({"success": true, "count": mm.instance_count if mm else 0, "visible_count": mm.visible_instance_count if mm else 0}) - _: - _send_response({"error": "Unknown multimesh action: %s" % action}) - - -func _cmd_procedural_mesh(params: Dictionary) -> void: - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent not found: %s" % parent_path}) - return - var verts_arr: Array = params.get("vertices", []) - var verts: PackedVector3Array = PackedVector3Array() - for v in verts_arr: - verts.append(Vector3(float(v[0]), float(v[1]), float(v[2]))) - var arrays: Array = [] - arrays.resize(Mesh.ARRAY_MAX) - arrays[Mesh.ARRAY_VERTEX] = verts - if params.has("normals"): - var norms: PackedVector3Array = PackedVector3Array() - for n in params["normals"]: - norms.append(Vector3(float(n[0]), float(n[1]), float(n[2]))) - arrays[Mesh.ARRAY_NORMAL] = norms - if params.has("uvs"): - var uvs: PackedVector2Array = PackedVector2Array() - for uv in params["uvs"]: - uvs.append(Vector2(float(uv[0]), float(uv[1]))) - arrays[Mesh.ARRAY_TEX_UV] = uvs - if params.has("indices"): - var indices: PackedInt32Array = PackedInt32Array() - for idx in params["indices"]: - indices.append(int(idx)) - arrays[Mesh.ARRAY_INDEX] = indices - var mesh: ArrayMesh = ArrayMesh.new() - mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) - var mi: MeshInstance3D = MeshInstance3D.new() - mi.mesh = mesh - if params.has("name") and not (params["name"] as String).is_empty(): - mi.name = params["name"] - parent.add_child(mi) - _send_response({"success": true, "path": str(mi.get_path()), "vertex_count": verts.size()}) - - -func _cmd_light_3d(params: Dictionary) -> void: - var action: String = params.get("action", "create") - if action == "create": - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent not found: %s" % parent_path}) - return - var light_type: String = params.get("light_type", "omni") - var light: Light3D - match light_type: - "directional": light = DirectionalLight3D.new() - "omni": light = OmniLight3D.new() - "spot": light = SpotLight3D.new() - _: - _send_response({"error": "Unknown light type: %s" % light_type}) - return - if params.has("color"): - var c: Dictionary = params["color"] - light.light_color = Color(float(c.get("r", 1)), float(c.get("g", 1)), float(c.get("b", 1))) - if params.has("energy"): - light.light_energy = float(params["energy"]) - if params.has("shadows"): - light.shadow_enabled = bool(params["shadows"]) - if light is OmniLight3D and params.has("range"): - (light as OmniLight3D).omni_range = float(params["range"]) - if light is SpotLight3D: - if params.has("range"): - (light as SpotLight3D).spot_range = float(params["range"]) - if params.has("spot_angle"): - (light as SpotLight3D).spot_angle = float(params["spot_angle"]) - if params.has("name") and not (params["name"] as String).is_empty(): - light.name = params["name"] - parent.add_child(light) - _send_response({"success": true, "action": "create", "path": str(light.get_path()), "type": light_type}) - elif action == "configure": - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is Light3D: - _send_response({"error": "Light3D not found: %s" % node_path}) - return - var light: Light3D = node as Light3D - if params.has("color"): - var c: Dictionary = params["color"] - light.light_color = Color(float(c.get("r", 1)), float(c.get("g", 1)), float(c.get("b", 1))) - if params.has("energy"): - light.light_energy = float(params["energy"]) - if params.has("shadows"): - light.shadow_enabled = bool(params["shadows"]) - _send_response({"success": true, "action": "configure", "path": str(node.get_path())}) - else: - _send_response({"error": "Unknown light_3d action: %s" % action}) - - -func _cmd_mesh_instance(params: Dictionary) -> void: - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent not found: %s" % parent_path}) - return - var mesh_type: String = params.get("mesh_type", "box") - var mesh: Mesh - match mesh_type: - "box": mesh = BoxMesh.new() - "sphere": mesh = SphereMesh.new() - "cylinder": mesh = CylinderMesh.new() - "capsule": mesh = CapsuleMesh.new() - "plane": mesh = PlaneMesh.new() - "quad": mesh = QuadMesh.new() - _: - _send_response({"error": "Unknown mesh type: %s" % mesh_type}) - return - if params.has("size") and mesh is BoxMesh: - var s: Dictionary = params["size"] - (mesh as BoxMesh).size = Vector3(float(s.get("x", 1)), float(s.get("y", 1)), float(s.get("z", 1))) - if params.has("radius"): - if mesh is SphereMesh: (mesh as SphereMesh).radius = float(params["radius"]) - elif mesh is CylinderMesh: (mesh as CylinderMesh).top_radius = float(params["radius"]) - elif mesh is CapsuleMesh: (mesh as CapsuleMesh).radius = float(params["radius"]) - if params.has("height"): - if mesh is CylinderMesh: (mesh as CylinderMesh).height = float(params["height"]) - elif mesh is CapsuleMesh: (mesh as CapsuleMesh).height = float(params["height"]) - elif mesh is SphereMesh: (mesh as SphereMesh).height = float(params["height"]) - var mi: MeshInstance3D = MeshInstance3D.new() - mi.mesh = mesh - if params.has("material") and params["material"] is String: - var mat: StandardMaterial3D = StandardMaterial3D.new() - var hex: String = params["material"] - if hex.begins_with("#") or hex.length() == 6 or hex.length() == 8: - mat.albedo_color = Color.from_string(hex, Color.WHITE) - mi.material_override = mat - if params.has("name") and not (params["name"] as String).is_empty(): - mi.name = params["name"] - parent.add_child(mi) - _send_response({"success": true, "path": str(mi.get_path()), "mesh_type": mesh_type}) - - -func _cmd_gridmap(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is GridMap: - _send_response({"error": "GridMap not found: %s" % node_path}) - return - var gm: GridMap = node as GridMap - var action: String = params.get("action", "get_used") - match action: - "set_cell": - gm.set_cell_item(Vector3i(int(params.get("x", 0)), int(params.get("y", 0)), int(params.get("z", 0))), int(params.get("item", 0)), int(params.get("orientation", 0))) - _send_response({"success": true, "action": "set_cell"}) - "get_cell": - var item: int = gm.get_cell_item(Vector3i(int(params.get("x", 0)), int(params.get("y", 0)), int(params.get("z", 0)))) - _send_response({"success": true, "action": "get_cell", "item": item}) - "clear": - gm.clear() - _send_response({"success": true, "action": "clear"}) - "get_used": - var cells: Array = gm.get_used_cells() - var result: Array = [] - for c in cells.slice(0, 100): - result.append({"x": c.x, "y": c.y, "z": c.z}) - _send_response({"success": true, "action": "get_used", "cells": result, "total": cells.size()}) - _: - _send_response({"error": "Unknown gridmap action: %s" % action}) - - -func _cmd_3d_effects(params: Dictionary) -> void: - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent not found: %s" % parent_path}) - return - var effect_type: String = params.get("effect_type", "") - var node: Node3D - match effect_type: - "reflection_probe": node = ReflectionProbe.new() - "decal": node = Decal.new() - "fog_volume": node = FogVolume.new() - _: - _send_response({"error": "Unknown effect type: %s" % effect_type}) - return - if params.has("size"): - var s: Dictionary = params["size"] - var size_v: Vector3 = Vector3(float(s.get("x", 1)), float(s.get("y", 1)), float(s.get("z", 1))) - if node is ReflectionProbe: (node as ReflectionProbe).size = size_v - elif node is Decal: (node as Decal).size = size_v - elif node is FogVolume: (node as FogVolume).size = size_v - if params.has("name") and not (params["name"] as String).is_empty(): - node.name = params["name"] - parent.add_child(node) - _send_response({"success": true, "path": str(node.get_path()), "effect_type": effect_type}) - - -func _cmd_gi(params: Dictionary) -> void: - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent not found: %s" % parent_path}) - return - var gi_type: String = params.get("gi_type", "voxel_gi") - var node: VisualInstance3D - match gi_type: - "voxel_gi": node = VoxelGI.new() - "lightmap_gi": node = LightmapGI.new() - _: - _send_response({"error": "Unknown GI type: %s" % gi_type}) - return - if params.has("size") and node is VoxelGI: - var s: Dictionary = params["size"] - (node as VoxelGI).size = Vector3(float(s.get("x", 10)), float(s.get("y", 10)), float(s.get("z", 10))) - if params.has("name") and not (params["name"] as String).is_empty(): - node.name = params["name"] - parent.add_child(node) - _send_response({"success": true, "path": str(node.get_path()), "gi_type": gi_type}) - - -func _cmd_path_3d(params: Dictionary) -> void: - var action: String = params.get("action", "create") - match action: - "create": - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent not found: %s" % parent_path}) - return - var path_node: Path3D = Path3D.new() - path_node.curve = Curve3D.new() - if params.has("name") and not (params["name"] as String).is_empty(): - path_node.name = params["name"] - if params.has("points"): - for p in params["points"]: - path_node.curve.add_point(Vector3(float(p.get("x", 0)), float(p.get("y", 0)), float(p.get("z", 0)))) - parent.add_child(path_node) - _send_response({"success": true, "action": "create", "path": str(path_node.get_path()), "point_count": path_node.curve.point_count}) - "add_point": - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is Path3D: - _send_response({"error": "Path3D not found: %s" % node_path}) - return - var p: Dictionary = params.get("point", {}) - (node as Path3D).curve.add_point(Vector3(float(p.get("x", 0)), float(p.get("y", 0)), float(p.get("z", 0)))) - _send_response({"success": true, "action": "add_point", "point_count": (node as Path3D).curve.point_count}) - "get_points": - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is Path3D: - _send_response({"error": "Path3D not found: %s" % node_path}) - return - var pts: Array = [] - for i in (node as Path3D).curve.point_count: - var pt: Vector3 = (node as Path3D).curve.get_point_position(i) - pts.append({"x": pt.x, "y": pt.y, "z": pt.z}) - _send_response({"success": true, "action": "get_points", "points": pts}) - _: - _send_response({"error": "Unknown path_3d action: %s" % action}) - - -func _cmd_sky(params: Dictionary) -> void: - var action: String = params.get("action", "create") - var env: Environment = _get_or_create_environment() - if env == null: - _send_response({"error": "Could not get or create environment"}) - return - var sky_type: String = params.get("sky_type", "procedural") - if action == "create" or env.sky == null: - env.sky = Sky.new() - env.background_mode = Environment.BG_SKY - var sky_mat: ProceduralSkyMaterial = ProceduralSkyMaterial.new() - if params.has("top_color"): - var c: Dictionary = params["top_color"] - sky_mat.sky_top_color = Color(float(c.get("r", 0.4)), float(c.get("g", 0.6)), float(c.get("b", 1.0))) - if params.has("bottom_color"): - var c: Dictionary = params["bottom_color"] - sky_mat.sky_horizon_color = Color(float(c.get("r", 0.7)), float(c.get("g", 0.8)), float(c.get("b", 0.9))) - if params.has("ground_color"): - var c: Dictionary = params["ground_color"] - sky_mat.ground_bottom_color = Color(float(c.get("r", 0.1)), float(c.get("g", 0.1)), float(c.get("b", 0.1))) - if params.has("sun_energy"): - sky_mat.sun_curve = float(params["sun_energy"]) - env.sky.sky_material = sky_mat - _send_response({"success": true, "action": action, "sky_type": sky_type}) - - -func _get_or_create_environment() -> Environment: - var cam: Camera3D = get_viewport().get_camera_3d() - if cam != null and cam.get_environment() != null: - return cam.get_environment() - var we: WorldEnvironment = null - for child in get_tree().root.get_children(): - if child is WorldEnvironment: - we = child as WorldEnvironment - break - if we != null and we.environment != null: - return we.environment - # Create one - we = WorldEnvironment.new() - we.environment = Environment.new() - get_tree().root.add_child(we) - return we.environment - - -func _cmd_camera_attributes(params: Dictionary) -> void: - var action: String = params.get("action", "get") - var cam: Camera3D = get_viewport().get_camera_3d() - if cam == null: - _send_response({"error": "No Camera3D found in viewport"}) - return - if action == "get": - var info: Dictionary = {"success": true, "action": "get"} - if cam.attributes != null: - info["has_attributes"] = true - else: - info["has_attributes"] = false - _send_response(info) - return - # set - if cam.attributes == null: - cam.attributes = CameraAttributesPractical.new() - var attr: CameraAttributesPractical = cam.attributes as CameraAttributesPractical - if attr == null: - _send_response({"error": "Camera attributes is not CameraAttributesPractical"}) - return - if params.has("dof_blur_far"): - attr.dof_blur_far_enabled = true - attr.dof_blur_far_distance = float(params["dof_blur_far"]) - if params.has("dof_blur_near"): - attr.dof_blur_near_enabled = true - attr.dof_blur_near_distance = float(params["dof_blur_near"]) - if params.has("dof_blur_amount"): - attr.dof_blur_amount = float(params["dof_blur_amount"]) - if params.has("auto_exposure"): - attr.auto_exposure_enabled = bool(params["auto_exposure"]) - _send_response({"success": true, "action": "set"}) - - -func _cmd_navigation_3d(params: Dictionary) -> void: - var action: String = params.get("action", "create") - match action: - "create": - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent not found: %s" % parent_path}) - return - var region: NavigationRegion3D = NavigationRegion3D.new() - region.navigation_mesh = NavigationMesh.new() - if params.has("cell_size"): - region.navigation_mesh.cell_size = float(params["cell_size"]) - if params.has("agent_radius"): - region.navigation_mesh.agent_radius = float(params["agent_radius"]) - if params.has("agent_height"): - region.navigation_mesh.agent_height = float(params["agent_height"]) - if params.has("name") and not (params["name"] as String).is_empty(): - region.name = params["name"] - parent.add_child(region) - _send_response({"success": true, "action": "create", "path": str(region.get_path())}) - "bake": - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is NavigationRegion3D: - _send_response({"error": "NavigationRegion3D not found: %s" % node_path}) - return - (node as NavigationRegion3D).bake_navigation_mesh() - await get_tree().process_frame - await get_tree().process_frame - _send_response({"success": true, "action": "bake"}) - _: - _send_response({"error": "Unknown navigation_3d action: %s" % action}) - - -func _cmd_physics_3d(params: Dictionary) -> void: - var action: String = params.get("action", "ray") - await get_tree().physics_frame - var space: PhysicsDirectSpaceState3D = get_viewport().world_3d.direct_space_state - match action: - "ray": - var from_d: Dictionary = params.get("from", {}) - var to_d: Dictionary = params.get("to", {}) - var from: Vector3 = Vector3(float(from_d.get("x", 0)), float(from_d.get("y", 0)), float(from_d.get("z", 0))) - var to: Vector3 = Vector3(float(to_d.get("x", 0)), float(to_d.get("y", 0)), float(to_d.get("z", 0))) - var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(from, to) - if params.has("collision_mask"): - query.collision_mask = int(params["collision_mask"]) - var result: Dictionary = space.intersect_ray(query) - if result.is_empty(): - _send_response({"success": true, "action": "ray", "hit": false}) - else: - _send_response({"success": true, "action": "ray", "hit": true, "position": _variant_to_json(result["position"]), "normal": _variant_to_json(result["normal"]), "collider": str(result.get("collider", ""))}) - "overlap": - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is Area3D: - _send_response({"error": "Area3D not found: %s" % node_path}) - return - var bodies: Array = (node as Area3D).get_overlapping_bodies() - var result: Array = [] - for b in bodies: - result.append({"name": b.name, "path": str(b.get_path())}) - _send_response({"success": true, "action": "overlap", "bodies": result}) - _: - _send_response({"error": "Unknown physics_3d action: %s" % action}) - - -# ========================================================================== -# Batch 3: 2D Systems + Animation Advanced + Audio Effects -# ========================================================================== - -func _cmd_canvas(params: Dictionary) -> void: - var action: String = params.get("action", "create_layer") - match action: - "create_layer": - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent not found: %s" % parent_path}) - return - var cl: CanvasLayer = CanvasLayer.new() - if params.has("layer"): - cl.layer = int(params["layer"]) - if params.has("name") and not (params["name"] as String).is_empty(): - cl.name = params["name"] - parent.add_child(cl) - _send_response({"success": true, "action": "create_layer", "path": str(cl.get_path())}) - "create_modulate": - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent not found: %s" % parent_path}) - return - var cm: CanvasModulate = CanvasModulate.new() - if params.has("color"): - var c: Dictionary = params["color"] - cm.color = Color(float(c.get("r", 1)), float(c.get("g", 1)), float(c.get("b", 1)), float(c.get("a", 1))) - if params.has("name") and not (params["name"] as String).is_empty(): - cm.name = params["name"] - parent.add_child(cm) - _send_response({"success": true, "action": "create_modulate", "path": str(cm.get_path())}) - _: - _send_response({"error": "Unknown canvas action: %s" % action}) - - -var _canvas_draw_node: Node2D = null -var _draw_commands: Array = [] - -func _cmd_canvas_draw(params: Dictionary) -> void: - var action: String = params.get("action", "line") - if action == "clear": - _draw_commands.clear() - if _canvas_draw_node != null and is_instance_valid(_canvas_draw_node): - _canvas_draw_node.queue_redraw() - _send_response({"success": true, "action": "clear"}) - return - # Ensure draw node - if _canvas_draw_node == null or not is_instance_valid(_canvas_draw_node): - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent not found: %s" % parent_path}) - return - _canvas_draw_node = Node2D.new() - _canvas_draw_node.name = "_McpCanvasDraw" - _canvas_draw_node.set_script(_create_draw_script()) - parent.add_child(_canvas_draw_node) - _canvas_draw_node.set("draw_commands", _draw_commands) - var color_d: Dictionary = params.get("color", {"r": 1.0, "g": 1.0, "b": 1.0, "a": 1.0}) - var color: Color = Color(float(color_d.get("r", 1)), float(color_d.get("g", 1)), float(color_d.get("b", 1)), float(color_d.get("a", 1))) - _draw_commands.append({"action": action, "params": params, "color": color}) - _canvas_draw_node.set("draw_commands", _draw_commands) - _canvas_draw_node.queue_redraw() - _send_response({"success": true, "action": action}) - -func _create_draw_script() -> GDScript: - var s: GDScript = GDScript.new() - s.source_code = """extends Node2D -var draw_commands: Array = [] -func _draw(): - for cmd in draw_commands: - var p = cmd.params - var c = cmd.color - match cmd.action: - "line": - var f = p.get("from", {}) - var t = p.get("to", {}) - draw_line(Vector2(float(f.get("x",0)),float(f.get("y",0))),Vector2(float(t.get("x",0)),float(t.get("y",0))),c,float(p.get("width",2))) - "rect": - var r = p.get("rect", {}) - draw_rect(Rect2(float(r.get("x",0)),float(r.get("y",0)),float(r.get("w",10)),float(r.get("h",10))),c,bool(p.get("filled",true))) - "circle": - var ct = p.get("center", {}) - draw_circle(Vector2(float(ct.get("x",0)),float(ct.get("y",0))),float(p.get("radius",10)),c) -""" - s.reload() - return s - - -func _cmd_light_2d(params: Dictionary) -> void: - var action: String = params.get("action", "create_point") - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent not found: %s" % parent_path}) - return - match action: - "create_point": - var light: PointLight2D = PointLight2D.new() - if params.has("color"): - var c: Dictionary = params["color"] - light.color = Color(float(c.get("r", 1)), float(c.get("g", 1)), float(c.get("b", 1)), float(c.get("a", 1))) - if params.has("energy"): - light.energy = float(params["energy"]) - # Create a simple gradient texture for the light - var tex: GradientTexture2D = GradientTexture2D.new() - tex.width = 128 - tex.height = 128 - tex.fill = GradientTexture2D.FILL_RADIAL - tex.gradient = Gradient.new() - light.texture = tex - if params.has("range"): - light.texture_scale = float(params["range"]) - if params.has("name") and not (params["name"] as String).is_empty(): - light.name = params["name"] - parent.add_child(light) - _send_response({"success": true, "action": "create_point", "path": str(light.get_path())}) - "create_directional": - var light: DirectionalLight2D = DirectionalLight2D.new() - if params.has("color"): - var c: Dictionary = params["color"] - light.color = Color(float(c.get("r", 1)), float(c.get("g", 1)), float(c.get("b", 1)), float(c.get("a", 1))) - if params.has("energy"): - light.energy = float(params["energy"]) - if params.has("name") and not (params["name"] as String).is_empty(): - light.name = params["name"] - parent.add_child(light) - _send_response({"success": true, "action": "create_directional", "path": str(light.get_path())}) - _: - _send_response({"error": "Unknown light_2d action: %s" % action}) - - -func _cmd_parallax(params: Dictionary) -> void: - var action: String = params.get("action", "create_background") - match action: - "create_background": - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent not found: %s" % parent_path}) - return - var bg: ParallaxBackground = ParallaxBackground.new() - if params.has("name") and not (params["name"] as String).is_empty(): - bg.name = params["name"] - parent.add_child(bg) - _send_response({"success": true, "action": "create_background", "path": str(bg.get_path())}) - "add_layer": - var parent_path: String = params.get("parent_path", "") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null or not parent is ParallaxBackground: - _send_response({"error": "ParallaxBackground not found: %s" % parent_path}) - return - var layer: ParallaxLayer = ParallaxLayer.new() - if params.has("motion_scale"): - var ms: Dictionary = params["motion_scale"] - layer.motion_scale = Vector2(float(ms.get("x", 1)), float(ms.get("y", 1))) - if params.has("motion_offset"): - var mo: Dictionary = params["motion_offset"] - layer.motion_offset = Vector2(float(mo.get("x", 0)), float(mo.get("y", 0))) - if params.has("mirroring"): - var mi: Dictionary = params["mirroring"] - layer.motion_mirroring = Vector2(float(mi.get("x", 0)), float(mi.get("y", 0))) - if params.has("name") and not (params["name"] as String).is_empty(): - layer.name = params["name"] - parent.add_child(layer) - _send_response({"success": true, "action": "add_layer", "path": str(layer.get_path())}) - _: - _send_response({"error": "Unknown parallax action: %s" % action}) - - -func _cmd_shape_2d(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - var action: String = params.get("action", "get_points") - match action: - "add_point": - var p: Dictionary = params.get("point", {}) - var pt: Vector2 = Vector2(float(p.get("x", 0)), float(p.get("y", 0))) - if node is Line2D: - (node as Line2D).add_point(pt) - elif node is Polygon2D: - var polygon: PackedVector2Array = (node as Polygon2D).polygon - polygon.append(pt) - (node as Polygon2D).polygon = polygon - _send_response({"success": true, "action": "add_point"}) - "set_points": - var pts: Array = params.get("points", []) - var packed: PackedVector2Array = PackedVector2Array() - for p in pts: - packed.append(Vector2(float(p.get("x", 0)), float(p.get("y", 0)))) - if node is Line2D: - (node as Line2D).points = packed - elif node is Polygon2D: - (node as Polygon2D).polygon = packed - _send_response({"success": true, "action": "set_points", "count": packed.size()}) - "clear": - if node is Line2D: - (node as Line2D).clear_points() - elif node is Polygon2D: - (node as Polygon2D).polygon = PackedVector2Array() - _send_response({"success": true, "action": "clear"}) - "get_points": - var pts: PackedVector2Array - if node is Line2D: - pts = (node as Line2D).points - elif node is Polygon2D: - pts = (node as Polygon2D).polygon - else: - _send_response({"error": "Node is not Line2D or Polygon2D"}) - return - var result: Array = [] - for p in pts: - result.append({"x": p.x, "y": p.y}) - _send_response({"success": true, "action": "get_points", "points": result}) - _: - _send_response({"error": "Unknown shape_2d action: %s" % action}) - - -func _cmd_path_2d(params: Dictionary) -> void: - var action: String = params.get("action", "create") - match action: - "create": - var parent_path: String = params.get("parent_path", "/root") - var parent: Node = get_tree().root.get_node_or_null(parent_path) - if parent == null: - _send_response({"error": "Parent not found: %s" % parent_path}) - return - var path_node: Path2D = Path2D.new() - path_node.curve = Curve2D.new() - if params.has("name") and not (params["name"] as String).is_empty(): - path_node.name = params["name"] - if params.has("points"): - for p in params["points"]: - path_node.curve.add_point(Vector2(float(p.get("x", 0)), float(p.get("y", 0)))) - parent.add_child(path_node) - _send_response({"success": true, "action": "create", "path": str(path_node.get_path()), "point_count": path_node.curve.point_count}) - "add_point": - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is Path2D: - _send_response({"error": "Path2D not found: %s" % node_path}) - return - var p: Dictionary = params.get("point", {}) - (node as Path2D).curve.add_point(Vector2(float(p.get("x", 0)), float(p.get("y", 0)))) - _send_response({"success": true, "action": "add_point", "point_count": (node as Path2D).curve.point_count}) - "get_points": - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is Path2D: - _send_response({"error": "Path2D not found: %s" % node_path}) - return - var pts: Array = [] - for i in (node as Path2D).curve.point_count: - var pt: Vector2 = (node as Path2D).curve.get_point_position(i) - pts.append({"x": pt.x, "y": pt.y}) - _send_response({"success": true, "action": "get_points", "points": pts}) - _: - _send_response({"error": "Unknown path_2d action: %s" % action}) - - -func _cmd_physics_2d(params: Dictionary) -> void: - var action: String = params.get("action", "ray") - await get_tree().physics_frame - var space: PhysicsDirectSpaceState2D = get_viewport().world_2d.direct_space_state - match action: - "ray": - var from_d: Dictionary = params.get("from", {}) - var to_d: Dictionary = params.get("to", {}) - var from: Vector2 = Vector2(float(from_d.get("x", 0)), float(from_d.get("y", 0))) - var to: Vector2 = Vector2(float(to_d.get("x", 0)), float(to_d.get("y", 0))) - var query: PhysicsRayQueryParameters2D = PhysicsRayQueryParameters2D.create(from, to) - if params.has("collision_mask"): - query.collision_mask = int(params["collision_mask"]) - var result: Dictionary = space.intersect_ray(query) - if result.is_empty(): - _send_response({"success": true, "action": "ray", "hit": false}) - else: - _send_response({"success": true, "action": "ray", "hit": true, "position": _variant_to_json(result["position"]), "normal": _variant_to_json(result["normal"]), "collider": str(result.get("collider", ""))}) - "overlap": - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is Area2D: - _send_response({"error": "Area2D not found: %s" % node_path}) - return - var bodies: Array = (node as Area2D).get_overlapping_bodies() - var result: Array = [] - for b in bodies: - result.append({"name": b.name, "path": str(b.get_path())}) - _send_response({"success": true, "action": "overlap", "bodies": result}) - _: - _send_response({"error": "Unknown physics_2d action: %s" % action}) - - -func _cmd_animation_tree(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is AnimationTree: - _send_response({"error": "AnimationTree not found: %s" % node_path}) - return - var tree: AnimationTree = node as AnimationTree - var action: String = params.get("action", "get_state") - match action: - "travel": - var state_name: String = params.get("state_name", "") - var playback = tree.get("parameters/playback") - if playback != null: - playback.travel(state_name) - _send_response({"success": true, "action": "travel", "state": state_name}) - "set_param": - var param_name: String = params.get("param_name", "") - var param_value = params.get("param_value", 0) - tree.set("parameters/" + param_name, param_value) - _send_response({"success": true, "action": "set_param", "param": param_name}) - "get_state": - var playback = tree.get("parameters/playback") - var current: String = "" - if playback != null: - current = playback.get_current_node() - _send_response({"success": true, "action": "get_state", "current": current}) - _: - _send_response({"error": "Unknown animation_tree action: %s" % action}) - - -func _cmd_animation_control(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is AnimationPlayer: - _send_response({"error": "AnimationPlayer not found: %s" % node_path}) - return - var player: AnimationPlayer = node as AnimationPlayer - var action: String = params.get("action", "get_info") - match action: - "seek": - var pos: float = float(params.get("position", 0)) - player.seek(pos) - _send_response({"success": true, "action": "seek", "position": pos}) - "queue": - var anim: String = params.get("animation_name", "") - player.queue(anim) - _send_response({"success": true, "action": "queue", "animation": anim}) - "set_speed": - player.speed_scale = float(params.get("speed", 1.0)) - _send_response({"success": true, "action": "set_speed", "speed": player.speed_scale}) - "stop": - player.stop() - _send_response({"success": true, "action": "stop"}) - "get_info": - var anims: PackedStringArray = player.get_animation_list() - _send_response({"success": true, "action": "get_info", "current": player.current_animation, "playing": player.is_playing(), "animations": Array(anims), "speed_scale": player.speed_scale, "position": player.current_animation_position}) - _: - _send_response({"error": "Unknown animation_control action: %s" % action}) - - -func _cmd_skeleton_ik(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is SkeletonIK3D: - _send_response({"error": "SkeletonIK3D not found: %s" % node_path}) - return - var ik: SkeletonIK3D = node as SkeletonIK3D - var action: String = params.get("action", "start") - match action: - "start": - ik.start() - _send_response({"success": true, "action": "start"}) - "stop": - ik.stop() - _send_response({"success": true, "action": "stop"}) - "set_target": - var t: Dictionary = params.get("target", {}) - var target_tf: Transform3D = Transform3D.IDENTITY - target_tf.origin = Vector3(float(t.get("x", 0)), float(t.get("y", 0)), float(t.get("z", 0))) - ik.target = target_tf - _send_response({"success": true, "action": "set_target"}) - _: - _send_response({"error": "Unknown skeleton_ik action: %s" % action}) - - -func _cmd_audio_effect(params: Dictionary) -> void: - var bus_name: String = params.get("bus_name", "Master") - var bus_idx: int = AudioServer.get_bus_index(bus_name) - if bus_idx < 0: - _send_response({"error": "Audio bus not found: %s" % bus_name}) - return - var action: String = params.get("action", "list") - match action: - "list": - var effects: Array = [] - for i in AudioServer.get_bus_effect_count(bus_idx): - var eff: AudioEffect = AudioServer.get_bus_effect(bus_idx, i) - effects.append({"index": i, "type": eff.get_class(), "enabled": AudioServer.is_bus_effect_enabled(bus_idx, i)}) - _send_response({"success": true, "action": "list", "bus": bus_name, "effects": effects}) - "add": - var effect_type: String = params.get("effect_type", "reverb") - var effect: AudioEffect - match effect_type: - "reverb": effect = AudioEffectReverb.new() - "delay": effect = AudioEffectDelay.new() - "chorus": effect = AudioEffectChorus.new() - "eq": effect = AudioEffectEQ6.new() - "compressor": effect = AudioEffectCompressor.new() - "limiter": effect = AudioEffectLimiter.new() - _: - _send_response({"error": "Unknown effect type: %s" % effect_type}) - return - AudioServer.add_bus_effect(bus_idx, effect) - _send_response({"success": true, "action": "add", "effect_type": effect_type, "index": AudioServer.get_bus_effect_count(bus_idx) - 1}) - "remove": - var idx: int = int(params.get("index", 0)) - AudioServer.remove_bus_effect(bus_idx, idx) - _send_response({"success": true, "action": "remove", "index": idx}) - _: - _send_response({"error": "Unknown audio_effect action: %s" % action}) - - -func _cmd_audio_bus_layout(params: Dictionary) -> void: - var action: String = params.get("action", "list") - match action: - "list": - var buses: Array = [] - for i in AudioServer.bus_count: - buses.append({"index": i, "name": AudioServer.get_bus_name(i), "volume": AudioServer.get_bus_volume_db(i), "mute": AudioServer.is_bus_mute(i), "solo": AudioServer.is_bus_solo(i), "send": AudioServer.get_bus_send(i), "effect_count": AudioServer.get_bus_effect_count(i)}) - _send_response({"success": true, "action": "list", "buses": buses}) - "add": - var bus_name: String = params.get("bus_name", "New Bus") - AudioServer.add_bus() - var idx: int = AudioServer.bus_count - 1 - AudioServer.set_bus_name(idx, bus_name) - _send_response({"success": true, "action": "add", "bus_name": bus_name, "index": idx}) - "remove": - var bus_name: String = params.get("bus_name", "") - var idx: int = AudioServer.get_bus_index(bus_name) - if idx <= 0: - _send_response({"error": "Cannot remove bus: %s" % bus_name}) - return - AudioServer.remove_bus(idx) - _send_response({"success": true, "action": "remove", "bus_name": bus_name}) - "set_send": - var bus_name: String = params.get("bus_name", "") - var send_to: String = params.get("send_to", "Master") - var idx: int = AudioServer.get_bus_index(bus_name) - if idx < 0: - _send_response({"error": "Bus not found: %s" % bus_name}) - return - AudioServer.set_bus_send(idx, send_to) - _send_response({"success": true, "action": "set_send", "bus": bus_name, "send_to": send_to}) - _: - _send_response({"error": "Unknown audio_bus_layout action: %s" % action}) - - -func _cmd_audio_spatial(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is AudioStreamPlayer3D: - _send_response({"error": "AudioStreamPlayer3D not found: %s" % node_path}) - return - var player: AudioStreamPlayer3D = node as AudioStreamPlayer3D - var action: String = params.get("action", "get_info") - if action == "get_info": - _send_response({"success": true, "max_distance": player.max_distance, "unit_size": player.unit_size, "max_db": player.max_db, "playing": player.playing}) - return - if params.has("max_distance"): - player.max_distance = float(params["max_distance"]) - if params.has("unit_size"): - player.unit_size = float(params["unit_size"]) - if params.has("max_db"): - player.max_db = float(params["max_db"]) - _send_response({"success": true, "action": "configure"}) - - -# ========================================================================== -# Batch 4: Locale (runtime) -# ========================================================================== - -func _cmd_locale(params: Dictionary) -> void: - var action: String = params.get("action", "get") - match action: - "get": - _send_response({"success": true, "locale": TranslationServer.get_locale()}) - "set": - var locale: String = params.get("locale", "en") - TranslationServer.set_locale(locale) - _send_response({"success": true, "action": "set", "locale": locale}) - "translate": - var key: String = params.get("key", "") - var translated: String = tr(key) - _send_response({"success": true, "key": key, "translated": translated}) - _: - _send_response({"error": "Unknown locale action: %s" % action}) - - -# ========================================================================== -# Batch 5: UI Controls + Rendering + Resource Runtime -# ========================================================================== - -func _cmd_ui_control(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is Control: - _send_response({"error": "Control not found: %s" % node_path}) - return - var ctrl: Control = node as Control - var action: String = params.get("action", "get_info") - match action: - "grab_focus": - ctrl.grab_focus() - _send_response({"success": true, "action": "grab_focus"}) - "release_focus": - ctrl.release_focus() - _send_response({"success": true, "action": "release_focus"}) - "configure": - if params.has("tooltip"): - ctrl.tooltip_text = str(params["tooltip"]) - if params.has("mouse_filter"): - match params["mouse_filter"]: - "stop": ctrl.mouse_filter = Control.MOUSE_FILTER_STOP - "pass": ctrl.mouse_filter = Control.MOUSE_FILTER_PASS - "ignore": ctrl.mouse_filter = Control.MOUSE_FILTER_IGNORE - if params.has("min_size"): - var s: Dictionary = params["min_size"] - ctrl.custom_minimum_size = Vector2(float(s.get("x", 0)), float(s.get("y", 0))) - _send_response({"success": true, "action": "configure"}) - "get_info": - _send_response({"success": true, "size": _variant_to_json(ctrl.size), "position": _variant_to_json(ctrl.position), "has_focus": ctrl.has_focus(), "visible": ctrl.visible, "tooltip": ctrl.tooltip_text, "mouse_filter": ctrl.mouse_filter}) - _: - _send_response({"error": "Unknown ui_control action: %s" % action}) - - -func _cmd_ui_text(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - var action: String = params.get("action", "get") - match action: - "get": - var text: String = "" - if node is LineEdit: text = (node as LineEdit).text - elif node is TextEdit: text = (node as TextEdit).text - elif node is RichTextLabel: text = (node as RichTextLabel).text - else: - _send_response({"error": "Node is not a text control"}) - return - _send_response({"success": true, "text": text}) - "set": - var text: String = str(params.get("text", "")) - if node is LineEdit: (node as LineEdit).text = text - elif node is TextEdit: (node as TextEdit).text = text - elif node is RichTextLabel: (node as RichTextLabel).text = text - _send_response({"success": true, "action": "set"}) - "append": - var text: String = str(params.get("text", "")) - if node is TextEdit: (node as TextEdit).text += text - elif node is RichTextLabel: (node as RichTextLabel).append_text(text) - _send_response({"success": true, "action": "append"}) - "clear": - if node is LineEdit: (node as LineEdit).text = "" - elif node is TextEdit: (node as TextEdit).text = "" - elif node is RichTextLabel: (node as RichTextLabel).clear() - _send_response({"success": true, "action": "clear"}) - "bbcode": - if node is RichTextLabel: - (node as RichTextLabel).bbcode_enabled = true - (node as RichTextLabel).text = str(params.get("text", "")) - _send_response({"success": true, "action": "bbcode"}) - _: - _send_response({"error": "Unknown ui_text action: %s" % action}) - - -func _cmd_ui_popup(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is Window: - _send_response({"error": "Window/Popup not found: %s" % node_path}) - return - var win: Window = node as Window - var action: String = params.get("action", "popup_centered") - match action: - "popup_centered": - if params.has("size"): - var s: Dictionary = params["size"] - win.popup_centered(Vector2i(int(s.get("x", 200)), int(s.get("y", 100)))) - else: - win.popup_centered() - _send_response({"success": true, "action": "popup_centered"}) - "popup": - win.popup() - _send_response({"success": true, "action": "popup"}) - "hide": - win.hide() - _send_response({"success": true, "action": "hide"}) - "get_info": - _send_response({"success": true, "visible": win.visible, "title": win.title, "size": _variant_to_json(win.size)}) - _: - _send_response({"error": "Unknown ui_popup action: %s" % action}) - - -func _cmd_ui_tree(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is Tree: - _send_response({"error": "Tree not found: %s" % node_path}) - return - var tree: Tree = node as Tree - var action: String = params.get("action", "get_items") - match action: - "get_items": - var items: Array = [] - var root: TreeItem = tree.get_root() - if root != null: - _collect_tree_items(root, items, 0) - _send_response({"success": true, "action": "get_items", "items": items}) - "add": - var text: String = str(params.get("text", "Item")) - var root: TreeItem = tree.get_root() - if root == null: - root = tree.create_item() - var item: TreeItem = tree.create_item(root) - item.set_text(int(params.get("column", 0)), text) - _send_response({"success": true, "action": "add", "text": text}) - _: - _send_response({"error": "Unknown ui_tree action: %s" % action}) - -func _collect_tree_items(item: TreeItem, result: Array, depth: int) -> void: - var col: int = 0 - result.append({"text": item.get_text(col), "depth": depth, "collapsed": item.collapsed}) - var child: TreeItem = item.get_first_child() - while child != null: - _collect_tree_items(child, result, depth + 1) - child = child.get_next() - - -func _cmd_ui_item_list(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - var action: String = params.get("action", "get_items") - if node is ItemList: - var il: ItemList = node as ItemList - match action: - "get_items": - var items: Array = [] - for i in il.item_count: - items.append({"index": i, "text": il.get_item_text(i), "selected": il.is_selected(i)}) - _send_response({"success": true, "items": items}) - "select": - il.select(int(params.get("index", 0))) - _send_response({"success": true, "action": "select"}) - "add": - il.add_item(str(params.get("text", "Item"))) - _send_response({"success": true, "action": "add"}) - "remove": - il.remove_item(int(params.get("index", 0))) - _send_response({"success": true, "action": "remove"}) - "clear": - il.clear() - _send_response({"success": true, "action": "clear"}) - _: - _send_response({"error": "Unknown ui_item_list action: %s" % action}) - elif node is OptionButton: - var ob: OptionButton = node as OptionButton - match action: - "get_items": - var items: Array = [] - for i in ob.item_count: - items.append({"index": i, "text": ob.get_item_text(i)}) - _send_response({"success": true, "items": items, "selected": ob.selected}) - "select": - ob.select(int(params.get("index", 0))) - _send_response({"success": true, "action": "select"}) - "add": - ob.add_item(str(params.get("text", "Item"))) - _send_response({"success": true, "action": "add"}) - _: - _send_response({"error": "Unknown action for OptionButton: %s" % action}) - else: - _send_response({"error": "Node is not ItemList or OptionButton"}) - - -func _cmd_ui_tabs(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - var action: String = params.get("action", "get_tabs") - if node is TabContainer: - var tc: TabContainer = node as TabContainer - match action: - "get_tabs": - var tabs: Array = [] - for i in tc.get_tab_count(): - tabs.append({"index": i, "title": tc.get_tab_title(i)}) - _send_response({"success": true, "tabs": tabs, "current": tc.current_tab}) - "set_current": - tc.current_tab = int(params.get("index", 0)) - _send_response({"success": true, "action": "set_current"}) - "set_title": - tc.set_tab_title(int(params.get("index", 0)), str(params.get("title", ""))) - _send_response({"success": true, "action": "set_title"}) - _: - _send_response({"error": "Unknown ui_tabs action: %s" % action}) - elif node is TabBar: - var tb: TabBar = node as TabBar - match action: - "get_tabs": - var tabs: Array = [] - for i in tb.tab_count: - tabs.append({"index": i, "title": tb.get_tab_title(i)}) - _send_response({"success": true, "tabs": tabs, "current": tb.current_tab}) - "set_current": - tb.current_tab = int(params.get("index", 0)) - _send_response({"success": true, "action": "set_current"}) - _: - _send_response({"error": "Unknown ui_tabs action: %s" % action}) - else: - _send_response({"error": "Node is not TabContainer or TabBar"}) - - -func _cmd_ui_menu(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null or not node is PopupMenu: - _send_response({"error": "PopupMenu not found: %s" % node_path}) - return - var menu: PopupMenu = node as PopupMenu - var action: String = params.get("action", "get_items") - match action: - "get_items": - var items: Array = [] - for i in menu.item_count: - items.append({"index": i, "text": menu.get_item_text(i), "checked": menu.is_item_checked(i), "disabled": menu.is_item_disabled(i), "id": menu.get_item_id(i)}) - _send_response({"success": true, "items": items}) - "add": - var text: String = str(params.get("text", "Item")) - var id: int = int(params.get("id", -1)) - menu.add_item(text, id) - _send_response({"success": true, "action": "add"}) - "remove": - menu.remove_item(int(params.get("index", 0))) - _send_response({"success": true, "action": "remove"}) - "set_checked": - menu.set_item_checked(int(params.get("index", 0)), bool(params.get("checked", true))) - _send_response({"success": true, "action": "set_checked"}) - "clear": - menu.clear() - _send_response({"success": true, "action": "clear"}) - _: - _send_response({"error": "Unknown ui_menu action: %s" % action}) - - -func _cmd_ui_range(params: Dictionary) -> void: - var node_path: String = params.get("node_path", "") - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - var action: String = params.get("action", "get") - if node is Range: - var r: Range = node as Range - if action == "get": - _send_response({"success": true, "value": r.value, "min": r.min_value, "max": r.max_value, "step": r.step}) - return - if params.has("value"): r.value = float(params["value"]) - if params.has("min_value"): r.min_value = float(params["min_value"]) - if params.has("max_value"): r.max_value = float(params["max_value"]) - if params.has("step"): r.step = float(params["step"]) - _send_response({"success": true, "action": "set", "value": r.value}) - elif node is ColorPicker: - var cp: ColorPicker = node as ColorPicker - if action == "get": - var c: Color = cp.color - _send_response({"success": true, "color": {"r": c.r, "g": c.g, "b": c.b, "a": c.a}}) - return - if params.has("color"): - var cd: Dictionary = params["color"] - cp.color = Color(float(cd.get("r", 0)), float(cd.get("g", 0)), float(cd.get("b", 0)), float(cd.get("a", 1))) - _send_response({"success": true, "action": "set"}) - else: - _send_response({"error": "Node is not Range or ColorPicker"}) - - -func _cmd_render_settings(params: Dictionary) -> void: - var vp: Viewport = get_viewport() - var action: String = params.get("action", "get") - if action == "get": - _send_response({"success": true, "msaa_2d": vp.msaa_2d, "msaa_3d": vp.msaa_3d, "screen_space_aa": vp.screen_space_aa, "use_taa": vp.use_taa, "scaling_3d_mode": vp.scaling_3d_mode, "scaling_3d_scale": vp.scaling_3d_scale}) - return - if params.has("msaa_2d"): - vp.msaa_2d = int(params["msaa_2d"]) as Viewport.MSAA - if params.has("msaa_3d"): - vp.msaa_3d = int(params["msaa_3d"]) as Viewport.MSAA - if params.has("fxaa"): - vp.screen_space_aa = Viewport.SCREEN_SPACE_AA_FXAA if bool(params["fxaa"]) else Viewport.SCREEN_SPACE_AA_DISABLED - if params.has("taa"): - vp.use_taa = bool(params["taa"]) - if params.has("scaling_mode"): - vp.scaling_3d_mode = int(params["scaling_mode"]) as Viewport.Scaling3DMode - if params.has("scaling_scale"): - vp.scaling_3d_scale = float(params["scaling_scale"]) - _send_response({"success": true, "action": "set"}) - - -func _cmd_resource(params: Dictionary) -> void: - var action: String = params.get("action", "load") - var res_path: String = params.get("path", "") - match action: - "load": - if not ResourceLoader.exists(res_path): - _send_response({"error": "Resource not found: %s" % res_path}) - return - var res: Resource = ResourceLoader.load(res_path) - if res == null: - _send_response({"error": "Failed to load resource: %s" % res_path}) - return - _send_response({"success": true, "action": "load", "path": res_path, "type": res.get_class()}) - "save": - var node_path: String = params.get("node_path", "") - var prop: String = params.get("property", "") - if node_path.is_empty(): - _send_response({"error": "node_path is required for save"}) - return - var node: Node = get_tree().root.get_node_or_null(node_path) - if node == null: - _send_response({"error": "Node not found: %s" % node_path}) - return - var res = node.get(prop) if not prop.is_empty() else null - if res is Resource: - var err: int = ResourceSaver.save(res, res_path) - _send_response({"success": err == OK, "action": "save", "path": res_path}) - else: - _send_response({"error": "Property is not a Resource"}) - "exists": - _send_response({"success": true, "action": "exists", "path": res_path, "exists": ResourceLoader.exists(res_path)}) - _: - _send_response({"error": "Unknown resource action: %s" % action}) - - -func _exit_tree() -> void: - _clear_debug_draw() - if _websocket != null: - _websocket.close() - _websocket = null - if _client != null: - _client.disconnect_from_host() - _client = null - if _server != null: - _server.stop() - _server = null - print("McpInteractionServer: Stopped") diff --git a/mcp_interaction_server.gd.uid b/mcp_interaction_server.gd.uid deleted file mode 100644 index 5cf2424..0000000 --- a/mcp_interaction_server.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bxbjxf7qb74ki diff --git a/tests/test_room_navigation.py b/tests/test_room_navigation.py deleted file mode 100644 index bc24c89..0000000 --- a/tests/test_room_navigation.py +++ /dev/null @@ -1,516 +0,0 @@ -#!/usr/bin/env python3 -"""Tests for kq4 room navigation tools. - -Tests build_room_graph.py, mcp_client.py, and kq4_room_navigator.py offline logic. -MCP runtime integration requires a running game instance. - -Run: python tests/test_room_navigation.py -""" - -import json -import re -import socket -import sys -import threading -from pathlib import Path -from unittest import TestCase, main as test_main - -# Add project root to path -PROJECT_ROOT = Path(__file__).resolve().parent.parent -sys.path.insert(0, str(PROJECT_ROOT)) -sys.path.insert(0, str(PROJECT_ROOT / "tools")) -sys.path.insert(0, str(PROJECT_ROOT / "scripts")) - -from build_room_graph import ( - NavigationStep, - RoomInfo, - TransitionInfo, - _resolve_target_room, - build_graph, - find_path, - find_uid_files, - get_connected_components, - parse_transitions, -) -from mcp_client import McpClient - - -# ─── Sample .tscn bodies for parse_transitions tests ───────────────── - -SCENE_WITH_BASIC_TRANSITIONS = """\ -[gd_scene format=3 uid="uid://abc123"] - -[node name="background" type="Node2D" unique_id=1] -script = ExtResource("1") - -[node name="kq4_010_forest_path" parent="." instance=ExtResource("4_xxx")] -position = Vector2(910, 542) -polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381) -appear_at_node = "kq4_004_ogres_cottage" -target = "uid://bsog5s257pres" -label = "Forest Path" - -[node name="entrance" parent="kq4_010_forest_path" index="0"] -position = Vector2(118, 514) - -[node name="exit" parent="kq4_010_forest_path" index="1"] -position = Vector2(151, 615) - -[connection signal="interacted" from="kq4_010_forest_path" to="." method="_on_forest_path_interacted"] -""" - -SCENE_WITH_SCALED_TRANSITIONS = """\ -[gd_scene format=3 uid="uid://xyz789"] - -[node name="background" type="Node2D" unique_id=1] -script = ExtResource("1") - -[node name="kq4_049_ogres_cottage" parent="." instance=ExtResource("4_xxx")] -position = Vector2(500, 300) -scale = Vector2(0.783, 0.78) -polygon = PackedVector2Array(100, 200, 300, 400, 500, 600, 700, 800) -appear_at_node = "kq4_004_ogres_cottage_exterior" -target = "uid://c5h5n8dreoa8k" -label = "Door" - -[node name="entrance" parent="kq4_049_ogres_cottage" index="0"] -position = Vector2(100, 200) - -[node name="exit" parent="kq4_049_ogres_cottage" index="1"] -position = Vector2(300, 400) -""" - -SCENE_WITH_AND_STRING_APPEAR_AT = """\ -[gd_scene format=3 uid="uid://and123"] - -[node name="background" type="Node2D" unique_id=1] -script = ExtResource("1") - -[node name="kq4_005_forest_grove" parent="." instance=ExtResource("4_xxx")] -position = Vector2(1766, 74) -polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381) -appear_at_node = &"kq4_004_ogres_cottage" -target = "uid://1c470jsfmdhxx" -label = "Forest Grove" - -[node name="entrance" parent="kq4_005_forest_grove" index="0"] -position = Vector2(24, 565) - -[node name="exit" parent="kq4_005_forest_grove" index="1"] -position = Vector2(293, 554) -""" - -SCENE_NO_TRANSITIONS = """\ -[gd_scene format=3 uid="uid://empty"] - -[node name="background" type="Node2D" unique_id=1] -script = ExtResource("1") - -[node name="some_setpiece" type="Polygon2D" parent="." unique_id=99] -polygon = PackedVector2Array(0, 0, 10, 10) -label = "Rock" -""" - - -# ─── Tests for parse_transitions ────────────────────────────────────── - - -class TestParseTransitions(TestCase): - """Unit tests for the .tscn transition parser.""" - - def test_basic_transition_parsed(self): - transitions = parse_transitions(SCENE_WITH_BASIC_TRANSITIONS, "test_room") - self.assertEqual(len(transitions), 1) - t = transitions[0] - self.assertEqual(t.exit_node_name, "kq4_010_forest_path") - self.assertEqual(t.target_uid, "uid://bsog5s257pres") - self.assertEqual(t.appear_at_node, "kq4_004_ogres_cottage") - self.assertEqual(t.label, "Forest Path") - self.assertEqual(len(t.polygon), 4) - self.assertEqual(t.position, (910.0, 542.0)) - self.assertEqual(t.scale, (1.0, 1.0)) - - def test_scaled_transition_parsed(self): - transitions = parse_transitions(SCENE_WITH_SCALED_TRANSITIONS, "test_room") - self.assertEqual(len(transitions), 1) - t = transitions[0] - self.assertEqual(t.exit_node_name, "kq4_049_ogres_cottage") - self.assertAlmostEqual(t.scale[0], 0.783) - self.assertAlmostEqual(t.scale[1], 0.78) - self.assertEqual(t.position, (500.0, 300.0)) - - def test_and_string_appear_at_node(self): - """GDScript &"string" syntax must also be recognized.""" - transitions = parse_transitions(SCENE_WITH_AND_STRING_APPEAR_AT, "test_room") - self.assertEqual(len(transitions), 1) - self.assertEqual(transitions[0].appear_at_node, "kq4_004_ogres_cottage") - - def test_no_transitions_in_scene(self): - transitions = parse_transitions(SCENE_NO_TRANSITIONS, "test_room") - self.assertEqual(len(transitions), 0) - - def test_polygon_centroid_basic(self): - """Test viewport_centroid with default position/scale.""" - t = TransitionInfo( - exit_node_name="x", - target_uid="uid://1", - appear_at_node="y", - label="X", - polygon=[(0, 0), (10, 0), (10, 10), (0, 10)], - ) - cx, cy = t.viewport_centroid() - self.assertAlmostEqual(cx, 5.0) - self.assertAlmostEqual(cy, 5.0) - - def test_polygon_centroid_with_position(self): - """Centroid accounts for node position.""" - t = TransitionInfo( - exit_node_name="x", - target_uid="uid://1", - appear_at_node="y", - label="X", - polygon=[(0, 0), (10, 0), (10, 10), (0, 10)], - position=(100.0, 200.0), - ) - cx, cy = t.viewport_centroid() - self.assertAlmostEqual(cx, 105.0) - self.assertAlmostEqual(cy, 205.0) - - def test_polygon_centroid_with_scale(self): - """Centroid accounts for both position and scale.""" - t = TransitionInfo( - exit_node_name="x", - target_uid="uid://1", - appear_at_node="y", - label="X", - polygon=[(0, 0), (10, 0), (10, 10), (0, 10)], - position=(100.0, 200.0), - scale=(0.8, 0.8), - ) - cx, cy = t.viewport_centroid() - # centroid_local = (5, 5), viewport = 0.8 * (100 + 5) = 84 - self.assertAlmostEqual(cx, 84.0) - self.assertAlmostEqual(cy, 164.0) - - def test_polygon_centroid_no_polygon(self): - """Returns position when polygon is empty.""" - t = TransitionInfo( - exit_node_name="x", - target_uid="uid://1", - appear_at_node="y", - label="X", - polygon=[], - position=(42.0, 99.0), - ) - cx, cy = t.viewport_centroid() - self.assertAlmostEqual(cx, 42.0) - self.assertAlmostEqual(cy, 99.0) - - def test_polygon_vertex_count(self): - """Verify polygon parsing produces correct number of vertices.""" - transitions = parse_transitions(SCENE_WITH_BASIC_TRANSITIONS, "test_room") - t = transitions[0] - # The example has 4 pairs: (-108,454), (-87,649), (376,658), (348,381) - self.assertEqual(len(t.polygon), 4) - self.assertAlmostEqual(t.polygon[0][0], -108.0) - self.assertAlmostEqual(t.polygon[0][1], 454.0) - - def test_transition_default_label_fallback(self): - """Label falls back to node_name when not specified.""" - body = '''\ -[node name="kq4_001_beach" parent="." instance=ExtResource("4")] -position = Vector2(100, 200) -polygon = PackedVector2Array(0, 0, 10, 10) -appear_at_node = "kq4_002_meadow" -target = "uid://test" -''' - transitions = parse_transitions(body, "room") - self.assertEqual(len(transitions), 1) - # No label attribute → should default to exit_node_name - self.assertEqual(transitions[0].label, "kq4_001_beach") - - -# ─── Tests for build_graph and find_path (using real project .tscn files) ── - - -class TestBuildGraph(TestCase): - """Integration tests using actual room scene files.""" - - @classmethod - def setUpClass(cls): - cls.scenes_dir = PROJECT_ROOT / "scenes" - if not cls.scenes_dir.exists(): - raise cls.skipTest("scenes/ directory not found") - cls.graph = build_graph(cls.scenes_dir) - - def test_graph_has_rooms(self): - # Should parse many rooms (project has ~96) - self.assertGreaterEqual(len(self.graph), 50) - - def test_known_rooms_exist(self): - expected = { - "kq4_003_fountain_pool", - "kq4_004_ogres_cottage", - "kq4_010_forest_path", - "kq4_005_forest_grove", - } - for room in expected: - self.assertIn(room, self.graph, f"{room} should be in graph") - - def test_room_has_exits(self): - # Room 004 has multiple exits - r = self.graph["kq4_004_ogres_cottage"] - self.assertGreaterEqual(len(r.transitions), 3) - - def test_transitions_have_positions(self): - """Transition nodes should have parsed positions.""" - r = self.graph["kq4_004_ogres_cottage"] - for t in r.transitions: - self.assertIsInstance(t.position, tuple) - self.assertEqual(len(t.position), 2) - - def test_transitions_have_scales(self): - """Transition nodes should have parsed scales (default to 1.0).""" - r = self.graph["kq4_004_ogres_cottage"] - for t in r.transitions: - self.assertIsInstance(t.scale, tuple) - self.assertEqual(len(t.scale), 2) - - def test_transitions_have_polygons(self): - """Transition nodes should have parsed polygon vertices.""" - r = self.graph["kq4_004_ogres_cottage"] - for t in r.transitions: - self.assertIsInstance(t.polygon, list) - self.assertGreater(len(t.polygon), 0, f"Empty polygon for {t.exit_node_name}") - - def test_rooms_have_uids(self): - """All rooms should have a UID from .tscn header.""" - for name, info in self.graph.items(): - self.assertIsNotNone(info.uid, f"{name} has no UID") - - def test_placeholder_template_excluded(self): - self.assertNotIn("kq4_placeholder_template", self.graph) - - -class TestFindPath(TestCase): - """BFS pathfinding tests against real project graph.""" - - @classmethod - def setUpClass(cls): - cls.scenes_dir = PROJECT_ROOT / "scenes" - if not cls.scenes_dir.exists(): - raise cls.skipTest("scenes/ directory not found") - cls.graph = build_graph(cls.scenes_dir) - - def test_path_1_hop(self): - """Room 003 → room 004 is directly connected.""" - steps = find_path(self.graph, "kq4_003_fountain_pool", "kq4_004_ogres_cottage") - self.assertIsNotNone(steps) - self.assertEqual(len(steps), 1) - self.assertEqual(steps[0].from_room, "kq4_003_fountain_pool") - self.assertEqual(steps[0].to_room, "kq4_004_ogres_cottage") - - def test_path_multi_hop(self): - """Room 003 → room 010 should be reachable (via room 004).""" - steps = find_path(self.graph, "kq4_003_fountain_pool", "kq4_010_forest_path") - self.assertIsNotNone(steps) - # Path: 003 → 004 → 010 = 2 steps - self.assertGreaterEqual(len(steps), 1) - - def test_same_room_returns_empty(self): - """Path from a room to itself is trivially empty.""" - steps = find_path( - self.graph, "kq4_003_fountain_pool", "kq4_003_fountain_pool" - ) - self.assertEqual(steps, []) - - def test_disconnected_rooms(self): - """Rooms in different components should return None.""" - # Room 9 is its own isolated component (no bidirectional wiring) - if "kq4_009_shady_wooded_area" in self.graph: - steps = find_path( - self.graph, - "kq4_003_fountain_pool", - "kq4_009_shady_wooded_area", - ) - # This may or may not be connected depending on wiring state - # Just verify no crash and correct return type - self.assertIsNone(steps) if steps is None else True - - def test_invalid_start_room(self): - with self.assertRaises(ValueError): - find_path(self.graph, "nonexistent_room", "kq4_003_fountain_pool") - - def test_invalid_end_room(self): - with self.assertRaises(ValueError): - find_path(self.graph, "kq4_003_fountain_pool", "nonexistent_room") - - def test_steps_have_coordinates(self): - """NavigationStep from real graph should have usable coordinates.""" - steps = find_path( - self.graph, "kq4_003_fountain_pool", "kq4_010_forest_path" - ) - if steps: - for step in steps: - cx, cy = step.viewport_centroid() - self.assertIsInstance(cx, float) - self.assertIsInstance(cy, float) - # Coordinates should be reasonable viewport values (0-2000 range typically) - self.assertGreater(cx, -500) - self.assertGreater(cy, -500) - - def test_bfs_finds_shortest_path(self): - """BFS guarantees shortest path. Verify no detour for 2-hop.""" - steps = find_path( - self.graph, "kq4_003_fountain_pool", "kq4_005_forest_grove" - ) - if steps: - # Should not take more than ~3 hops for nearby rooms - self.assertLessEqual(len(steps), 4) - - def test_steps_preserve_polygon(self): - """Step polygon should match source transition's polygon.""" - # Find a step and verify it has polygon vertices - steps = find_path( - self.graph, "kq4_003_fountain_pool", "kq4_010_forest_path" - ) - if steps: - for step in steps: - self.assertGreater( - len(step.polygon), 0, - f"Step {step.exit_node_name} has no polygon" - ) - - -class TestResolveTargetRoom(TestCase): - """Test UID → room name resolution.""" - - @classmethod - def setUpClass(cls): - cls.scenes_dir = PROJECT_ROOT / "scenes" - if not cls.scenes_dir.exists(): - raise cls.skipTest("scenes/ directory not found") - cls.graph = build_graph(cls.scenes_dir) - - def test_resolve_known_uid(self): - """A transition pointing to a known UID should resolve to a room name.""" - # Find any transition that resolves (target room exists in graph) - found_resolvable = False - for rinfo in self.graph.values(): - for t in rinfo.transitions: - resolved = _resolve_target_room(t, self.graph) - if resolved is not None: - found_resolvable = True - # Verify it resolves to a valid room name - self.assertIn(resolved, self.graph) - # At least some transitions should resolve (largest component has 36 rooms) - if not found_resolvable: - self.fail("No transitions could be resolved — graph may be in bad state") - - def test_resolve_unknown_uid(self): - """A transition with an unknown UID returns None.""" - t = TransitionInfo( - exit_node_name="test", - target_uid="uid://doesnotexist12345", - appear_at_node="something", - label="Test", - polygon=[], - ) - result = _resolve_target_room(t, self.graph) - self.assertIsNone(result) - - -class TestConnectedComponents(TestCase): - """Verify connected component analysis.""" - - @classmethod - def setUpClass(cls): - cls.scenes_dir = PROJECT_ROOT / "scenes" - if not cls.scenes_dir.exists(): - raise cls.skipTest("scenes/ directory not found") - cls.graph = build_graph(cls.scenes_dir) - cls.components = get_connected_components(cls.graph) - - def test_all_rooms_covered(self): - """Every room should appear in exactly one component.""" - all_covered = set() - for comp in self.components: - for r in comp: - self.assertNotIn(r, all_covered) - all_covered.update(comp) - all_graph_rooms = set(self.graph.keys()) - self.assertEqual(all_covered, all_graph_rooms) - - def test_most_connected(self): - """Largest component should have significant room count.""" - self.assertGreater(len(self.components[0]), 10) - - def test_component_contains_known_room(self): - """kq4_003_fountain_pool should be in the largest connected component.""" - largest = self.components[0] - self.assertIn("kq4_003_fountain_pool", largest) - - -class TestFindUIDFiles(TestCase): - """Test .uid file discovery.""" - - def test_find_uids(self): - uid_map = find_uid_files(PROJECT_ROOT) - # Should find at least some .uid files in the repo - self.assertIsInstance(uid_map, dict) - - def test_uid_format(self): - uid_map = find_uid_files(PROJECT_ROOT) - for uid_key in uid_map: - self.assertTrue( - uid_key.startswith("uid://"), f"Bad UID format: {uid_key}" - ) - - -# ─── MCP client tests (unit only, no live Godot required) ────────────── - -class TestMcpClient(TestCase): - """Unit tests for McpClient. Full integration requires running Godot on port 9090.""" - - def test_client_creation(self): - c = McpClient(host="127.0.0.1", port=9999) - self.assertEqual(c.host, "127.0.0.1") - self.assertEqual(c.port, 9999) - - def test_connect_required(self): - """send raises ConnectionError when called without connecting.""" - with self.assertRaises(ConnectionError): - McpClient(host="127.0.0.1", port=9999).eval_gdscript("return 42") - - -# ─── Navigator offline logic tests ────────────────────────────────── - - -class TestNavigatorOffline(TestCase): - """Test kq4_room_navigator.py without MCP.""" - - def test_navigator_import(self): - """Basic sanity: module loads without error.""" - from kq4_room_navigator import GDSCRIPT_GET_CURRENT_ROOM, NavigationError - - self.assertIsInstance(GDSCRIPT_GET_CURRENT_ROOM, str) - - def test_navigation_error_raised(self): - from kq4_room_navigator import NavigationError - - with self.assertRaises(NavigationError): - raise NavigationError("test") - - -if __name__ == "__main__": - loader = test_main(exit=False, verbosity=2) - result = loader.result - - failed = len(result.failures) + len(result.errors) - print(f"\n{'=' * 60}") - if failed: - print(f"FAILED: {failed} test(s) out of {result.testsRun}") - sys.exit(1) - else: - passed = result.testsRun - skipped = len(result.skipped)