350 lines
11 KiB
C++
350 lines
11 KiB
C++
/**************************************************************************/
|
|
/* aethex_cloud.cpp */
|
|
/**************************************************************************/
|
|
/* AeThex Engine */
|
|
/* https://aethex.dev */
|
|
/**************************************************************************/
|
|
|
|
#include "aethex_cloud.h"
|
|
#include "aethex_auth.h"
|
|
#include "aethex_cloud_save.h"
|
|
#include "aethex_asset_library.h"
|
|
#include "aethex_telemetry.h"
|
|
|
|
#include "core/io/json.h"
|
|
#include "core/config/project_settings.h"
|
|
#include "core/version.h"
|
|
|
|
AethexCloud *AethexCloud::singleton = nullptr;
|
|
|
|
AethexCloud *AethexCloud::get_singleton() {
|
|
return singleton;
|
|
}
|
|
|
|
void AethexCloud::_bind_methods() {
|
|
// Configuration
|
|
ClassDB::bind_method(D_METHOD("set_gateway_url", "url"), &AethexCloud::set_gateway_url);
|
|
ClassDB::bind_method(D_METHOD("get_gateway_url"), &AethexCloud::get_gateway_url);
|
|
ClassDB::bind_method(D_METHOD("set_api_key", "key"), &AethexCloud::set_api_key);
|
|
ClassDB::bind_method(D_METHOD("get_api_key"), &AethexCloud::get_api_key);
|
|
|
|
// Connection
|
|
ClassDB::bind_method(D_METHOD("connect_to_gateway"), &AethexCloud::connect_to_gateway);
|
|
ClassDB::bind_method(D_METHOD("disconnect_from_gateway"), &AethexCloud::disconnect_from_gateway);
|
|
ClassDB::bind_method(D_METHOD("get_status"), &AethexCloud::get_status);
|
|
ClassDB::bind_method(D_METHOD("is_connected"), &AethexCloud::is_connected);
|
|
|
|
// Auth token
|
|
ClassDB::bind_method(D_METHOD("set_auth_token", "token"), &AethexCloud::set_auth_token);
|
|
ClassDB::bind_method(D_METHOD("get_auth_token"), &AethexCloud::get_auth_token);
|
|
|
|
// Sub-services
|
|
ClassDB::bind_method(D_METHOD("get_auth"), &AethexCloud::get_auth);
|
|
ClassDB::bind_method(D_METHOD("get_cloud_save"), &AethexCloud::get_cloud_save);
|
|
ClassDB::bind_method(D_METHOD("get_asset_library"), &AethexCloud::get_asset_library);
|
|
ClassDB::bind_method(D_METHOD("get_telemetry"), &AethexCloud::get_telemetry);
|
|
|
|
// HTTP
|
|
ClassDB::bind_method(D_METHOD("make_request", "endpoint", "method", "data"), &AethexCloud::make_request, DEFVAL(HTTPClient::METHOD_GET), DEFVAL(Dictionary()));
|
|
ClassDB::bind_method(D_METHOD("make_request_async", "endpoint", "method", "data", "callback"), &AethexCloud::make_request_async, DEFVAL(HTTPClient::METHOD_GET), DEFVAL(Dictionary()), DEFVAL(Callable()));
|
|
|
|
// Properties
|
|
ADD_PROPERTY(PropertyInfo(Variant::STRING, "gateway_url"), "set_gateway_url", "get_gateway_url");
|
|
ADD_PROPERTY(PropertyInfo(Variant::STRING, "api_key"), "set_api_key", "get_api_key");
|
|
ADD_PROPERTY(PropertyInfo(Variant::STRING, "auth_token"), "set_auth_token", "get_auth_token");
|
|
|
|
// Signals
|
|
ADD_SIGNAL(MethodInfo("connected"));
|
|
ADD_SIGNAL(MethodInfo("disconnected"));
|
|
ADD_SIGNAL(MethodInfo("connection_error", PropertyInfo(Variant::STRING, "error")));
|
|
ADD_SIGNAL(MethodInfo("message_received", PropertyInfo(Variant::DICTIONARY, "message")));
|
|
|
|
// Enums
|
|
BIND_ENUM_CONSTANT(STATUS_DISCONNECTED);
|
|
BIND_ENUM_CONSTANT(STATUS_CONNECTING);
|
|
BIND_ENUM_CONSTANT(STATUS_CONNECTED);
|
|
BIND_ENUM_CONSTANT(STATUS_ERROR);
|
|
}
|
|
|
|
AethexCloud::AethexCloud() {
|
|
singleton = this;
|
|
|
|
// Default gateway URL - can be overridden in project settings
|
|
gateway_url = "https://engine.aethex.dev";
|
|
|
|
// Load from project settings if available
|
|
if (ProjectSettings::get_singleton()->has_setting("aethex/cloud/gateway_url")) {
|
|
gateway_url = GLOBAL_GET("aethex/cloud/gateway_url");
|
|
}
|
|
if (ProjectSettings::get_singleton()->has_setting("aethex/cloud/api_key")) {
|
|
api_key = GLOBAL_GET("aethex/cloud/api_key");
|
|
}
|
|
|
|
// Initialize sub-services
|
|
auth.instantiate();
|
|
cloud_save.instantiate();
|
|
asset_library.instantiate();
|
|
telemetry.instantiate();
|
|
|
|
// Set parent reference
|
|
auth->set_cloud(this);
|
|
cloud_save->set_cloud(this);
|
|
asset_library->set_cloud(this);
|
|
telemetry->set_cloud(this);
|
|
|
|
// Initialize WebSocket
|
|
websocket.instantiate();
|
|
}
|
|
|
|
AethexCloud::~AethexCloud() {
|
|
disconnect_from_gateway();
|
|
singleton = nullptr;
|
|
}
|
|
|
|
void AethexCloud::set_gateway_url(const String &p_url) {
|
|
gateway_url = p_url;
|
|
}
|
|
|
|
String AethexCloud::get_gateway_url() const {
|
|
return gateway_url;
|
|
}
|
|
|
|
void AethexCloud::set_api_key(const String &p_key) {
|
|
api_key = p_key;
|
|
}
|
|
|
|
String AethexCloud::get_api_key() const {
|
|
return api_key;
|
|
}
|
|
|
|
void AethexCloud::set_auth_token(const String &p_token) {
|
|
auth_token = p_token;
|
|
}
|
|
|
|
String AethexCloud::get_auth_token() const {
|
|
return auth_token;
|
|
}
|
|
|
|
Error AethexCloud::connect_to_gateway() {
|
|
if (status == STATUS_CONNECTED || status == STATUS_CONNECTING) {
|
|
return OK;
|
|
}
|
|
|
|
status = STATUS_CONNECTING;
|
|
|
|
// Connect WebSocket for real-time
|
|
String ws_url = gateway_url.replace("https://", "wss://").replace("http://", "ws://") + "/ws";
|
|
|
|
Error err = websocket->connect_to_url(ws_url);
|
|
if (err != OK) {
|
|
status = STATUS_ERROR;
|
|
emit_signal("connection_error", "Failed to connect to WebSocket");
|
|
return err;
|
|
}
|
|
|
|
return OK;
|
|
}
|
|
|
|
void AethexCloud::disconnect_from_gateway() {
|
|
if (websocket.is_valid()) {
|
|
websocket->close();
|
|
}
|
|
websocket_connected = false;
|
|
status = STATUS_DISCONNECTED;
|
|
emit_signal("disconnected");
|
|
}
|
|
|
|
AethexCloud::ConnectionStatus AethexCloud::get_status() const {
|
|
return status;
|
|
}
|
|
|
|
bool AethexCloud::is_connected() const {
|
|
return status == STATUS_CONNECTED;
|
|
}
|
|
|
|
Ref<AethexAuth> AethexCloud::get_auth() const {
|
|
return auth;
|
|
}
|
|
|
|
Ref<AethexCloudSave> AethexCloud::get_cloud_save() const {
|
|
return cloud_save;
|
|
}
|
|
|
|
Ref<AethexAssetLibrary> AethexCloud::get_asset_library() const {
|
|
return asset_library;
|
|
}
|
|
|
|
Ref<AethexTelemetry> AethexCloud::get_telemetry() const {
|
|
return telemetry;
|
|
}
|
|
|
|
Dictionary AethexCloud::make_request(const String &p_endpoint, HTTPClient::Method p_method, const Dictionary &p_data) {
|
|
Dictionary result;
|
|
result["success"] = false;
|
|
|
|
Ref<HTTPClient> http;
|
|
http.instantiate();
|
|
|
|
// Parse gateway URL
|
|
String host = gateway_url.replace("https://", "").replace("http://", "");
|
|
bool use_ssl = gateway_url.begins_with("https://");
|
|
int port = use_ssl ? 443 : 80;
|
|
|
|
// Connect
|
|
Error err = http->connect_to_host(host, port);
|
|
if (err != OK) {
|
|
result["error"] = "Failed to connect to gateway";
|
|
return result;
|
|
}
|
|
|
|
// Wait for connection
|
|
while (http->get_status() == HTTPClient::STATUS_CONNECTING ||
|
|
http->get_status() == HTTPClient::STATUS_RESOLVING) {
|
|
http->poll();
|
|
OS::get_singleton()->delay_usec(100000); // 100ms
|
|
}
|
|
|
|
if (http->get_status() != HTTPClient::STATUS_CONNECTED) {
|
|
result["error"] = "Connection failed";
|
|
return result;
|
|
}
|
|
|
|
// Prepare headers
|
|
Vector<String> headers;
|
|
headers.push_back("Content-Type: application/json");
|
|
headers.push_back("X-Engine-Version: " + String(VERSION_FULL_CONFIG));
|
|
|
|
if (!auth_token.is_empty()) {
|
|
headers.push_back("Authorization: Bearer " + auth_token);
|
|
}
|
|
if (!api_key.is_empty()) {
|
|
headers.push_back("X-API-Key: " + api_key);
|
|
}
|
|
|
|
// Prepare body
|
|
String body_str;
|
|
if (!p_data.is_empty()) {
|
|
body_str = JSON::stringify(p_data);
|
|
}
|
|
|
|
// Make request
|
|
err = http->request(p_method, p_endpoint, headers, body_str);
|
|
if (err != OK) {
|
|
result["error"] = "Request failed";
|
|
return result;
|
|
}
|
|
|
|
// Wait for response
|
|
while (http->get_status() == HTTPClient::STATUS_REQUESTING) {
|
|
http->poll();
|
|
OS::get_singleton()->delay_usec(100000);
|
|
}
|
|
|
|
if (!http->has_response()) {
|
|
result["error"] = "No response";
|
|
return result;
|
|
}
|
|
|
|
// Read response
|
|
PackedByteArray response_body;
|
|
while (http->get_status() == HTTPClient::STATUS_BODY) {
|
|
http->poll();
|
|
PackedByteArray chunk = http->read_response_body_chunk();
|
|
if (chunk.size() > 0) {
|
|
response_body.append_array(chunk);
|
|
}
|
|
OS::get_singleton()->delay_usec(10000);
|
|
}
|
|
|
|
// Parse response
|
|
int response_code = http->get_response_code();
|
|
String response_str = String::utf8((const char *)response_body.ptr(), response_body.size());
|
|
|
|
result["status_code"] = response_code;
|
|
result["success"] = response_code >= 200 && response_code < 300;
|
|
|
|
// Try to parse as JSON
|
|
JSON json;
|
|
if (json.parse(response_str) == OK) {
|
|
result["data"] = json.get_data();
|
|
} else {
|
|
result["data"] = response_str;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void AethexCloud::make_request_async(const String &p_endpoint, HTTPClient::Method p_method, const Dictionary &p_data, const Callable &p_callback) {
|
|
// For async requests, we'll use HTTPRequest node in actual implementation
|
|
// This is a simplified version that calls synchronously
|
|
// TODO: Implement proper async with threading or HTTPRequest
|
|
Dictionary result = make_request(p_endpoint, p_method, p_data);
|
|
if (p_callback.is_valid()) {
|
|
p_callback.call(result);
|
|
}
|
|
}
|
|
|
|
void AethexCloud::_on_websocket_message(const PackedByteArray &p_data) {
|
|
String message_str = String::utf8((const char *)p_data.ptr(), p_data.size());
|
|
|
|
JSON json;
|
|
if (json.parse(message_str) == OK) {
|
|
Dictionary message = json.get_data();
|
|
String type = message.get("type", "");
|
|
|
|
if (type == "connected") {
|
|
status = STATUS_CONNECTED;
|
|
websocket_connected = true;
|
|
emit_signal("connected");
|
|
|
|
// Authenticate WebSocket if we have a token
|
|
if (!auth_token.is_empty()) {
|
|
Dictionary auth_msg;
|
|
auth_msg["type"] = "auth";
|
|
auth_msg["token"] = auth_token;
|
|
auth_msg["engineVersion"] = VERSION_FULL_CONFIG;
|
|
websocket->send_text(JSON::stringify(auth_msg));
|
|
}
|
|
} else if (type == "auth_success") {
|
|
// WebSocket authenticated
|
|
} else if (type == "pong") {
|
|
// Heartbeat response
|
|
} else {
|
|
emit_signal("message_received", message);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AethexCloud::process(double p_delta) {
|
|
if (!websocket.is_valid()) {
|
|
return;
|
|
}
|
|
|
|
websocket->poll();
|
|
|
|
WebSocketPeer::State ws_state = websocket->get_ready_state();
|
|
|
|
switch (ws_state) {
|
|
case WebSocketPeer::STATE_OPEN: {
|
|
while (websocket->get_available_packet_count() > 0) {
|
|
PackedByteArray packet = websocket->get_packet();
|
|
_on_websocket_message(packet);
|
|
}
|
|
} break;
|
|
|
|
case WebSocketPeer::STATE_CLOSING:
|
|
// Still processing
|
|
break;
|
|
|
|
case WebSocketPeer::STATE_CLOSED: {
|
|
if (websocket_connected) {
|
|
websocket_connected = false;
|
|
status = STATUS_DISCONNECTED;
|
|
emit_signal("disconnected");
|
|
}
|
|
} break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|