1174 lines
32 KiB
C++
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);
|
|
}
|
|
}
|
|
}
|