4422 lines
165 KiB
GDScript
4422 lines
165 KiB
GDScript
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")
|