## Autoload injected by Godot MCP Pro plugin at runtime. ## Handles runtime game inspection: scene tree, node properties, frame capture, property monitoring. extends Node const REQUEST_PATH := "user://mcp_game_request" const RESPONSE_PATH := "user://mcp_game_response" enum State { IDLE, CAPTURING_FRAMES, MONITORING, RECORDING, MOVING_TO, WATCHING_SIGNALS } var _state := State.IDLE var _pending_command: bool = false # Crash recovery flag # Frame capture state var _capture_frames_remaining: int = 0 var _capture_frame_interval: int = 1 var _capture_frame_counter: int = 0 var _capture_half_res: bool = true var _captured_images: Array = [] # Array of base64 strings var _capture_node_path: String = "" # Optional node to track per frame var _capture_node_props: Array = [] # Properties to snapshot per frame var _capture_frame_data: Array = [] # Array of per-frame node snapshots # Recording state var _recording_events: Array = [] var _recording_start_msec: int = 0 # Monitor state var _monitor_node_path: String = "" var _monitor_properties: Array = [] var _monitor_frames_remaining: int = 0 var _monitor_frame_interval: int = 1 var _monitor_frame_counter: int = 0 var _monitor_timeline: Array = [] # Array of sample dicts # Signal watch state var _watch_nodes: Array = [] # Array of node paths being watched var _watch_signal_filter: Array = [] # Optional signal name filter var _watch_log: Array = [] # Array of {time_ms, node, signal, args} var _watch_start_msec: int = 0 var _watch_duration_ms: int = 5000 var _watch_connections: Array = [] # Array of {node, signal, callable} for cleanup # Move-to state var _moveto_target: Vector3 = Vector3.ZERO var _moveto_player: Node3D = null var _moveto_camera_pivot: Node3D = null var _moveto_arrival_radius: float = 1.5 var _moveto_timeout: float = 15.0 var _moveto_elapsed: float = 0.0 var _moveto_run: bool = false var _moveto_look_at: bool = true var _moveto_keys_held: Array = [] # Track injected keys for guaranteed release func _ready() -> void: process_mode = Node.PROCESS_MODE_ALWAYS func _process(_delta: float) -> void: # Crash recovery: if a command was in progress but never wrote a response if _pending_command and not FileAccess.file_exists(REQUEST_PATH) and not FileAccess.file_exists(RESPONSE_PATH): push_warning("[MCP] Recovered from crashed command — writing error response") _pending_command = false _state = State.IDLE # Signal editor plugin to auto-press debugger Continue var flag := FileAccess.open("user://mcp_debugger_continue", FileAccess.WRITE) if flag: flag.close() _write_response({"error": "Command crashed (runtime error). Check Godot debugger."}) return match _state: State.IDLE: if FileAccess.file_exists(REQUEST_PATH): _handle_request() State.CAPTURING_FRAMES: _process_capture() State.MONITORING: _process_monitor() State.RECORDING: if FileAccess.file_exists(REQUEST_PATH): _handle_request() State.MOVING_TO: _process_move_to(_delta) State.WATCHING_SIGNALS: _process_watch_signals() # ── Request handling ────────────────────────────────────────────────────────── func _handle_request() -> void: var file := FileAccess.open(REQUEST_PATH, FileAccess.READ) if file == null: return var text := file.get_as_text() file.close() DirAccess.remove_absolute(REQUEST_PATH) var parsed = JSON.parse_string(text) if parsed == null or not parsed is Dictionary: _write_response({"error": "Invalid request JSON"}) return # Abort any in-progress operation _state = State.IDLE _pending_command = true var command: String = parsed.get("command", "") var params: Dictionary = parsed.get("params", {}) match command: "get_scene_tree": _cmd_get_scene_tree(params) "get_node_properties": _cmd_get_node_properties(params) "set_node_property": _cmd_set_node_property(params) "capture_frames": _cmd_capture_frames(params) "monitor_properties": _cmd_monitor_properties(params) "execute_script": _cmd_execute_script(params) "start_recording": _cmd_start_recording(params) "stop_recording": _cmd_stop_recording(params) "replay_recording": _cmd_replay_recording(params) "find_nodes_by_script": _cmd_find_nodes_by_script(params) "get_autoload": _cmd_get_autoload(params) "batch_get_properties": _cmd_batch_get_properties(params) "find_ui_elements": _cmd_find_ui_elements(params) "click_button_by_text": _cmd_click_button_by_text(params) "wait_for_node": _cmd_wait_for_node(params) "find_nearby_nodes": _cmd_find_nearby_nodes(params) "navigate_to": _cmd_navigate_to(params) "move_to": _cmd_move_to(params) "watch_signals": _cmd_watch_signals(params) "assert_node_state": _cmd_assert_node_state(params) "get_performance_monitors": _cmd_get_performance_monitors(params) _: _write_response({"error": "Unknown command: %s" % command}) # ── get_scene_tree ──────────────────────────────────────────────────────────── func _cmd_get_scene_tree(params: Dictionary) -> void: var root := get_tree().current_scene if root == null: _write_response({"error": "No current scene"}) return var max_depth: int = params.get("max_depth", -1) var script_filter: String = params.get("script_filter", "") var type_filter: String = params.get("type_filter", "") var named_only: bool = params.get("named_only", false) var has_filter: bool = not script_filter.is_empty() or not type_filter.is_empty() or named_only if has_filter: var tree: Variant = _build_filtered_node_tree(root, max_depth, script_filter, type_filter, named_only) if tree == null: _write_response({"tree": null, "message": "No nodes matched the filter"}) else: _write_response({"tree": tree}) else: var tree := _build_node_tree(root, max_depth) _write_response({"tree": tree}) func _build_node_tree(node: Node, max_depth: int, current_depth: int = 0) -> Dictionary: var result := { "name": node.name, "type": node.get_class(), "path": str(node.get_path()), } var script: Script = node.get_script() if script: result["script"] = script.resource_path if max_depth == -1 or current_depth < max_depth: var children: Array = [] for child in node.get_children(): children.append(_build_node_tree(child, max_depth, current_depth + 1)) if not children.is_empty(): result["children"] = children return result ## Build a filtered tree. Returns null if this subtree has no matches. func _build_filtered_node_tree(node: Node, max_depth: int, script_filter: String, type_filter: String, named_only: bool, current_depth: int = 0) -> Variant: var node_matches := _node_matches_filter(node, script_filter, type_filter, named_only) # Build children first to check if any descendant matches var matched_children: Array = [] if max_depth == -1 or current_depth < max_depth: for child in node.get_children(): var child_tree: Variant = _build_filtered_node_tree(child, max_depth, script_filter, type_filter, named_only, current_depth + 1) if child_tree != null: matched_children.append(child_tree) # Include this node if it matches or if any descendant matches if not node_matches and matched_children.is_empty(): return null var result := { "name": node.name, "type": node.get_class(), "path": str(node.get_path()), } var script: Script = node.get_script() if script: result["script"] = script.resource_path if not matched_children.is_empty(): result["children"] = matched_children return result func _node_matches_filter(node: Node, script_filter: String, type_filter: String, named_only: bool) -> bool: if named_only and (node.name as String).begins_with("@"): return false if not type_filter.is_empty() and not node.is_class(type_filter): return false if not script_filter.is_empty(): var script: Script = node.get_script() if script == null: return false if not script.resource_path.to_lower().contains(script_filter.to_lower()): return false return true # ── get_node_properties ─────────────────────────────────────────────────────── func _cmd_get_node_properties(params: Dictionary) -> void: var node_path: String = params.get("node_path", "") if node_path.is_empty(): _write_response({"error": "node_path is required"}) return var node := get_node_or_null(NodePath(node_path)) if node == null: _write_response({"error": "Node not found: %s" % node_path}) return var filter: Array = params.get("properties", []) var props: Dictionary = {} if filter.is_empty(): for prop_info in node.get_property_list(): var prop_name: String = prop_info["name"] var usage: int = prop_info["usage"] if not (usage & PROPERTY_USAGE_EDITOR): continue if prop_name.begins_with("_") or prop_name == "script": continue props[prop_name] = _serialize_value(node.get(prop_name)) else: for prop_name: String in filter: var value: Variant = node.get(prop_name) props[prop_name] = _serialize_value(value) _write_response({ "node_path": str(node.get_path()), "type": node.get_class(), "properties": props, }) # ── capture_frames ──────────────────────────────────────────────────────────── func _cmd_capture_frames(params: Dictionary) -> void: _pending_command = false # Async command — don't trigger crash recovery var count: int = clampi(params.get("count", 5), 1, 30) var interval: int = maxi(params.get("frame_interval", 10), 1) _capture_half_res = params.get("half_resolution", true) # Optional node_data tracking _capture_node_path = "" _capture_node_props = [] _capture_frame_data.clear() var node_data: Dictionary = params.get("node_data", {}) if not node_data.is_empty(): _capture_node_path = node_data.get("node_path", "") _capture_node_props = node_data.get("properties", []) _captured_images.clear() _capture_frames_remaining = count _capture_frame_interval = interval _capture_frame_counter = 0 _state = State.CAPTURING_FRAMES _capture_one_frame() func _process_capture() -> void: if FileAccess.file_exists(REQUEST_PATH): _state = State.IDLE _handle_request() return _capture_frame_counter += 1 if _capture_frame_counter >= _capture_frame_interval: _capture_frame_counter = 0 _capture_one_frame() func _capture_one_frame() -> void: var viewport := get_viewport() if viewport == null: _finish_capture() return var image := viewport.get_texture().get_image() if image == null: _finish_capture() return if _capture_half_res: var new_size := image.get_size() / 2 image.resize(new_size.x, new_size.y, Image.INTERPOLATE_BILINEAR) var png_buffer := image.save_png_to_buffer() _captured_images.append(Marshalls.raw_to_base64(png_buffer)) # Snapshot node properties if tracking if not _capture_node_path.is_empty() and not _capture_node_props.is_empty(): var snap := {} var node := get_tree().root.get_node_or_null(_capture_node_path) if node: for prop_name in _capture_node_props: var val = node.get(prop_name) snap[prop_name] = _serialize_value(val) _capture_frame_data.append(snap) _capture_frames_remaining -= 1 if _capture_frames_remaining <= 0: _finish_capture() func _finish_capture() -> void: _state = State.IDLE var viewport := get_viewport() var w := 0 var h := 0 if viewport: var size := viewport.get_visible_rect().size if _capture_half_res: size /= 2 w = int(size.x) h = int(size.y) var response := { "frames": _captured_images, "count": _captured_images.size(), "width": w, "height": h, "half_resolution": _capture_half_res, } if not _capture_frame_data.is_empty(): response["frame_data"] = _capture_frame_data _write_response(response) _captured_images.clear() _capture_frame_data.clear() # ── monitor_properties ──────────────────────────────────────────────────────── func _cmd_monitor_properties(params: Dictionary) -> void: _pending_command = false # Async command — don't trigger crash recovery _monitor_node_path = params.get("node_path", "") _monitor_properties = params.get("properties", []) if _monitor_node_path.is_empty() or _monitor_properties.is_empty(): _write_response({"error": "node_path and properties are required"}) return var frame_count: int = clampi(params.get("frame_count", 60), 1, 600) var interval: int = maxi(params.get("frame_interval", 1), 1) _monitor_timeline.clear() _monitor_frames_remaining = frame_count _monitor_frame_interval = interval _monitor_frame_counter = 0 _state = State.MONITORING _sample_one_frame() func _process_monitor() -> void: if FileAccess.file_exists(REQUEST_PATH): _state = State.IDLE _handle_request() return _monitor_frame_counter += 1 if _monitor_frame_counter >= _monitor_frame_interval: _monitor_frame_counter = 0 _sample_one_frame() func _sample_one_frame() -> void: var sample: Dictionary = {} var node := get_node_or_null(NodePath(_monitor_node_path)) if node == null: for prop_name: String in _monitor_properties: sample[prop_name] = null else: for prop_name: String in _monitor_properties: sample[prop_name] = _serialize_value(node.get(prop_name)) _monitor_timeline.append(sample) _monitor_frames_remaining -= 1 if _monitor_frames_remaining <= 0: _finish_monitor() func _finish_monitor() -> void: _state = State.IDLE _write_response({ "node_path": _monitor_node_path, "properties": _monitor_properties, "samples": _monitor_timeline, "sample_count": _monitor_timeline.size(), "frame_interval": _monitor_frame_interval, }) _monitor_timeline.clear() # ── watch_signals ──────────────────────────────────────────────────────────── func _cmd_watch_signals(params: Dictionary) -> void: _pending_command = false # Async command — don't trigger crash recovery if not params.has("node_paths") or not params["node_paths"] is Array: _write_response({"error": "node_paths array is required"}) return var node_paths: Array = params["node_paths"] if node_paths.is_empty(): _write_response({"error": "node_paths array is empty"}) return _watch_signal_filter = params.get("signal_filter", []) if params.has("signal_filter") and params["signal_filter"] is Array else [] _watch_duration_ms = clampi(params.get("duration_ms", 5000), 500, 30000) _watch_log.clear() _watch_connections.clear() _watch_nodes = node_paths # Connect to signals on each node var connected_count: int = 0 for node_path_str: String in node_paths: var node := get_node_or_null(NodePath(node_path_str)) if node == null: _watch_log.append({"warning": "Node not found: %s" % node_path_str}) continue for sig_info: Dictionary in node.get_signal_list(): var sig_name: String = sig_info["name"] # Apply filter if specified if not _watch_signal_filter.is_empty(): var match_found := false for filter_str: String in _watch_signal_filter: if sig_name.contains(filter_str): match_found = true break if not match_found: continue # Create a callable matched to the signal's argument count var arg_count: int = sig_info["args"].size() var cb := _make_signal_callback(node_path_str, sig_name, arg_count) if cb.is_valid() and not node.is_connected(sig_name, cb): node.connect(sig_name, cb) _watch_connections.append({"node": node, "signal": sig_name, "callable": cb}) connected_count += 1 if connected_count == 0 and _watch_log.is_empty(): _write_response({"error": "No signals connected. Check node_paths and signal_filter."}) return _watch_start_msec = Time.get_ticks_msec() _state = State.WATCHING_SIGNALS func _on_signal_fired(node_path_str: String, sig_name: String, args: Array) -> void: var elapsed: int = Time.get_ticks_msec() - _watch_start_msec var entry: Dictionary = { "time_ms": elapsed, "node": node_path_str, "signal": sig_name, } if not args.is_empty(): var serialized: Array = [] for a: Variant in args: serialized.append(_serialize_value(a)) entry["args"] = serialized _watch_log.append(entry) func _make_signal_callback(node_path_str: String, sig_name: String, arg_count: int) -> Callable: var np := node_path_str var sn := sig_name match arg_count: 0: return func() -> void: _on_signal_fired(np, sn, []) 1: return func(a: Variant) -> void: _on_signal_fired(np, sn, [a]) 2: return func(a: Variant, b: Variant) -> void: _on_signal_fired(np, sn, [a, b]) 3: return func(a: Variant, b: Variant, c: Variant) -> void: _on_signal_fired(np, sn, [a, b, c]) 4: return func(a: Variant, b: Variant, c: Variant, d: Variant) -> void: _on_signal_fired(np, sn, [a, b, c, d]) _: # For 5+ args, drop extra args and log without them var cb := func() -> void: _on_signal_fired(np, sn, []) return cb.unbind(arg_count) func _process_watch_signals() -> void: # Check for abort (new request) if FileAccess.file_exists(REQUEST_PATH): _finish_watch_signals() _state = State.IDLE _handle_request() return var elapsed: int = Time.get_ticks_msec() - _watch_start_msec if elapsed >= _watch_duration_ms: _finish_watch_signals() func _finish_watch_signals() -> void: # Disconnect all watchers for conn: Dictionary in _watch_connections: var node: Node = conn["node"] as Node if is_instance_valid(node): var sig_name: String = conn["signal"] var cb: Callable = conn["callable"] if node.is_connected(sig_name, cb): node.disconnect(sig_name, cb) _watch_connections.clear() _state = State.IDLE _write_response({ "node_paths": _watch_nodes, "signal_filter": _watch_signal_filter, "duration_ms": _watch_duration_ms, "events": _watch_log, "event_count": _watch_log.size(), }) _watch_log.clear() # ── set_node_property ───────────────────────────────────────────────────────── func _cmd_set_node_property(params: Dictionary) -> void: var node_path: String = params.get("node_path", "") if node_path.is_empty(): _write_response({"error": "node_path is required"}) return var property: String = params.get("property", "") if property.is_empty(): _write_response({"error": "property is required"}) return if not params.has("value"): _write_response({"error": "value is required"}) return var node := get_node_or_null(NodePath(node_path)) if node == null: _write_response({"error": "Node not found: %s" % node_path}) return var old_value: Variant = node.get(property) var raw_value: Variant = params.get("value") var parsed_value: Variant = _parse_value_for_type(raw_value, typeof(old_value)) node.set(property, parsed_value) var new_value: Variant = node.get(property) _write_response({ "node_path": str(node.get_path()), "property": property, "old_value": _serialize_value(old_value), "new_value": _serialize_value(new_value), }) func _parse_value_for_type(raw: Variant, target_type: int) -> Variant: if typeof(raw) == target_type: return raw # Handle Dictionary → Vector2/Vector3/Color conversion if raw is Dictionary: var dict: Dictionary = raw match target_type: TYPE_VECTOR3: return Vector3( float(dict.get("x", 0)), float(dict.get("y", 0)), float(dict.get("z", 0)) ) TYPE_VECTOR3I: return Vector3i( int(dict.get("x", 0)), int(dict.get("y", 0)), int(dict.get("z", 0)) ) TYPE_VECTOR2: return Vector2( float(dict.get("x", 0)), float(dict.get("y", 0)) ) TYPE_VECTOR2I: return Vector2i( int(dict.get("x", 0)), int(dict.get("y", 0)) ) TYPE_COLOR: return Color( float(dict.get("r", 0)), float(dict.get("g", 0)), float(dict.get("b", 0)), float(dict.get("a", 1)) ) return raw if raw is String: var raw_str: String = raw if raw_str.begins_with("#"): return Color.html(raw_str) var expr := Expression.new() var err := expr.parse(raw_str) if err == OK: var result: Variant = expr.execute() if not expr.has_execute_failed(): return result return raw_str if raw is float and target_type == TYPE_INT: return int(raw) if raw is int and target_type == TYPE_FLOAT: return float(raw) return raw # ── execute_script ──────────────────────────────────────────────────────────── func _cmd_execute_script(params: Dictionary) -> void: var code: String = params.get("code", "") if code.is_empty(): _write_response({"error": "code is required"}) return # Normalize indentation: convert leading spaces to tabs var raw_lines := code.split("\n") var indent_size := 0 for raw_line in raw_lines: var spaces := 0 while spaces < raw_line.length() and raw_line[spaces] == " ": spaces += 1 if spaces > 0 and (indent_size == 0 or spaces < indent_size): indent_size = spaces if indent_size > 0: var space_unit := " ".repeat(indent_size) for idx in raw_lines.size(): var rl: String = raw_lines[idx] var tabs := "" while rl.begins_with(space_unit): tabs += "\t" rl = rl.substr(indent_size) raw_lines[idx] = tabs + rl # Separate top-level func definitions (place at class level, not inside run()) var class_funcs: PackedStringArray = [] var body_lines: PackedStringArray = [] var i := 0 while i < raw_lines.size(): var line: String = raw_lines[i] if not line.begins_with("\t") and not line.begins_with(" ") and line.begins_with("func "): class_funcs.append(line) i += 1 while i < raw_lines.size(): var next_line: String = raw_lines[i] if next_line.is_empty() or next_line.begins_with("\t"): class_funcs.append(next_line) i += 1 else: break else: body_lines.append(line) i += 1 var wrapped := """extends Node var _mcp_output: Array = [] var _mcp_error: String = "" func _mcp_print(value: Variant) -> void: _mcp_output.append(str(value)) func _safe_get(node: Node, prop: String, default: Variant = null) -> Variant: if node == null: return default return node.get(prop) if prop in node else default """ # Add user's top-level functions at class level for func_line in class_funcs: wrapped += func_line + "\n" if class_funcs.size() > 0: wrapped += "\n" wrapped += "func run() -> Variant:\n" for line in body_lines: wrapped += "\t" + line + "\n" wrapped += "\treturn _mcp_output\n" var script := GDScript.new() script.source_code = wrapped var err := script.reload() if err != OK: _write_response({"error": "Script compilation failed: %s" % error_string(err)}) return var temp_node := Node.new() temp_node.set_script(script) get_tree().current_scene.add_child(temp_node) var output: Variant = null if temp_node.has_method("run"): output = temp_node.run() var mcp_output: Array = temp_node.get("_mcp_output") if temp_node.get("_mcp_output") is Array else [] temp_node.queue_free() _write_response({ "output": mcp_output, "return_value": str(output) if output != null else null, }) # ── find_nodes_by_script ────────────────────────────────────────────────────── func _cmd_find_nodes_by_script(params: Dictionary) -> void: var script_name: String = params.get("script", "") if script_name.is_empty(): _write_response({"error": "'script' is required"}) return var root := get_tree().current_scene if root == null: _write_response({"error": "No current scene"}) return var prop_filter: Array = params.get("properties", []) var matches: Array = [] _find_nodes_by_script_recursive(root, script_name.to_lower(), prop_filter, matches) _write_response({"nodes": matches, "count": matches.size()}) func _find_nodes_by_script_recursive(node: Node, script_filter: String, prop_filter: Array, results: Array) -> void: var script: Script = node.get_script() if script and script.resource_path.to_lower().contains(script_filter): var entry := { "name": node.name, "path": str(node.get_path()), "type": node.get_class(), "script": script.resource_path, } var props: Dictionary = {} if prop_filter.is_empty(): for prop_info in node.get_property_list(): var prop_name: String = prop_info["name"] var usage: int = prop_info["usage"] if not (usage & PROPERTY_USAGE_EDITOR): continue if prop_name.begins_with("_") or prop_name == "script": continue props[prop_name] = _serialize_value(node.get(prop_name)) else: for prop_name: String in prop_filter: props[prop_name] = _serialize_value(node.get(prop_name)) entry["properties"] = props results.append(entry) for child in node.get_children(): _find_nodes_by_script_recursive(child, script_filter, prop_filter, results) # ── get_autoload ───────────────────────────────────────────────────────────── func _cmd_get_autoload(params: Dictionary) -> void: var autoload_name: String = params.get("name", "") if autoload_name.is_empty(): _write_response({"error": "'name' is required"}) return var node := get_node_or_null(NodePath("/root/" + autoload_name)) if node == null: _write_response({"error": "Autoload not found: %s" % autoload_name}) return var prop_filter: Array = params.get("properties", []) var props: Dictionary = {} if prop_filter.is_empty(): for prop_info in node.get_property_list(): var prop_name: String = prop_info["name"] var usage: int = prop_info["usage"] if not (usage & PROPERTY_USAGE_EDITOR): continue if prop_name.begins_with("_") or prop_name == "script": continue props[prop_name] = _serialize_value(node.get(prop_name)) else: for prop_name: String in prop_filter: props[prop_name] = _serialize_value(node.get(prop_name)) var result := { "name": autoload_name, "path": str(node.get_path()), "type": node.get_class(), "properties": props, } var script: Script = node.get_script() if script: result["script"] = script.resource_path _write_response(result) # ── batch_get_properties ───────────────────────────────────────────────────── func _cmd_batch_get_properties(params: Dictionary) -> void: var nodes: Array = params.get("nodes", []) if nodes.is_empty(): _write_response({"error": "'nodes' array is required"}) return var results: Array = [] for entry: Dictionary in nodes: var node_path: String = entry.get("path", "") var prop_filter: Array = entry.get("properties", []) if node_path.is_empty(): results.append({"path": "", "properties": {}, "error": "Empty path"}) continue var node := get_node_or_null(NodePath(node_path)) if node == null: results.append({"path": node_path, "properties": {}, "error": "Node not found"}) continue var props: Dictionary = {} if prop_filter.is_empty(): for prop_info in node.get_property_list(): var prop_name: String = prop_info["name"] var usage: int = prop_info["usage"] if not (usage & PROPERTY_USAGE_EDITOR): continue if prop_name.begins_with("_") or prop_name == "script": continue props[prop_name] = _serialize_value(node.get(prop_name)) else: for prop_name: String in prop_filter: props[prop_name] = _serialize_value(node.get(prop_name)) results.append({"path": node_path, "properties": props}) _write_response({"nodes": results, "count": results.size()}) # ── find_ui_elements ───────────────────────────────────────────────────────── func _cmd_find_ui_elements(params: Dictionary) -> void: var root := get_tree().current_scene if root == null: _write_response({"error": "No current scene"}) return var type_filter: String = params.get("type_filter", "") var elements: Array = [] _find_ui_recursive(root, type_filter, elements) _write_response({"elements": elements, "count": elements.size()}) func _find_ui_recursive(node: Node, type_filter: String, results: Array) -> void: if node is Control and node.visible: var ctrl: Control = node var entry: Dictionary = {} var include := false if ctrl is Button: var btn: Button = ctrl entry["type"] = "Button" entry["text"] = btn.text entry["disabled"] = btn.disabled include = true elif ctrl is Label: var lbl: Label = ctrl entry["type"] = "Label" entry["text"] = lbl.text include = true elif ctrl is LineEdit: var le: LineEdit = ctrl entry["type"] = "LineEdit" entry["text"] = le.text entry["placeholder"] = le.placeholder_text include = true elif ctrl is TextEdit: var te: TextEdit = ctrl entry["type"] = "TextEdit" entry["text"] = te.text.left(200) include = true elif ctrl is OptionButton: var ob: OptionButton = ctrl entry["type"] = "OptionButton" entry["text"] = ob.text entry["selected"] = ob.selected include = true elif ctrl is CheckBox: var cb: CheckBox = ctrl entry["type"] = "CheckBox" entry["text"] = cb.text entry["checked"] = cb.button_pressed include = true elif ctrl is HSlider or ctrl is VSlider: var sl: Range = ctrl entry["type"] = "HSlider" if ctrl is HSlider else "VSlider" entry["value"] = sl.value entry["min"] = sl.min_value entry["max"] = sl.max_value include = true if include: if not type_filter.is_empty() and entry.get("type", "") != type_filter: pass # Skip non-matching types else: var rect := ctrl.get_global_rect() entry["name"] = str(ctrl.name) entry["path"] = str(ctrl.get_path()) entry["rect"] = { "x": rect.position.x, "y": rect.position.y, "width": rect.size.x, "height": rect.size.y, } entry["center"] = { "x": rect.position.x + rect.size.x / 2.0, "y": rect.position.y + rect.size.y / 2.0, } results.append(entry) for child in node.get_children(): _find_ui_recursive(child, type_filter, results) # ── click_button_by_text ───────────────────────────────────────────────────── func _cmd_click_button_by_text(params: Dictionary) -> void: var text: String = params.get("text", "") var partial: bool = params.get("partial", true) if text.is_empty(): _write_response({"error": "'text' is required"}) return var root := get_tree().current_scene if root == null: _write_response({"error": "No current scene"}) return var btn: Button = _find_button_by_text(root, text, partial) if btn == null: _write_response({"error": "No visible button found with text: '%s'" % text}) return var rect := btn.get_global_rect() var center := rect.get_center() var btn_text_value := btn.text # Capture button path before clicking — the click may trigger a scene # transition that removes the node from the tree. var btn_path := str(btn.get_path()) if btn.is_inside_tree() else "" # Emit the pressed signal directly — more reliable than Input.parse_input_event # which doesn't always reach GUI Controls. btn.emit_signal("pressed") # After the click, the node may have been freed due to scene transition. # Re-check before accessing any node properties. if not is_instance_valid(btn) or not btn.is_inside_tree(): _write_response({ "clicked": true, "button_text": btn_text_value, "button_path": btn_path, "position": {"x": center.x, "y": center.y}, "note": "Button was removed from scene tree after click (likely a scene transition)", }) return _write_response({ "clicked": true, "button_text": btn.text, "button_path": str(btn.get_path()), "position": {"x": center.x, "y": center.y}, }) func _find_button_by_text(node: Node, text: String, partial: bool) -> Button: if node is Button and node.visible: var btn: Button = node var btn_text := btn.text.to_lower().strip_edges() var search_text := text.to_lower().strip_edges() if partial and btn_text.contains(search_text): return btn elif not partial and btn_text == search_text: return btn for child in node.get_children(): var found := _find_button_by_text(child, text, partial) if found != null: return found return null # ── wait_for_node ──────────────────────────────────────────────────────────── func _cmd_wait_for_node(params: Dictionary) -> void: _pending_command = false # Async command — don't trigger crash recovery var node_path: String = params.get("node_path", "") if node_path.is_empty(): _write_response({"error": "'node_path' is required"}) return var timeout_sec: float = params.get("timeout", 5.0) var poll_interval: int = maxi(int(params.get("poll_frames", 5)), 1) var attempts := int(timeout_sec / (poll_interval / 60.0)) var frame_counter := 0 for i in attempts: var node := get_node_or_null(NodePath(node_path)) if node != null: var result := { "found": true, "node_path": str(node.get_path()), "type": node.get_class(), "name": str(node.name), } var script: Script = node.get_script() if script: result["script"] = script.resource_path _write_response(result) return # Wait poll_interval frames for _f in poll_interval: await get_tree().process_frame _write_response({ "found": false, "node_path": node_path, "error": "Node not found after %.1fs" % timeout_sec, }) # ── find_nearby_nodes ───────────────────────────────────────────────────────── func _cmd_find_nearby_nodes(params: Dictionary) -> void: var radius: float = float(params.get("radius", 20.0)) var max_results: int = int(params.get("max_results", 10)) var type_filter: String = params.get("type_filter", "") var group_filter: String = params.get("group_filter", "") # Resolve origin position var origin := Vector3.ZERO var position_param: Variant = params.get("position", null) if position_param is String: # node_path — use its global_position var origin_node := get_node_or_null(NodePath(position_param as String)) if origin_node == null: _write_response({"error": "Origin node not found: %s" % position_param}) return if origin_node is Node3D: origin = (origin_node as Node3D).global_position elif origin_node is Node2D: var pos2d: Vector2 = (origin_node as Node2D).global_position origin = Vector3(pos2d.x, pos2d.y, 0) else: _write_response({"error": "Origin node is not Node2D or Node3D: %s" % position_param}) return elif position_param is Dictionary: var dict: Dictionary = position_param origin = Vector3(float(dict.get("x", 0)), float(dict.get("y", 0)), float(dict.get("z", 0))) elif position_param == null: _write_response({"error": "'position' is required (node_path string or {x,y,z} object)"}) return var root := get_tree().current_scene if root == null: _write_response({"error": "No current scene"}) return # Collect all spatial nodes within radius var candidates: Array = [] _find_nearby_recursive(root, origin, radius, type_filter, group_filter, candidates) # Sort by distance candidates.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["distance"] < b["distance"] ) # Limit results if candidates.size() > max_results: candidates.resize(max_results) _write_response({ "origin": {"x": origin.x, "y": origin.y, "z": origin.z}, "radius": radius, "nodes": candidates, "count": candidates.size(), }) func _find_nearby_recursive(node: Node, origin: Vector3, radius: float, type_filter: String, group_filter: String, results: Array) -> void: var pos := Vector3.ZERO var is_spatial := false if node is Node3D: pos = (node as Node3D).global_position is_spatial = true elif node is Node2D: var pos2d: Vector2 = (node as Node2D).global_position pos = Vector3(pos2d.x, pos2d.y, 0) is_spatial = true if is_spatial: var diff := pos - origin var dist := diff.length() if dist <= radius: # Apply filters var passes := true if not type_filter.is_empty() and not node.is_class(type_filter): passes = false if not group_filter.is_empty() and not node.is_in_group(group_filter): passes = false if passes: var entry: Dictionary = { "node_path": str(node.get_path()), "name": str(node.name), "type": node.get_class(), "distance": snappedf(dist, 0.01), "global_position": {"x": snappedf(pos.x, 0.01), "y": snappedf(pos.y, 0.01), "z": snappedf(pos.z, 0.01)}, "direction": {"x": snappedf(diff.x, 0.01), "y": snappedf(diff.y, 0.01), "z": snappedf(diff.z, 0.01)}, } var script: Script = node.get_script() if script: entry["script"] = script.resource_path results.append(entry) for child in node.get_children(): _find_nearby_recursive(child, origin, radius, type_filter, group_filter, results) # ── navigate_to ────────────────────────────────────────────────────────────── func _cmd_navigate_to(params: Dictionary) -> void: # Resolve player position var player_path: String = params.get("player_path", "/root/Main/Player") var player := get_node_or_null(NodePath(player_path)) if player == null: _write_response({"error": "Player not found: %s" % player_path}) return var player_pos := Vector3.ZERO if player is Node3D: player_pos = (player as Node3D).global_position else: _write_response({"error": "Player is not Node3D: %s" % player_path}) return # Resolve target position var target_param: Variant = params.get("target", null) var target_pos := Vector3.ZERO if target_param is String: var target_node := get_node_or_null(NodePath(target_param as String)) if target_node == null: _write_response({"error": "Target node not found: %s" % target_param}) return if target_node is Node3D: target_pos = (target_node as Node3D).global_position else: _write_response({"error": "Target is not Node3D: %s" % target_param}) return elif target_param is Dictionary: var dict: Dictionary = target_param target_pos = Vector3(float(dict.get("x", 0)), float(dict.get("y", 0)), float(dict.get("z", 0))) else: _write_response({"error": "'target' is required (node_path string or {x,y,z} object)"}) return # Calculate world direction (XZ plane for 3D movement) var world_dir := target_pos - player_pos var distance := world_dir.length() var flat_dir := Vector3(world_dir.x, 0, world_dir.z).normalized() # Find camera for relative direction var camera_path: String = params.get("camera_path", "") var camera: Camera3D = null if not camera_path.is_empty(): var cam_node := get_node_or_null(NodePath(camera_path)) if cam_node is Camera3D: camera = cam_node else: # Auto-detect: find Camera3D in scene camera = get_viewport().get_camera_3d() var suggested_keys: Array = [] var camera_yaw_delta: float = 0.0 var camera_forward := Vector3.ZERO if camera != null: # Camera forward (XZ plane) camera_forward = -camera.global_basis.z var cam_flat := Vector3(camera_forward.x, 0, camera_forward.z).normalized() var cam_right := Vector3(camera_forward.z, 0, -camera_forward.x).normalized() if flat_dir.length() > 0.01: # Project target direction onto camera axes var forward_dot := flat_dir.dot(cam_flat) var right_dot := flat_dir.dot(cam_right) # Suggest keys based on dominant direction if forward_dot > 0.3: suggested_keys.append("KEY_W") elif forward_dot < -0.3: suggested_keys.append("KEY_S") if right_dot > 0.3: suggested_keys.append("KEY_D") elif right_dot < -0.3: suggested_keys.append("KEY_A") # Calculate yaw rotation needed to face target directly (W only) var angle_to_target := atan2(flat_dir.x, flat_dir.z) var cam_yaw := atan2(cam_flat.x, cam_flat.z) camera_yaw_delta = angle_to_target - cam_yaw # Normalize to [-PI, PI] while camera_yaw_delta > PI: camera_yaw_delta -= TAU while camera_yaw_delta < -PI: camera_yaw_delta += TAU # Estimate walk duration (rough: assume ~5 units/sec movement speed) var move_speed: float = float(params.get("move_speed", 5.0)) var estimated_duration := distance / move_speed if move_speed > 0 else 0.0 # Convert yaw delta to approximate mouse relative_x pixels # Typical: 400px mouse movement ≈ PI radians var mouse_sensitivity_scale: float = 400.0 / PI var suggested_mouse_x := -camera_yaw_delta * mouse_sensitivity_scale _write_response({ "distance": snappedf(distance, 0.01), "world_direction": { "x": snappedf(world_dir.x, 0.01), "y": snappedf(world_dir.y, 0.01), "z": snappedf(world_dir.z, 0.01), }, "flat_direction": { "x": snappedf(flat_dir.x, 0.01), "z": snappedf(flat_dir.z, 0.01), }, "suggested_keys": suggested_keys, "camera_rotation_delta": { "yaw_radians": snappedf(camera_yaw_delta, 0.001), "suggested_mouse_relative_x": snappedf(suggested_mouse_x, 1.0), }, "estimated_duration": snappedf(estimated_duration, 0.1), "player_position": {"x": snappedf(player_pos.x, 0.01), "y": snappedf(player_pos.y, 0.01), "z": snappedf(player_pos.z, 0.01)}, "target_position": {"x": snappedf(target_pos.x, 0.01), "y": snappedf(target_pos.y, 0.01), "z": snappedf(target_pos.z, 0.01)}, }) # ── move_to ─────────────────────────────────────────────────────────────────── func _cmd_move_to(params: Dictionary) -> void: # Resolve player node var player_path: String = params.get("player_path", "/root/Main/Player") var player := get_node_or_null(NodePath(player_path)) if player == null or not player is Node3D: _write_response({"error": "Player not found or not Node3D: %s" % player_path}) return _moveto_player = player as Node3D # Resolve target position var target_param: Variant = params.get("target", null) if target_param is String: var target_node := get_node_or_null(NodePath(target_param as String)) if target_node == null: _write_response({"error": "Target node not found: %s" % target_param}) return if target_node is Node3D: _moveto_target = (target_node as Node3D).global_position else: _write_response({"error": "Target is not Node3D: %s" % target_param}) return elif target_param is Dictionary: var dict: Dictionary = target_param _moveto_target = Vector3(float(dict.get("x", 0)), float(dict.get("y", 0)), float(dict.get("z", 0))) else: _write_response({"error": "'target' is required (node_path string or {x,y,z} object)"}) return # Resolve camera pivot _moveto_camera_pivot = null var camera_path: String = params.get("camera_path", "") if not camera_path.is_empty(): var cam_node := get_node_or_null(NodePath(camera_path)) if cam_node is Node3D: _moveto_camera_pivot = cam_node as Node3D else: # Auto-detect: look for SpringArm3D child of player for child in _moveto_player.get_children(): if child is SpringArm3D: _moveto_camera_pivot = child as Node3D break # Fallback: active Camera3D's parent if _moveto_camera_pivot == null: var cam := get_viewport().get_camera_3d() if cam != null and cam.get_parent() is Node3D and cam.get_parent() != get_tree().root: _moveto_camera_pivot = cam.get_parent() as Node3D # Read params _moveto_arrival_radius = float(params.get("arrival_radius", 1.5)) _moveto_timeout = float(params.get("timeout", 15.0)) _moveto_run = bool(params.get("run", false)) _moveto_look_at = bool(params.get("look_at_target", true)) _moveto_elapsed = 0.0 _moveto_keys_held.clear() # Check if already at target var dist := _moveto_player.global_position.distance_to(_moveto_target) if dist <= _moveto_arrival_radius: _write_response({ "success": true, "arrived": true, "final_distance": snappedf(dist, 0.01), "final_position": _serialize_value(_moveto_player.global_position), "target_position": _serialize_value(_moveto_target), "elapsed_time": 0.0, }) return # Start walking — async state, don't trigger crash recovery _pending_command = false _state = State.MOVING_TO # Inject walk key _inject_key(KEY_W, true) if _moveto_run: _inject_key(KEY_SHIFT, true) func _process_move_to(delta: float) -> void: # Check for abort (new command arrived) if FileAccess.file_exists(REQUEST_PATH): _finish_move_to(false, "Aborted by new command") _state = State.IDLE _handle_request() return _moveto_elapsed += delta # Timeout check if _moveto_elapsed >= _moveto_timeout: _finish_move_to(false, "Timeout after %.1fs" % _moveto_timeout) return # Safety: player freed if not is_instance_valid(_moveto_player): _finish_move_to(false, "Player node was freed") return var player_pos := _moveto_player.global_position var flat_target := Vector3(_moveto_target.x, player_pos.y, _moveto_target.z) var dist := player_pos.distance_to(flat_target) # Arrival check (XZ distance only, ignore Y) if dist <= _moveto_arrival_radius: _finish_move_to(true, "Arrived") return # Rotate camera pivot toward target if _moveto_look_at and _moveto_camera_pivot != null and is_instance_valid(_moveto_camera_pivot): var dir := flat_target - player_pos if dir.length_squared() > 0.01: var target_yaw := atan2(-dir.x, -dir.z) var current_yaw: float = _moveto_camera_pivot.rotation.y # Lerp toward target yaw (~10 rad/s) var yaw_diff := target_yaw - current_yaw # Normalize to [-PI, PI] while yaw_diff > PI: yaw_diff -= TAU while yaw_diff < -PI: yaw_diff += TAU var max_step := 10.0 * delta var step := clampf(yaw_diff, -max_step, max_step) _moveto_camera_pivot.rotation.y += step func _finish_move_to(success: bool, message: String) -> void: # Release all held keys _release_all_keys() _state = State.IDLE var final_pos := Vector3.ZERO var final_dist := 0.0 if is_instance_valid(_moveto_player): final_pos = _moveto_player.global_position final_dist = final_pos.distance_to(_moveto_target) _write_response({ "success": success, "arrived": success, "message": message, "final_distance": snappedf(final_dist, 0.01), "final_position": _serialize_value(final_pos), "target_position": _serialize_value(_moveto_target), "elapsed_time": snappedf(_moveto_elapsed, 0.01), }) func _inject_key(keycode: int, pressed: bool) -> void: var event := InputEventKey.new() event.keycode = keycode event.pressed = pressed Input.parse_input_event(event) if pressed: _moveto_keys_held.append(keycode) else: _moveto_keys_held.erase(keycode) func _release_all_keys() -> void: for keycode: int in _moveto_keys_held.duplicate(): var event := InputEventKey.new() event.keycode = keycode event.pressed = false Input.parse_input_event(event) _moveto_keys_held.clear() # ── Recording ──────────────────────────────────────────────────────────────── func _cmd_start_recording(_params: Dictionary) -> void: _recording_events.clear() _recording_start_msec = Time.get_ticks_msec() _state = State.RECORDING set_process_input(true) _write_response({"recording": true, "message": "Recording started"}) func _cmd_stop_recording(_params: Dictionary) -> void: set_process_input(false) _state = State.IDLE var events := _recording_events.duplicate() var duration_ms := Time.get_ticks_msec() - _recording_start_msec _write_response({ "recording": false, "events": events, "event_count": events.size(), "duration_ms": duration_ms, }) func _cmd_replay_recording(params: Dictionary) -> void: _pending_command = false # Async command — don't trigger crash recovery var events: Array = params.get("events", []) if events.is_empty(): _write_response({"error": "No events to replay"}) return var speed: float = params.get("speed", 1.0) var start_msec := Time.get_ticks_msec() for event_data: Dictionary in events: var delay_ms: int = event_data.get("time_ms", 0) var adjusted_delay := int(delay_ms / speed) while Time.get_ticks_msec() - start_msec < adjusted_delay: await get_tree().process_frame var event := _reconstruct_event(event_data) if event != null: Input.parse_input_event(event) _write_response({ "replayed": true, "event_count": events.size(), "speed": speed, }) func _input(event: InputEvent) -> void: if _state != State.RECORDING: return var time_ms := Time.get_ticks_msec() - _recording_start_msec var data: Dictionary = {"time_ms": time_ms} if event is InputEventKey: var key: InputEventKey = event data["type"] = "key" data["keycode"] = OS.get_keycode_string(key.keycode) if key.keycode != 0 else "" data["physical_keycode"] = OS.get_keycode_string(key.physical_keycode) if key.physical_keycode != 0 else "" data["pressed"] = key.pressed data["shift"] = key.shift_pressed data["ctrl"] = key.ctrl_pressed data["alt"] = key.alt_pressed elif event is InputEventMouseButton: var mb: InputEventMouseButton = event data["type"] = "mouse_button" data["button"] = mb.button_index data["pressed"] = mb.pressed data["position"] = {"x": mb.position.x, "y": mb.position.y} data["double_click"] = mb.double_click elif event is InputEventMouseMotion: var mm: InputEventMouseMotion = event data["type"] = "mouse_motion" data["position"] = {"x": mm.position.x, "y": mm.position.y} data["relative"] = {"x": mm.relative.x, "y": mm.relative.y} elif event is InputEventAction: var act: InputEventAction = event data["type"] = "action" data["action"] = act.action data["pressed"] = act.pressed data["strength"] = act.strength else: return _recording_events.append(data) func _reconstruct_event(data: Dictionary) -> InputEvent: var type: String = data.get("type", "") match type: "key": var event := InputEventKey.new() var keycode_str: String = data.get("keycode", "") if not keycode_str.is_empty(): event.keycode = OS.find_keycode_from_string(keycode_str) event.pressed = data.get("pressed", true) event.shift_pressed = data.get("shift", false) event.ctrl_pressed = data.get("ctrl", false) event.alt_pressed = data.get("alt", false) return event "mouse_button": var event := InputEventMouseButton.new() event.button_index = data.get("button", MOUSE_BUTTON_LEFT) event.pressed = data.get("pressed", true) event.double_click = data.get("double_click", false) var pos: Dictionary = data.get("position", {}) event.position = Vector2(pos.get("x", 0.0), pos.get("y", 0.0)) event.global_position = event.position return event "mouse_motion": var event := InputEventMouseMotion.new() var pos: Dictionary = data.get("position", {}) event.position = Vector2(pos.get("x", 0.0), pos.get("y", 0.0)) event.global_position = event.position var rel: Dictionary = data.get("relative", {}) event.relative = Vector2(rel.get("x", 0.0), rel.get("y", 0.0)) return event "action": var event := InputEventAction.new() event.action = data.get("action", "") event.pressed = data.get("pressed", true) event.strength = data.get("strength", 1.0) return event return null # ── Helpers ─────────────────────────────────────────────────────────────────── func _write_response(data: Dictionary) -> void: _pending_command = false var json := JSON.stringify(data) var file := FileAccess.open(RESPONSE_PATH, FileAccess.WRITE) if file: file.store_string(json) file.close() # ── assert_node_state ───────────────────────────────────────────────────────── func _cmd_assert_node_state(params: Dictionary) -> void: var node_path: String = params.get("node_path", "") if node_path.is_empty(): _write_response({"error": "node_path is required"}) return var property: String = params.get("property", "") if property.is_empty(): _write_response({"error": "property is required"}) return var node := get_node_or_null(NodePath(node_path)) if node == null: _write_response({"error": "Node not found: %s" % node_path}) return var actual: Variant if ":" in property: actual = node.get_indexed(NodePath(property)) else: actual = node.get(property) var expected: Variant = params.get("expected", null) var operator: String = params.get("operator", "eq") var passed: bool = false match operator: "eq": passed = (str(actual) == str(expected)) or (actual == expected) "neq": passed = (actual != expected) and (str(actual) != str(expected)) "gt": passed = float(actual) > float(expected) "lt": passed = float(actual) < float(expected) "gte": passed = float(actual) >= float(expected) "lte": passed = float(actual) <= float(expected) "contains": passed = str(actual).contains(str(expected)) "type_is": passed = typeof(actual) == int(expected) or type_string(typeof(actual)) == str(expected) _: _write_response({"error": "Unknown operator: %s" % operator}) return _write_response({ "result": { "assertion": "node_state", "node_path": node_path, "property": property, "operator": operator, "expected": _serialize_value(expected), "actual": _serialize_value(actual), "passed": passed, } }) # ── get_performance_monitors ────────────────────────────────────────────────── func _cmd_get_performance_monitors(_params: Dictionary) -> void: var monitors := {} monitors["fps"] = Performance.get_monitor(Performance.TIME_FPS) monitors["frame_time_msec"] = Performance.get_monitor(Performance.TIME_PROCESS) * 1000.0 monitors["physics_frame_time_msec"] = Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS) * 1000.0 monitors["navigation_process_msec"] = Performance.get_monitor(Performance.TIME_NAVIGATION_PROCESS) * 1000.0 monitors["memory_static"] = Performance.get_monitor(Performance.MEMORY_STATIC) monitors["memory_static_max"] = Performance.get_monitor(Performance.MEMORY_STATIC_MAX) monitors["object_count"] = Performance.get_monitor(Performance.OBJECT_COUNT) monitors["object_resource_count"] = Performance.get_monitor(Performance.OBJECT_RESOURCE_COUNT) monitors["object_node_count"] = Performance.get_monitor(Performance.OBJECT_NODE_COUNT) monitors["object_orphan_node_count"] = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) monitors["render_total_objects_in_frame"] = Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME) monitors["render_total_primitives_in_frame"] = Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME) monitors["render_total_draw_calls_in_frame"] = Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME) monitors["render_video_mem_used"] = Performance.get_monitor(Performance.RENDER_VIDEO_MEM_USED) monitors["physics_2d_active_objects"] = Performance.get_monitor(Performance.PHYSICS_2D_ACTIVE_OBJECTS) monitors["physics_2d_collision_pairs"] = Performance.get_monitor(Performance.PHYSICS_2D_COLLISION_PAIRS) monitors["physics_2d_island_count"] = Performance.get_monitor(Performance.PHYSICS_2D_ISLAND_COUNT) monitors["physics_3d_active_objects"] = Performance.get_monitor(Performance.PHYSICS_3D_ACTIVE_OBJECTS) monitors["physics_3d_collision_pairs"] = Performance.get_monitor(Performance.PHYSICS_3D_COLLISION_PAIRS) monitors["physics_3d_island_count"] = Performance.get_monitor(Performance.PHYSICS_3D_ISLAND_COUNT) monitors["navigation_active_maps"] = Performance.get_monitor(Performance.NAVIGATION_ACTIVE_MAPS) monitors["navigation_region_count"] = Performance.get_monitor(Performance.NAVIGATION_REGION_COUNT) monitors["navigation_agent_count"] = Performance.get_monitor(Performance.NAVIGATION_AGENT_COUNT) _write_response({"result": {"monitors": monitors, "process": "game"}}) func _serialize_value(value: Variant) -> Variant: if value == null: return null match typeof(value): TYPE_VECTOR2: var v: Vector2 = value return {"x": v.x, "y": v.y} TYPE_VECTOR2I: var v: Vector2i = value return {"x": v.x, "y": v.y} TYPE_VECTOR3: var v: Vector3 = value return {"x": v.x, "y": v.y, "z": v.z} TYPE_VECTOR3I: var v: Vector3i = value return {"x": v.x, "y": v.y, "z": v.z} TYPE_RECT2: var r: Rect2 = value return {"x": r.position.x, "y": r.position.y, "width": r.size.x, "height": r.size.y} TYPE_COLOR: var c: Color = value return {"r": c.r, "g": c.g, "b": c.b, "a": c.a, "html": "#" + c.to_html()} TYPE_NODE_PATH: return str(value) TYPE_OBJECT: if value is Resource: var res: Resource = value return {"type": res.get_class(), "path": res.resource_path} return str(value) TYPE_ARRAY: var arr: Array = value var result: Array = [] for item in arr: result.append(_serialize_value(item)) return result TYPE_DICTIONARY: var dict: Dictionary = value var result: Dictionary = {} for key in dict: result[str(key)] = _serialize_value(dict[key]) return result _: return value