/**************************************************************************/ /* studio_bridge.cpp */ /**************************************************************************/ #include "studio_bridge.h" #include "scene/main/scene_tree.h" #include "scene/resources/packed_scene.h" #include "core/io/resource_loader.h" #include "core/io/resource_saver.h" #include "core/io/dir_access.h" #include "core/io/file_access.h" #include "core/io/ip_address.h" #include "core/object/class_db.h" #include "core/object/script_language.h" #include "core/crypto/crypto_core.h" #include "core/os/time.h" StudioBridge *StudioBridge::singleton = nullptr; StudioBridge *StudioBridge::get_singleton() { return singleton; } void StudioBridge::_bind_methods() { ClassDB::bind_method(D_METHOD("start_server", "port"), &StudioBridge::start_server, DEFVAL(6007)); ClassDB::bind_method(D_METHOD("stop_server"), &StudioBridge::stop_server); ClassDB::bind_method(D_METHOD("is_server_running"), &StudioBridge::is_server_running); ClassDB::bind_method(D_METHOD("process"), &StudioBridge::process); ClassDB::bind_method(D_METHOD("emit_console_output", "message", "type"), &StudioBridge::emit_console_output, DEFVAL("info")); } StudioBridge::StudioBridge() { singleton = this; } StudioBridge::~StudioBridge() { stop_server(); singleton = nullptr; } Error StudioBridge::start_server(int p_port) { if (is_running) { return ERR_ALREADY_IN_USE; } server_port = p_port; tcp_server.instantiate(); IPAddress bind_address("127.0.0.1"); Error err = tcp_server->listen(server_port, bind_address); // Localhost only for security if (err != OK) { print_line(vformat("StudioBridge: Failed to start server on port %d: %s", server_port, error_names[err])); tcp_server.unref(); return err; } is_running = true; print_line(vformat("StudioBridge: Server started on port %d", server_port)); print_line("StudioBridge: Connect Studio to http://localhost:" + itos(server_port)); return OK; } void StudioBridge::stop_server() { if (!is_running) { return; } // Close all WebSocket clients for (int i = 0; i < websocket_clients.size(); i++) { websocket_clients[i]->close(1000, "Server shutting down"); } websocket_clients.clear(); // Close all pending clients for (int i = 0; i < pending_clients.size(); i++) { pending_clients[i]->disconnect_from_host(); } pending_clients.clear(); // Stop listening if (tcp_server.is_valid()) { tcp_server->stop(); tcp_server.unref(); } is_running = false; print_line("StudioBridge: Server stopped"); } void StudioBridge::process() { if (!is_running || !tcp_server.is_valid()) { return; } // Process WebSocket clients _process_websocket_clients(); // Accept new connections if (tcp_server->is_connection_available()) { Ref client = tcp_server->take_connection(); if (client.is_valid()) { pending_clients.push_back(client); print_line("StudioBridge: New client connected"); } } // Process existing connections for (int i = pending_clients.size() - 1; i >= 0; i--) { Ref client = pending_clients[i]; if (client->get_status() != StreamPeerTCP::STATUS_CONNECTED) { pending_clients.remove_at(i); continue; } int available = client->get_available_bytes(); if (available > 0) { // Read the HTTP request Vector buffer; buffer.resize(available); Error err = client->get_data(buffer.ptrw(), available); if (err != OK) { pending_clients.remove_at(i); continue; } String request = String::utf8((const char *)buffer.ptr(), buffer.size()); // Parse HTTP request Dictionary headers; String body = _parse_http_request(request, headers); // Check if it's a WebSocket upgrade request if (_is_websocket_upgrade(headers)) { String ws_key = headers["sec-websocket-key"]; String accept_key = _build_websocket_accept_key(ws_key); String handshake = _build_websocket_handshake_response(accept_key); // Send handshake response client->put_data((const uint8_t *)handshake.utf8().get_data(), handshake.utf8().length()); // Create WebSocket peer and accept the stream Ref ws = WebSocketPeer::create(); if (ws.is_valid()) { Error ws_err = ws->accept_stream(client); if (ws_err == OK) { websocket_clients.push_back(ws); print_line("StudioBridge: WebSocket client upgraded"); } else { print_line("StudioBridge: Failed to upgrade to WebSocket"); } } // Remove from pending (now a WebSocket) pending_clients.remove_at(i); continue; } // Check if it's a POST to /rpc if (headers.has("method") && headers["method"] == "POST" && headers.has("path") && headers["path"] == "/rpc") { // Parse JSON body Ref json; json.instantiate(); Error parse_err = json->parse(body); if (parse_err == OK) { Dictionary rpc_request = json->get_data(); String method = rpc_request.get("method", ""); Dictionary params = rpc_request.get("params", Dictionary()); // Handle RPC call Dictionary result = handle_rpc_call(method, params); // Convert result to JSON String result_json = JSON::stringify(result); // Send HTTP response String response = _build_http_response(result_json); client->put_data((const uint8_t *)response.utf8().get_data(), response.utf8().length()); } else { // Invalid JSON Dictionary error; error["success"] = false; error["error"] = "Invalid JSON in request body"; String error_json = JSON::stringify(error); String response = _build_http_response(error_json); client->put_data((const uint8_t *)response.utf8().get_data(), response.utf8().length()); } } else { // Invalid request Dictionary error; error["success"] = false; error["error"] = "Only POST /rpc is supported"; String error_json = JSON::stringify(error); String response = _build_http_response(error_json); client->put_data((const uint8_t *)response.utf8().get_data(), response.utf8().length()); } // Close connection (HTTP/1.0 style - one request per connection) client->disconnect_from_host(); pending_clients.remove_at(i); } } } String StudioBridge::_parse_http_request(const String &p_request, Dictionary &r_headers) { // Split request into lines Vector lines = p_request.split("\r\n"); if (lines.size() == 0) { return ""; } // Parse request line (e.g., "POST /rpc HTTP/1.1") Vector request_line = lines[0].split(" "); if (request_line.size() >= 2) { r_headers["method"] = request_line[0]; r_headers["path"] = request_line[1]; } // Find the empty line that separates headers from body int body_start = -1; for (int i = 1; i < lines.size(); i++) { if (lines[i].is_empty() || lines[i] == "\r" || lines[i] == "\n") { body_start = i + 1; break; } // Parse header (e.g., "Content-Type: application/json") int colon_pos = lines[i].find(":"); if (colon_pos > 0) { String key = lines[i].substr(0, colon_pos).strip_edges().to_lower(); String value = lines[i].substr(colon_pos + 1).strip_edges(); r_headers[key] = value; } } // Extract body if (body_start > 0 && body_start < lines.size()) { String body; for (int i = body_start; i < lines.size(); i++) { body += lines[i]; if (i < lines.size() - 1) { body += "\r\n"; } } return body.strip_edges(); } return ""; } String StudioBridge::_build_http_response(const String &p_body, const String &p_content_type) { String response = "HTTP/1.1 200 OK\r\n"; response += "Content-Type: " + p_content_type + "\r\n"; response += "Content-Length: " + itos(p_body.utf8().length()) + "\r\n"; response += "Access-Control-Allow-Origin: *\r\n"; // Allow CORS for web Studio response += "Connection: close\r\n"; response += "\r\n"; response += p_body; return response; } Dictionary StudioBridge::handle_rpc_call(const String &p_method, const Dictionary &p_params) { // Basic operations if (p_method == "loadScene") { return _handle_load_scene(p_params); } else if (p_method == "saveScene") { return _handle_save_scene(p_params); } else if (p_method == "createNode") { return _handle_create_node(p_params); } else if (p_method == "deleteNode") { return _handle_delete_node(p_params); } else if (p_method == "setProperty") { return _handle_set_property(p_params); } else if (p_method == "getProperty") { return _handle_get_property(p_params); } else if (p_method == "getSceneTree") { return _handle_get_scene_tree(p_params); } else if (p_method == "selectNode") { return _handle_select_node(p_params); } else if (p_method == "runGame") { return _handle_run_game(p_params); } else if (p_method == "stopGame") { return _handle_stop_game(p_params); } // Advanced node operations else if (p_method == "moveNode") { return _handle_move_node(p_params); } else if (p_method == "duplicateNode") { return _handle_duplicate_node(p_params); } else if (p_method == "renameNode") { return _handle_rename_node(p_params); } else if (p_method == "reparentNode") { return _handle_reparent_node(p_params); } // Advanced properties else if (p_method == "getAllProperties") { return _handle_get_all_properties(p_params); } else if (p_method == "getPropertyInfo") { return _handle_get_property_info(p_params); } // File system else if (p_method == "listDirectory") { return _handle_list_directory(p_params); } else if (p_method == "getRecentFiles") { return _handle_get_recent_files(p_params); } // Scripts else if (p_method == "attachScript") { return _handle_attach_script(p_params); } else if (p_method == "detachScript") { return _handle_detach_script(p_params); } else if (p_method == "getNodeScript") { return _handle_get_node_script(p_params); } else if (p_method == "saveScript") { return _handle_save_script(p_params); } // Groups else if (p_method == "getNodeGroups") { return _handle_get_node_groups(p_params); } else if (p_method == "addToGroup") { return _handle_add_to_group(p_params); } else if (p_method == "removeFromGroup") { return _handle_remove_from_group(p_params); } // Utilities else if (p_method == "getAllNodeTypes") { return _handle_get_all_node_types(p_params); } return _error_response(vformat("Unknown method: %s", p_method)); } Dictionary StudioBridge::_handle_load_scene(const Dictionary &p_params) { String path = p_params.get("path", ""); if (path.is_empty()) { return _error_response("Missing 'path' parameter"); } Ref scene = ResourceLoader::load(path); if (scene.is_null()) { return _error_response(vformat("Failed to load scene: %s", path)); } Node *instance = scene->instantiate(); if (!instance) { return _error_response("Failed to instantiate scene"); } set_edited_scene_root(instance); emit_scene_changed(); Dictionary result; result["path"] = path; result["root"] = _node_to_dict(instance); return _success_response(result); } Dictionary StudioBridge::_handle_save_scene(const Dictionary &p_params) { String path = p_params.get("path", ""); if (path.is_empty()) { return _error_response("Missing 'path' parameter"); } if (!edited_scene_root) { return _error_response("No scene loaded"); } Ref packed; packed.instantiate(); Error err = packed->pack(edited_scene_root); if (err != OK) { return _error_response("Failed to pack scene"); } err = ResourceSaver::save(packed, path); if (err != OK) { return _error_response(vformat("Failed to save scene to: %s", path)); } Dictionary result; result["path"] = path; return _success_response(result); } Dictionary StudioBridge::_handle_create_node(const Dictionary &p_params) { String type = p_params.get("type", ""); String parent_path = p_params.get("parent", ""); String name = p_params.get("name", ""); if (type.is_empty()) { return _error_response("Missing 'type' parameter"); } if (!edited_scene_root) { return _error_response("No scene loaded"); } // Create node instance Object *obj = ClassDB::instantiate(type); if (!obj) { return _error_response(vformat("Unknown node type: %s", type)); } Node *node = Object::cast_to(obj); if (!node) { memdelete(obj); return _error_response(vformat("Not a Node type: %s", type)); } if (!name.is_empty()) { node->set_name(name); } // Find parent node Node *parent = edited_scene_root; if (!parent_path.is_empty()) { parent = edited_scene_root->get_node_or_null(parent_path); if (!parent) { memdelete(node); return _error_response(vformat("Parent not found: %s", parent_path)); } } parent->add_child(node); node->set_owner(edited_scene_root); Dictionary result = _node_to_dict(node); return _success_response(result); } Dictionary StudioBridge::_handle_delete_node(const Dictionary &p_params) { String node_path = p_params.get("path", ""); if (node_path.is_empty()) { return _error_response("Missing 'path' parameter"); } if (!edited_scene_root) { return _error_response("No scene loaded"); } Node *node = edited_scene_root->get_node_or_null(node_path); if (!node) { return _error_response(vformat("Node not found: %s", node_path)); } if (node == edited_scene_root) { return _error_response("Cannot delete root node"); } String parent_path = String(node->get_parent()->get_path()); node->queue_free(); Dictionary result; result["deleted_path"] = node_path; result["parent_path"] = parent_path; return _success_response(result); } Dictionary StudioBridge::_handle_set_property(const Dictionary &p_params) { String node_path = p_params.get("path", ""); String property = p_params.get("property", ""); Variant value = p_params.get("value", Variant()); if (node_path.is_empty() || property.is_empty()) { return _error_response("Missing 'path' or 'property' parameter"); } if (!edited_scene_root) { return _error_response("No scene loaded"); } Node *node = edited_scene_root->get_node_or_null(node_path); if (!node) { return _error_response(vformat("Node not found: %s", node_path)); } bool valid = false; node->set(property, value, &valid); if (!valid) { return _error_response(vformat("Invalid property: %s", property)); } emit_property_changed(node, property); Dictionary result; result["path"] = node_path; result["property"] = property; result["value"] = value; return _success_response(result); } Dictionary StudioBridge::_handle_get_property(const Dictionary &p_params) { String node_path = p_params.get("path", ""); String property = p_params.get("property", ""); if (node_path.is_empty() || property.is_empty()) { return _error_response("Missing 'path' or 'property' parameter"); } if (!edited_scene_root) { return _error_response("No scene loaded"); } Node *node = edited_scene_root->get_node_or_null(node_path); if (!node) { return _error_response(vformat("Node not found: %s", node_path)); } bool valid = false; Variant value = node->get(property, &valid); if (!valid) { return _error_response(vformat("Invalid property: %s", property)); } Dictionary result; result["path"] = node_path; result["property"] = property; result["value"] = value; return _success_response(result); } Dictionary StudioBridge::_handle_get_scene_tree(const Dictionary &p_params) { if (!edited_scene_root) { return _error_response("No scene loaded"); } Dictionary result = _node_to_dict(edited_scene_root); return _success_response(result); } Dictionary StudioBridge::_handle_select_node(const Dictionary &p_params) { String node_path = p_params.get("path", ""); if (!edited_scene_root) { return _error_response("No scene loaded"); } Node *node = nullptr; if (!node_path.is_empty()) { node = edited_scene_root->get_node_or_null(node_path); if (!node) { return _error_response(vformat("Node not found: %s", node_path)); } } set_selected_node(node); emit_node_selected(node); Dictionary result; if (node) { result["node"] = _node_to_dict(node); } return _success_response(result); } Dictionary StudioBridge::_handle_run_game(const Dictionary &p_params) { // TODO: Implement game running // This would launch the game in a separate process or window emit_console_output("Game started", "info"); return _success_response(); } Dictionary StudioBridge::_handle_stop_game(const Dictionary &p_params) { // TODO: Implement game stopping emit_console_output("Game stopped", "info"); return _success_response(); } // Advanced node operations Dictionary StudioBridge::_handle_move_node(const Dictionary &p_params) { String node_path = p_params.get("path", ""); String new_parent_path = p_params.get("newParent", ""); int position = p_params.get("position", -1); if (node_path.is_empty() || new_parent_path.is_empty()) { return _error_response("Missing 'path' or 'newParent' parameter"); } if (!edited_scene_root) { return _error_response("No scene loaded"); } Node *node = edited_scene_root->get_node_or_null(node_path); if (!node) { return _error_response(vformat("Node not found: %s", node_path)); } Node *new_parent = edited_scene_root->get_node_or_null(new_parent_path); if (!new_parent) { return _error_response(vformat("Parent not found: %s", new_parent_path)); } Node *old_parent = node->get_parent(); if (old_parent) { old_parent->remove_child(node); } new_parent->add_child(node); if (position >= 0 && position < new_parent->get_child_count()) { new_parent->move_child(node, position); } node->set_owner(edited_scene_root); Dictionary result; result["path"] = String(node->get_path()); result["newParent"] = new_parent_path; return _success_response(result); } Dictionary StudioBridge::_handle_duplicate_node(const Dictionary &p_params) { String node_path = p_params.get("path", ""); if (node_path.is_empty()) { return _error_response("Missing 'path' parameter"); } if (!edited_scene_root) { return _error_response("No scene loaded"); } Node *node = edited_scene_root->get_node_or_null(node_path); if (!node) { return _error_response(vformat("Node not found: %s", node_path)); } Node *duplicate = node->duplicate(); if (!duplicate) { return _error_response("Failed to duplicate node"); } // Generate unique name String base_name = node->get_name(); String new_name = base_name + "Copy"; int suffix = 2; Node *parent = node->get_parent(); while (parent && parent->has_node(new_name)) { new_name = vformat("%sCopy%d", base_name, suffix++); } duplicate->set_name(new_name); if (parent) { parent->add_child(duplicate); duplicate->set_owner(edited_scene_root); } return _success_response(_node_to_dict(duplicate)); } Dictionary StudioBridge::_handle_rename_node(const Dictionary &p_params) { String node_path = p_params.get("path", ""); String new_name = p_params.get("name", ""); if (node_path.is_empty() || new_name.is_empty()) { return _error_response("Missing 'path' or 'name' parameter"); } if (!edited_scene_root) { return _error_response("No scene loaded"); } Node *node = edited_scene_root->get_node_or_null(node_path); if (!node) { return _error_response(vformat("Node not found: %s", node_path)); } node->set_name(new_name); Dictionary result; result["oldPath"] = node_path; result["newPath"] = String(node->get_path()); result["name"] = new_name; return _success_response(result); } Dictionary StudioBridge::_handle_reparent_node(const Dictionary &p_params) { // Alias for move_node return _handle_move_node(p_params); } // Advanced properties Dictionary StudioBridge::_handle_get_all_properties(const Dictionary &p_params) { String node_path = p_params.get("path", ""); if (node_path.is_empty()) { return _error_response("Missing 'path' parameter"); } if (!edited_scene_root) { return _error_response("No scene loaded"); } Node *node = edited_scene_root->get_node_or_null(node_path); if (!node) { return _error_response(vformat("Node not found: %s", node_path)); } Array properties; List prop_list; node->get_property_list(&prop_list); for (const PropertyInfo &prop : prop_list) { // Skip internal properties if (prop.usage & PROPERTY_USAGE_INTERNAL) { continue; } Dictionary prop_dict; prop_dict["name"] = prop.name; prop_dict["type"] = Variant::get_type_name(prop.type); prop_dict["value"] = node->get(prop.name); prop_dict["hint"] = prop.hint; prop_dict["hint_string"] = prop.hint_string; properties.push_back(prop_dict); } return _success_response(properties); } Dictionary StudioBridge::_handle_get_property_info(const Dictionary &p_params) { String node_path = p_params.get("path", ""); String property = p_params.get("property", ""); if (node_path.is_empty() || property.is_empty()) { return _error_response("Missing 'path' or 'property' parameter"); } if (!edited_scene_root) { return _error_response("No scene loaded"); } Node *node = edited_scene_root->get_node_or_null(node_path); if (!node) { return _error_response(vformat("Node not found: %s", node_path)); } List prop_list; node->get_property_list(&prop_list); for (const PropertyInfo &prop : prop_list) { if (prop.name == property) { Dictionary info; info["name"] = prop.name; info["type"] = Variant::get_type_name(prop.type); info["hint"] = prop.hint; info["hint_string"] = prop.hint_string; info["usage"] = prop.usage; return _success_response(info); } } return _error_response(vformat("Property not found: %s", property)); } // File system Dictionary StudioBridge::_handle_list_directory(const Dictionary &p_params) { String path = p_params.get("path", "res://"); Ref dir = DirAccess::open(path); if (dir.is_null()) { return _error_response(vformat("Cannot open directory: %s", path)); } Array files; Array directories; dir->list_dir_begin(); String file_name = dir->get_next(); while (!file_name.is_empty()) { if (file_name != "." && file_name != "..") { Dictionary entry; entry["name"] = file_name; entry["path"] = path.path_join(file_name); if (dir->current_is_dir()) { directories.push_back(entry); } else { entry["size"] = dir->get_space_left(); // Placeholder files.push_back(entry); } } file_name = dir->get_next(); } dir->list_dir_end(); Dictionary result; result["path"] = path; result["directories"] = directories; result["files"] = files; return _success_response(result); } Dictionary StudioBridge::_handle_get_recent_files(const Dictionary &p_params) { // TODO: Implement recent files tracking Array recent; return _success_response(recent); } // Scripts Dictionary StudioBridge::_handle_attach_script(const Dictionary &p_params) { String node_path = p_params.get("nodePath", ""); String script_path = p_params.get("scriptPath", ""); if (node_path.is_empty() || script_path.is_empty()) { return _error_response("Missing 'nodePath' or 'scriptPath' parameter"); } if (!edited_scene_root) { return _error_response("No scene loaded"); } Node *node = edited_scene_root->get_node_or_null(node_path); if (!node) { return _error_response(vformat("Node not found: %s", node_path)); } Ref