AeThex-Engine-Core/engine/modules/studio_bridge/studio_bridge.cpp

1174 lines
32 KiB
C++

/**************************************************************************/
/* 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<StreamPeerTCP> 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<StreamPeerTCP> 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<uint8_t> 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<WebSocketPeer> 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;
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<String> lines = p_request.split("\r\n");
if (lines.size() == 0) {
return "";
}
// Parse request line (e.g., "POST /rpc HTTP/1.1")
Vector<String> 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<PackedScene> 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<PackedScene> 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<Node>(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<PropertyInfo> 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<PropertyInfo> 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<DirAccess> 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<Script> script = ResourceLoader::load(script_path);
if (script.is_null()) {
return _error_response(vformat("Failed to load script: %s", script_path));
}
node->set_script(script);
Dictionary result;
result["nodePath"] = node_path;
result["scriptPath"] = script_path;
return _success_response(result);
}
Dictionary StudioBridge::_handle_detach_script(const Dictionary &p_params) {
String node_path = p_params.get("nodePath", "");
if (node_path.is_empty()) {
return _error_response("Missing 'nodePath' 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_script(Ref<Script>());
Dictionary result;
result["nodePath"] = node_path;
return _success_response(result);
}
Dictionary StudioBridge::_handle_get_node_script(const Dictionary &p_params) {
String node_path = p_params.get("nodePath", "");
if (node_path.is_empty()) {
return _error_response("Missing 'nodePath' 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<Script> script = node->get_script();
if (script.is_null()) {
Dictionary result;
result["hasScript"] = false;
return _success_response(result);
}
Dictionary result;
result["hasScript"] = true;
result["scriptPath"] = String(script->get_path());
result["language"] = script->get_language()->get_name();
return _success_response(result);
}
Dictionary StudioBridge::_handle_save_script(const Dictionary &p_params) {
String path = p_params.get("path", "");
String content = p_params.get("content", "");
if (path.is_empty()) {
return _error_response("Missing 'path' parameter");
}
Ref<FileAccess> file = FileAccess::open(path, FileAccess::WRITE);
if (file.is_null()) {
return _error_response(vformat("Cannot write to file: %s", path));
}
file->store_string(content);
file->close();
Dictionary result;
result["path"] = path;
result["size"] = content.length();
return _success_response(result);
}
// Groups
Dictionary StudioBridge::_handle_get_node_groups(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 groups;
List<Node::GroupInfo> group_list;
node->get_groups(&group_list);
for (const Node::GroupInfo &group : group_list) {
groups.push_back(group.name);
}
return _success_response(groups);
}
Dictionary StudioBridge::_handle_add_to_group(const Dictionary &p_params) {
String node_path = p_params.get("path", "");
String group_name = p_params.get("group", "");
if (node_path.is_empty() || group_name.is_empty()) {
return _error_response("Missing 'path' or 'group' 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->add_to_group(group_name);
Dictionary result;
result["nodePath"] = node_path;
result["group"] = group_name;
return _success_response(result);
}
Dictionary StudioBridge::_handle_remove_from_group(const Dictionary &p_params) {
String node_path = p_params.get("path", "");
String group_name = p_params.get("group", "");
if (node_path.is_empty() || group_name.is_empty()) {
return _error_response("Missing 'path' or 'group' 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->remove_from_group(group_name);
Dictionary result;
result["nodePath"] = node_path;
result["group"] = group_name;
return _success_response(result);
}
// Utilities
Dictionary StudioBridge::_handle_get_all_node_types(const Dictionary &p_params) {
Array types;
LocalVector<StringName> class_list;
ClassDB::get_class_list(class_list);
for (const StringName &class_name : class_list) {
if (ClassDB::is_parent_class(class_name, "Node") && ClassDB::can_instantiate(class_name)) {
Dictionary type_info;
type_info["name"] = class_name;
type_info["parent"] = ClassDB::get_parent_class(class_name);
types.push_back(type_info);
}
}
return _success_response(types);
}
// Helper methods
Dictionary StudioBridge::_node_to_dict(Node *p_node) {
Dictionary dict;
if (!p_node) {
return dict;
}
dict["name"] = p_node->get_name();
dict["type"] = p_node->get_class();
dict["path"] = String(p_node->get_path());
// Add children recursively
Array children;
for (int i = 0; i < p_node->get_child_count(); i++) {
Node *child = p_node->get_child(i);
children.push_back(_node_to_dict(child));
}
dict["children"] = children;
return dict;
}
Dictionary StudioBridge::_error_response(const String &p_message) {
Dictionary response;
response["success"] = false;
response["error"] = p_message;
print_line(vformat("StudioBridge Error: %s", p_message));
return response;
}
Dictionary StudioBridge::_success_response(const Variant &p_result) {
Dictionary response;
response["success"] = true;
response["result"] = p_result;
return response;
}
void StudioBridge::emit_scene_changed() {
Dictionary data;
if (edited_scene_root) {
data["scene"] = _node_to_dict(edited_scene_root);
}
_broadcast_event("scene_changed", data);
print_line("StudioBridge: Scene changed event");
}
void StudioBridge::emit_node_selected(Node *p_node) {
Dictionary data;
if (p_node) {
data["node"] = _node_to_dict(p_node);
print_line(vformat("StudioBridge: Node selected: %s", p_node->get_name()));
}
_broadcast_event("node_selected", data);
}
void StudioBridge::emit_property_changed(Node *p_node, const String &p_property) {
Dictionary data;
if (p_node) {
data["nodePath"] = String(p_node->get_path());
data["property"] = p_property;
bool valid = false;
data["value"] = p_node->get(p_property, &valid);
print_line(vformat("StudioBridge: Property changed: %s.%s", p_node->get_name(), p_property));
}
_broadcast_event("property_changed", data);
}
void StudioBridge::emit_console_output(const String &p_message, const String &p_type) {
Dictionary data;
data["message"] = p_message;
data["type"] = p_type;
_broadcast_event("console_output", data);
print_line(vformat("StudioBridge [%s]: %s", p_type, p_message));
}
void StudioBridge::set_edited_scene_root(Node *p_node) {
edited_scene_root = p_node;
}
void StudioBridge::set_selected_node(Node *p_node) {
selected_node = p_node;
}
// WebSocket implementation
bool StudioBridge::_is_websocket_upgrade(const Dictionary &p_headers) {
// Check for WebSocket upgrade request
if (p_headers.has("upgrade") && p_headers.has("sec-websocket-key")) {
String upgrade = String(p_headers["upgrade"]).to_lower();
return upgrade == "websocket";
}
return false;
}
String StudioBridge::_build_websocket_accept_key(const String &p_key) {
// WebSocket handshake: SHA-1(key + GUID)
String magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
String combined = p_key + magic;
// Calculate SHA-1 hash
CryptoCore::SHA1Context sha1;
sha1.start();
sha1.update((const uint8_t *)combined.utf8().get_data(),combined.utf8().length());
uint8_t hash[20];
sha1.finish(hash);
// Base64 encode
return CryptoCore::b64_encode_str(hash, 20);
}
String StudioBridge::_build_websocket_handshake_response(const String &p_accept_key) {
String response = "HTTP/1.1 101 Switching Protocols\r\n";
response += "Upgrade: websocket\r\n";
response += "Connection: Upgrade\r\n";
response += "Sec-WebSocket-Accept: " + p_accept_key + "\r\n";
response += "\r\n";
return response;
}
void StudioBridge::_process_websocket_clients() {
// Poll and clean up disconnected WebSocket clients
for (int i = websocket_clients.size() - 1; i >= 0; i--) {
Ref<WebSocketPeer> ws = websocket_clients[i];
ws->poll();
WebSocketPeer::State state = ws->get_ready_state();
if (state == WebSocketPeer::STATE_CLOSED) {
websocket_clients.remove_at(i);
print_line("StudioBridge: WebSocket client disconnected");
} else if (state == WebSocketPeer::STATE_OPEN) {
// Handle incoming WebSocket messages (if needed)
while (ws->get_available_packet_count() > 0) {
Vector<uint8_t> packet;
Error err = ws->get_packet_buffer(packet);
if (err == OK) {
String message = String::utf8((const char *)packet.ptr(), packet.size());
// Could handle ping/pong or client commands here
print_line("WebSocket message received: " + message);
}
}
}
}
}
void StudioBridge::_broadcast_event(const String &p_event_type, const Dictionary &p_data) {
// Create event JSON
Dictionary event;
event["type"] = p_event_type;
event["data"] = p_data;
event["timestamp"] = Time::get_singleton()->get_unix_time_from_system();
String event_json = JSON::stringify(event);
// Send to all connected WebSocket clients
for (int i = 0; i < websocket_clients.size(); i++) {
Ref<WebSocketPeer> ws = websocket_clients[i];
if (ws.is_valid() && ws->get_ready_state() == WebSocketPeer::STATE_OPEN) {
ws->send_text(event_json);
}
}
}