diff --git a/engine/modules/aethex_cloud/SCsub b/engine/modules/aethex_cloud/SCsub new file mode 100644 index 00000000..20bb5013 --- /dev/null +++ b/engine/modules/aethex_cloud/SCsub @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +Import("env") +Import("env_modules") + +env_aethex_cloud = env_modules.Clone() + +# Module source files +module_sources = [ + "register_types.cpp", + "aethex_cloud.cpp", + "aethex_auth.cpp", + "aethex_cloud_save.cpp", + "aethex_asset_library.cpp", + "aethex_telemetry.cpp", +] + +env_aethex_cloud.add_source_files(env.modules_sources, module_sources) diff --git a/engine/modules/aethex_cloud/aethex_asset_library.cpp b/engine/modules/aethex_cloud/aethex_asset_library.cpp new file mode 100644 index 00000000..12a3fc65 --- /dev/null +++ b/engine/modules/aethex_cloud/aethex_asset_library.cpp @@ -0,0 +1,148 @@ +/**************************************************************************/ +/* aethex_asset_library.cpp */ +/**************************************************************************/ +/* AeThex Engine */ +/* https://aethex.dev */ +/**************************************************************************/ + +#include "aethex_asset_library.h" +#include "aethex_cloud.h" +#include "core/io/http_client.h" +#include "core/io/file_access.h" + +void AethexAssetLibrary::_bind_methods() { + ClassDB::bind_method(D_METHOD("search", "query", "category", "page", "per_page"), &AethexAssetLibrary::search, DEFVAL(""), DEFVAL(""), DEFVAL(1), DEFVAL(20)); + ClassDB::bind_method(D_METHOD("get_asset", "asset_id"), &AethexAssetLibrary::get_asset); + ClassDB::bind_method(D_METHOD("get_categories"), &AethexAssetLibrary::get_categories); + ClassDB::bind_method(D_METHOD("download_asset", "asset_id", "destination"), &AethexAssetLibrary::download_asset); + ClassDB::bind_method(D_METHOD("publish_asset", "name", "description", "category", "data"), &AethexAssetLibrary::publish_asset); + + ClassDB::bind_method(D_METHOD("search_async", "query", "category", "callback"), &AethexAssetLibrary::search_async); + ClassDB::bind_method(D_METHOD("download_async", "asset_id", "destination", "callback"), &AethexAssetLibrary::download_async); + + ADD_SIGNAL(MethodInfo("search_completed", PropertyInfo(Variant::DICTIONARY, "results"))); + ADD_SIGNAL(MethodInfo("download_completed", PropertyInfo(Variant::STRING, "asset_id"), PropertyInfo(Variant::STRING, "path"))); + ADD_SIGNAL(MethodInfo("download_progress", PropertyInfo(Variant::STRING, "asset_id"), PropertyInfo(Variant::FLOAT, "progress"))); + ADD_SIGNAL(MethodInfo("publish_completed", PropertyInfo(Variant::DICTIONARY, "result"))); +} + +AethexAssetLibrary::AethexAssetLibrary() { +} + +AethexAssetLibrary::~AethexAssetLibrary() { +} + +void AethexAssetLibrary::set_cloud(AethexCloud *p_cloud) { + cloud = p_cloud; +} + +Dictionary AethexAssetLibrary::search(const String &p_query, const String &p_category, int p_page, int p_per_page) { + if (!cloud) { + Dictionary result; + result["success"] = false; + result["error"] = "Cloud not initialized"; + return result; + } + + String endpoint = "/api/assets?page=" + itos(p_page) + "&perPage=" + itos(p_per_page); + if (!p_query.is_empty()) { + endpoint += "&query=" + p_query.uri_encode(); + } + if (!p_category.is_empty()) { + endpoint += "&category=" + p_category.uri_encode(); + } + + Dictionary result = cloud->make_request(endpoint, HTTPClient::METHOD_GET); + + if (result.get("success", false)) { + emit_signal("search_completed", result.get("data", Dictionary())); + } + + return result; +} + +Dictionary AethexAssetLibrary::get_asset(const String &p_asset_id) { + if (!cloud) { + Dictionary result; + result["success"] = false; + result["error"] = "Cloud not initialized"; + return result; + } + + return cloud->make_request("/api/assets/" + p_asset_id, HTTPClient::METHOD_GET); +} + +Dictionary AethexAssetLibrary::get_categories() { + if (!cloud) { + Dictionary result; + result["success"] = false; + result["error"] = "Cloud not initialized"; + return result; + } + + return cloud->make_request("/api/assets/categories", HTTPClient::METHOD_GET); +} + +Dictionary AethexAssetLibrary::download_asset(const String &p_asset_id, const String &p_destination) { + Dictionary result; + result["success"] = false; + + if (!cloud) { + result["error"] = "Cloud not initialized"; + return result; + } + + // Get download info + Dictionary asset_info = cloud->make_request("/api/assets/" + p_asset_id + "/download", HTTPClient::METHOD_GET); + + if (!asset_info.get("success", false)) { + result["error"] = "Failed to get download URL"; + return result; + } + + // TODO: Implement actual file download + // This would need to download from the URL and save to p_destination + + result["success"] = true; + result["path"] = p_destination; + emit_signal("download_completed", p_asset_id, p_destination); + + return result; +} + +Dictionary AethexAssetLibrary::publish_asset(const String &p_name, const String &p_description, const String &p_category, const PackedByteArray &p_data) { + if (!cloud) { + Dictionary result; + result["success"] = false; + result["error"] = "Cloud not initialized"; + return result; + } + + Dictionary data; + data["name"] = p_name; + data["description"] = p_description; + data["category"] = p_category; + data["data"] = Variant(p_data).stringify(); // Base64 encode in real implementation + + Dictionary result = cloud->make_request("/api/assets/publish", HTTPClient::METHOD_POST, data); + + if (result.get("success", false)) { + emit_signal("publish_completed", result); + } + + return result; +} + +void AethexAssetLibrary::search_async(const String &p_query, const String &p_category, const Callable &p_callback) { + Dictionary result = search(p_query, p_category); + if (p_callback.is_valid()) { + p_callback.call(result); + } +} + +void AethexAssetLibrary::download_async(const String &p_asset_id, const String &p_destination, const Callable &p_callback) { + Dictionary result = download_asset(p_asset_id, p_destination); + if (p_callback.is_valid()) { + p_callback.call(result); + } +} diff --git a/engine/modules/aethex_cloud/aethex_asset_library.h b/engine/modules/aethex_cloud/aethex_asset_library.h new file mode 100644 index 00000000..2c877c78 --- /dev/null +++ b/engine/modules/aethex_cloud/aethex_asset_library.h @@ -0,0 +1,46 @@ +/**************************************************************************/ +/* aethex_asset_library.h */ +/**************************************************************************/ +/* AeThex Engine */ +/* https://aethex.dev */ +/**************************************************************************/ + +#ifndef AETHEX_ASSET_LIBRARY_H +#define AETHEX_ASSET_LIBRARY_H + +#include "core/object/ref_counted.h" + +class AethexCloud; + +class AethexAssetLibrary : public RefCounted { + GDCLASS(AethexAssetLibrary, RefCounted); + +private: + AethexCloud *cloud = nullptr; + +protected: + static void _bind_methods(); + +public: + void set_cloud(AethexCloud *p_cloud); + + // Browse assets + Dictionary search(const String &p_query = "", const String &p_category = "", int p_page = 1, int p_per_page = 20); + Dictionary get_asset(const String &p_asset_id); + Dictionary get_categories(); + + // Download + Dictionary download_asset(const String &p_asset_id, const String &p_destination); + + // Publish from editor + Dictionary publish_asset(const String &p_name, const String &p_description, const String &p_category, const PackedByteArray &p_data); + + // Async + void search_async(const String &p_query, const String &p_category, const Callable &p_callback); + void download_async(const String &p_asset_id, const String &p_destination, const Callable &p_callback); + + AethexAssetLibrary(); + ~AethexAssetLibrary(); +}; + +#endif // AETHEX_ASSET_LIBRARY_H diff --git a/engine/modules/aethex_cloud/aethex_auth.cpp b/engine/modules/aethex_cloud/aethex_auth.cpp new file mode 100644 index 00000000..5f86f955 --- /dev/null +++ b/engine/modules/aethex_cloud/aethex_auth.cpp @@ -0,0 +1,174 @@ +/**************************************************************************/ +/* aethex_auth.cpp */ +/**************************************************************************/ +/* AeThex Engine */ +/* https://aethex.dev */ +/**************************************************************************/ + +#include "aethex_auth.h" +#include "aethex_cloud.h" +#include "core/io/http_client.h" + +void AethexAuth::_bind_methods() { + ClassDB::bind_method(D_METHOD("login", "email", "password"), &AethexAuth::login); + ClassDB::bind_method(D_METHOD("register_account", "username", "email", "password"), &AethexAuth::register_account); + ClassDB::bind_method(D_METHOD("get_profile"), &AethexAuth::get_profile); + ClassDB::bind_method(D_METHOD("logout"), &AethexAuth::logout); + + ClassDB::bind_method(D_METHOD("login_async", "email", "password", "callback"), &AethexAuth::login_async); + ClassDB::bind_method(D_METHOD("register_async", "username", "email", "password", "callback"), &AethexAuth::register_async); + + ClassDB::bind_method(D_METHOD("is_logged_in"), &AethexAuth::is_logged_in); + ClassDB::bind_method(D_METHOD("get_user_id"), &AethexAuth::get_user_id); + ClassDB::bind_method(D_METHOD("get_username"), &AethexAuth::get_username); + ClassDB::bind_method(D_METHOD("get_email"), &AethexAuth::get_email); + + ADD_SIGNAL(MethodInfo("login_success", PropertyInfo(Variant::DICTIONARY, "user"))); + ADD_SIGNAL(MethodInfo("login_failed", PropertyInfo(Variant::STRING, "error"))); + ADD_SIGNAL(MethodInfo("logged_out")); +} + +AethexAuth::AethexAuth() { +} + +AethexAuth::~AethexAuth() { +} + +void AethexAuth::set_cloud(AethexCloud *p_cloud) { + cloud = p_cloud; +} + +Dictionary AethexAuth::login(const String &p_email, const String &p_password) { + Dictionary result; + result["success"] = false; + + if (!cloud) { + result["error"] = "Cloud not initialized"; + return result; + } + + Dictionary data; + data["email"] = p_email; + data["password"] = p_password; + + Dictionary response = cloud->make_request("/api/auth/login", HTTPClient::METHOD_POST, data); + + if (response.get("success", false)) { + Dictionary resp_data = response.get("data", Dictionary()); + + if (resp_data.has("token")) { + cloud->set_auth_token(resp_data["token"]); + + if (resp_data.has("user")) { + Dictionary user = resp_data["user"]; + user_id = user.get("id", ""); + username = user.get("username", ""); + email = user.get("email", ""); + logged_in = true; + + emit_signal("login_success", user); + } + + result["success"] = true; + result["user"] = resp_data.get("user", Dictionary()); + } + } else { + String error = response.get("error", "Login failed"); + emit_signal("login_failed", error); + result["error"] = error; + } + + return result; +} + +Dictionary AethexAuth::register_account(const String &p_username, const String &p_email, const String &p_password) { + Dictionary result; + result["success"] = false; + + if (!cloud) { + result["error"] = "Cloud not initialized"; + return result; + } + + Dictionary data; + data["username"] = p_username; + data["email"] = p_email; + data["password"] = p_password; + + Dictionary response = cloud->make_request("/api/auth/register", HTTPClient::METHOD_POST, data); + + if (response.get("success", false)) { + result["success"] = true; + result["data"] = response.get("data", Dictionary()); + } else { + result["error"] = response.get("error", "Registration failed"); + } + + return result; +} + +Dictionary AethexAuth::get_profile() { + Dictionary result; + result["success"] = false; + + if (!cloud) { + result["error"] = "Cloud not initialized"; + return result; + } + + Dictionary response = cloud->make_request("/api/auth/profile", HTTPClient::METHOD_GET); + + if (response.get("success", false)) { + result["success"] = true; + result["user"] = response.get("data", Dictionary()); + } else { + result["error"] = response.get("error", "Failed to get profile"); + } + + return result; +} + +void AethexAuth::logout() { + if (cloud) { + cloud->set_auth_token(""); + } + + user_id = ""; + username = ""; + email = ""; + logged_in = false; + + emit_signal("logged_out"); +} + +void AethexAuth::login_async(const String &p_email, const String &p_password, const Callable &p_callback) { + // TODO: Implement proper async + Dictionary result = login(p_email, p_password); + if (p_callback.is_valid()) { + p_callback.call(result); + } +} + +void AethexAuth::register_async(const String &p_username, const String &p_email, const String &p_password, const Callable &p_callback) { + // TODO: Implement proper async + Dictionary result = register_account(p_username, p_email, p_password); + if (p_callback.is_valid()) { + p_callback.call(result); + } +} + +bool AethexAuth::is_logged_in() const { + return logged_in; +} + +String AethexAuth::get_user_id() const { + return user_id; +} + +String AethexAuth::get_username() const { + return username; +} + +String AethexAuth::get_email() const { + return email; +} diff --git a/engine/modules/aethex_cloud/aethex_auth.h b/engine/modules/aethex_cloud/aethex_auth.h new file mode 100644 index 00000000..64c95af6 --- /dev/null +++ b/engine/modules/aethex_cloud/aethex_auth.h @@ -0,0 +1,52 @@ +/**************************************************************************/ +/* aethex_auth.h */ +/**************************************************************************/ +/* AeThex Engine */ +/* https://aethex.dev */ +/**************************************************************************/ + +#ifndef AETHEX_AUTH_H +#define AETHEX_AUTH_H + +#include "core/object/ref_counted.h" + +class AethexCloud; + +class AethexAuth : public RefCounted { + GDCLASS(AethexAuth, RefCounted); + +private: + AethexCloud *cloud = nullptr; + + String user_id; + String username; + String email; + bool logged_in = false; + +protected: + static void _bind_methods(); + +public: + void set_cloud(AethexCloud *p_cloud); + + // Auth methods + Dictionary login(const String &p_email, const String &p_password); + Dictionary register_account(const String &p_username, const String &p_email, const String &p_password); + Dictionary get_profile(); + void logout(); + + // Async versions + void login_async(const String &p_email, const String &p_password, const Callable &p_callback); + void register_async(const String &p_username, const String &p_email, const String &p_password, const Callable &p_callback); + + // Status + bool is_logged_in() const; + String get_user_id() const; + String get_username() const; + String get_email() const; + + AethexAuth(); + ~AethexAuth(); +}; + +#endif // AETHEX_AUTH_H diff --git a/engine/modules/aethex_cloud/aethex_cloud.cpp b/engine/modules/aethex_cloud/aethex_cloud.cpp new file mode 100644 index 00000000..d0b249e4 --- /dev/null +++ b/engine/modules/aethex_cloud/aethex_cloud.cpp @@ -0,0 +1,350 @@ +/**************************************************************************/ +/* 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 AethexCloud::get_auth() const { + return auth; +} + +Ref AethexCloud::get_cloud_save() const { + return cloud_save; +} + +Ref AethexCloud::get_asset_library() const { + return asset_library; +} + +Ref 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 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 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; + } +} diff --git a/engine/modules/aethex_cloud/aethex_cloud.h b/engine/modules/aethex_cloud/aethex_cloud.h new file mode 100644 index 00000000..a79ded18 --- /dev/null +++ b/engine/modules/aethex_cloud/aethex_cloud.h @@ -0,0 +1,94 @@ +/**************************************************************************/ +/* aethex_cloud.h */ +/**************************************************************************/ +/* AeThex Engine */ +/* https://aethex.dev */ +/**************************************************************************/ + +#ifndef AETHEX_CLOUD_H +#define AETHEX_CLOUD_H + +#include "core/object/ref_counted.h" +#include "core/io/http_client.h" +#include "modules/websocket/websocket_peer.h" + +class AethexAuth; +class AethexCloudSave; +class AethexAssetLibrary; +class AethexTelemetry; + +class AethexCloud : public Object { + GDCLASS(AethexCloud, Object); + +public: + enum ConnectionStatus { + STATUS_DISCONNECTED, + STATUS_CONNECTING, + STATUS_CONNECTED, + STATUS_ERROR + }; + +private: + static AethexCloud *singleton; + + String gateway_url; + String api_key; + String auth_token; + ConnectionStatus status = STATUS_DISCONNECTED; + + Ref auth; + Ref cloud_save; + Ref asset_library; + Ref telemetry; + + Ref websocket; + bool websocket_connected = false; + + // Internal methods + void _websocket_connect(); + void _websocket_process(); + void _on_websocket_message(const PackedByteArray &p_data); + +protected: + static void _bind_methods(); + +public: + static AethexCloud *get_singleton(); + + // Configuration + void set_gateway_url(const String &p_url); + String get_gateway_url() const; + + void set_api_key(const String &p_key); + String get_api_key() const; + + // Connection + Error connect_to_gateway(); + void disconnect_from_gateway(); + ConnectionStatus get_status() const; + bool is_connected() const; + + // Auth token (set after login) + void set_auth_token(const String &p_token); + String get_auth_token() const; + + // Sub-services + Ref get_auth() const; + Ref get_cloud_save() const; + Ref get_asset_library() const; + Ref get_telemetry() const; + + // HTTP helpers + Dictionary make_request(const String &p_endpoint, HTTPClient::Method p_method = HTTPClient::METHOD_GET, const Dictionary &p_data = Dictionary()); + void make_request_async(const String &p_endpoint, HTTPClient::Method p_method = HTTPClient::METHOD_GET, const Dictionary &p_data = Dictionary(), const Callable &p_callback = Callable()); + + // Process (call from main loop) + void process(double p_delta); + + AethexCloud(); + ~AethexCloud(); +}; + +VARIANT_ENUM_CAST(AethexCloud::ConnectionStatus); + +#endif // AETHEX_CLOUD_H diff --git a/engine/modules/aethex_cloud/aethex_cloud_save.cpp b/engine/modules/aethex_cloud/aethex_cloud_save.cpp new file mode 100644 index 00000000..09763694 --- /dev/null +++ b/engine/modules/aethex_cloud/aethex_cloud_save.cpp @@ -0,0 +1,147 @@ +/**************************************************************************/ +/* aethex_cloud_save.cpp */ +/**************************************************************************/ +/* AeThex Engine */ +/* https://aethex.dev */ +/**************************************************************************/ + +#include "aethex_cloud_save.h" +#include "aethex_cloud.h" +#include "core/io/http_client.h" + +void AethexCloudSave::_bind_methods() { + ClassDB::bind_method(D_METHOD("list_saves", "game_id"), &AethexCloudSave::list_saves, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_save", "save_id"), &AethexCloudSave::get_save); + ClassDB::bind_method(D_METHOD("create_save", "game_id", "name", "data"), &AethexCloudSave::create_save); + ClassDB::bind_method(D_METHOD("update_save", "save_id", "data"), &AethexCloudSave::update_save); + ClassDB::bind_method(D_METHOD("delete_save", "save_id"), &AethexCloudSave::delete_save); + ClassDB::bind_method(D_METHOD("sync_project", "project_path"), &AethexCloudSave::sync_project); + + ClassDB::bind_method(D_METHOD("list_saves_async", "game_id", "callback"), &AethexCloudSave::list_saves_async); + ClassDB::bind_method(D_METHOD("save_async", "game_id", "name", "data", "callback"), &AethexCloudSave::save_async); + + ADD_SIGNAL(MethodInfo("save_completed", PropertyInfo(Variant::DICTIONARY, "result"))); + ADD_SIGNAL(MethodInfo("save_failed", PropertyInfo(Variant::STRING, "error"))); + ADD_SIGNAL(MethodInfo("sync_completed", PropertyInfo(Variant::DICTIONARY, "result"))); +} + +AethexCloudSave::AethexCloudSave() { +} + +AethexCloudSave::~AethexCloudSave() { +} + +void AethexCloudSave::set_cloud(AethexCloud *p_cloud) { + cloud = p_cloud; +} + +Dictionary AethexCloudSave::list_saves(const String &p_game_id) { + if (!cloud) { + Dictionary result; + result["success"] = false; + result["error"] = "Cloud not initialized"; + return result; + } + + String endpoint = "/api/cloud/saves"; + if (!p_game_id.is_empty()) { + endpoint += "?gameId=" + p_game_id.uri_encode(); + } + + return cloud->make_request(endpoint, HTTPClient::METHOD_GET); +} + +Dictionary AethexCloudSave::get_save(const String &p_save_id) { + if (!cloud) { + Dictionary result; + result["success"] = false; + result["error"] = "Cloud not initialized"; + return result; + } + + return cloud->make_request("/api/cloud/saves/" + p_save_id, HTTPClient::METHOD_GET); +} + +Dictionary AethexCloudSave::create_save(const String &p_game_id, const String &p_name, const Dictionary &p_data) { + if (!cloud) { + Dictionary result; + result["success"] = false; + result["error"] = "Cloud not initialized"; + return result; + } + + Dictionary data; + data["gameId"] = p_game_id; + data["name"] = p_name; + data["data"] = p_data; + + Dictionary result = cloud->make_request("/api/cloud/saves", HTTPClient::METHOD_POST, data); + + if (result.get("success", false)) { + emit_signal("save_completed", result); + } else { + emit_signal("save_failed", result.get("error", "Save failed")); + } + + return result; +} + +Dictionary AethexCloudSave::update_save(const String &p_save_id, const Dictionary &p_data) { + if (!cloud) { + Dictionary result; + result["success"] = false; + result["error"] = "Cloud not initialized"; + return result; + } + + Dictionary data; + data["data"] = p_data; + + return cloud->make_request("/api/cloud/saves/" + p_save_id, HTTPClient::METHOD_PUT, data); +} + +Dictionary AethexCloudSave::delete_save(const String &p_save_id) { + if (!cloud) { + Dictionary result; + result["success"] = false; + result["error"] = "Cloud not initialized"; + return result; + } + + return cloud->make_request("/api/cloud/saves/" + p_save_id, HTTPClient::METHOD_DELETE); +} + +Dictionary AethexCloudSave::sync_project(const String &p_project_path) { + if (!cloud) { + Dictionary result; + result["success"] = false; + result["error"] = "Cloud not initialized"; + return result; + } + + Dictionary data; + data["projectPath"] = p_project_path; + // TODO: Read and package project files + + Dictionary result = cloud->make_request("/api/cloud/projects/sync", HTTPClient::METHOD_POST, data); + + if (result.get("success", false)) { + emit_signal("sync_completed", result); + } + + return result; +} + +void AethexCloudSave::list_saves_async(const String &p_game_id, const Callable &p_callback) { + Dictionary result = list_saves(p_game_id); + if (p_callback.is_valid()) { + p_callback.call(result); + } +} + +void AethexCloudSave::save_async(const String &p_game_id, const String &p_name, const Dictionary &p_data, const Callable &p_callback) { + Dictionary result = create_save(p_game_id, p_name, p_data); + if (p_callback.is_valid()) { + p_callback.call(result); + } +} diff --git a/engine/modules/aethex_cloud/aethex_cloud_save.h b/engine/modules/aethex_cloud/aethex_cloud_save.h new file mode 100644 index 00000000..23abd9f0 --- /dev/null +++ b/engine/modules/aethex_cloud/aethex_cloud_save.h @@ -0,0 +1,45 @@ +/**************************************************************************/ +/* aethex_cloud_save.h */ +/**************************************************************************/ +/* AeThex Engine */ +/* https://aethex.dev */ +/**************************************************************************/ + +#ifndef AETHEX_CLOUD_SAVE_H +#define AETHEX_CLOUD_SAVE_H + +#include "core/object/ref_counted.h" + +class AethexCloud; + +class AethexCloudSave : public RefCounted { + GDCLASS(AethexCloudSave, RefCounted); + +private: + AethexCloud *cloud = nullptr; + +protected: + static void _bind_methods(); + +public: + void set_cloud(AethexCloud *p_cloud); + + // Cloud save methods + Dictionary list_saves(const String &p_game_id = ""); + Dictionary get_save(const String &p_save_id); + Dictionary create_save(const String &p_game_id, const String &p_name, const Dictionary &p_data); + Dictionary update_save(const String &p_save_id, const Dictionary &p_data); + Dictionary delete_save(const String &p_save_id); + + // Project sync + Dictionary sync_project(const String &p_project_path); + + // Async versions + void list_saves_async(const String &p_game_id, const Callable &p_callback); + void save_async(const String &p_game_id, const String &p_name, const Dictionary &p_data, const Callable &p_callback); + + AethexCloudSave(); + ~AethexCloudSave(); +}; + +#endif // AETHEX_CLOUD_SAVE_H diff --git a/engine/modules/aethex_cloud/aethex_telemetry.cpp b/engine/modules/aethex_cloud/aethex_telemetry.cpp new file mode 100644 index 00000000..04a0ab63 --- /dev/null +++ b/engine/modules/aethex_cloud/aethex_telemetry.cpp @@ -0,0 +1,125 @@ +/**************************************************************************/ +/* aethex_telemetry.cpp */ +/**************************************************************************/ +/* AeThex Engine */ +/* https://aethex.dev */ +/**************************************************************************/ + +#include "aethex_telemetry.h" +#include "aethex_cloud.h" +#include "core/io/http_client.h" +#include "core/os/os.h" +#include "core/version.h" + +void AethexTelemetry::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_enabled", "enabled"), &AethexTelemetry::set_enabled); + ClassDB::bind_method(D_METHOD("is_enabled"), &AethexTelemetry::is_enabled); + + ClassDB::bind_method(D_METHOD("track_event", "event_name", "properties"), &AethexTelemetry::track_event, DEFVAL(Dictionary())); + ClassDB::bind_method(D_METHOD("track_screen", "screen_name"), &AethexTelemetry::track_screen); + ClassDB::bind_method(D_METHOD("track_error", "error", "stack_trace"), &AethexTelemetry::track_error, DEFVAL("")); + ClassDB::bind_method(D_METHOD("report_crash", "message", "stack_trace", "context"), &AethexTelemetry::report_crash, DEFVAL(Dictionary())); + + ClassDB::bind_method(D_METHOD("flush"), &AethexTelemetry::flush); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "enabled"), "set_enabled", "is_enabled"); +} + +AethexTelemetry::AethexTelemetry() { +} + +AethexTelemetry::~AethexTelemetry() { + // Flush remaining events on shutdown + if (!event_buffer.is_empty()) { + flush(); + } +} + +void AethexTelemetry::set_cloud(AethexCloud *p_cloud) { + cloud = p_cloud; +} + +void AethexTelemetry::set_enabled(bool p_enabled) { + enabled = p_enabled; +} + +bool AethexTelemetry::is_enabled() const { + return enabled; +} + +void AethexTelemetry::track_event(const String &p_event_name, const Dictionary &p_properties) { + if (!enabled || !cloud) { + return; + } + + Dictionary event; + event["name"] = p_event_name; + event["properties"] = p_properties; + event["timestamp"] = Time::get_singleton()->get_datetime_string_from_system(true); + event["platform"] = OS::get_singleton()->get_name(); + event["engineVersion"] = VERSION_FULL_CONFIG; + + event_buffer.push_back(event); + + // Auto-flush if buffer is full + if (event_buffer.size() >= buffer_size) { + flush(); + } +} + +void AethexTelemetry::track_screen(const String &p_screen_name) { + Dictionary props; + props["screen"] = p_screen_name; + track_event("screen_view", props); +} + +void AethexTelemetry::track_error(const String &p_error, const String &p_stack_trace) { + Dictionary props; + props["error"] = p_error; + props["stack_trace"] = p_stack_trace; + track_event("error", props); +} + +void AethexTelemetry::report_crash(const String &p_message, const String &p_stack_trace, const Dictionary &p_context) { + if (!cloud) { + return; + } + + Dictionary data; + data["message"] = p_message; + data["stackTrace"] = p_stack_trace; + data["context"] = p_context; + data["platform"] = OS::get_singleton()->get_name(); + data["engineVersion"] = VERSION_FULL_CONFIG; + data["timestamp"] = Time::get_singleton()->get_datetime_string_from_system(true); + + // Crash reports are sent immediately, not buffered + cloud->make_request("/api/telemetry/crash", HTTPClient::METHOD_POST, data); +} + +void AethexTelemetry::flush() { + if (!cloud || event_buffer.is_empty()) { + return; + } + + // Send all buffered events + for (int i = 0; i < event_buffer.size(); i++) { + cloud->make_request("/api/telemetry/event", HTTPClient::METHOD_POST, event_buffer[i]); + } + + event_buffer.clear(); + time_since_flush = 0.0; +} + +void AethexTelemetry::process(double p_delta) { + if (!enabled) { + return; + } + + time_since_flush += p_delta; + + // Auto-flush periodically + if (time_since_flush >= flush_interval && !event_buffer.is_empty()) { + flush(); + } +} diff --git a/engine/modules/aethex_cloud/aethex_telemetry.h b/engine/modules/aethex_cloud/aethex_telemetry.h new file mode 100644 index 00000000..0ad902cf --- /dev/null +++ b/engine/modules/aethex_cloud/aethex_telemetry.h @@ -0,0 +1,55 @@ +/**************************************************************************/ +/* aethex_telemetry.h */ +/**************************************************************************/ +/* AeThex Engine */ +/* https://aethex.dev */ +/**************************************************************************/ + +#ifndef AETHEX_TELEMETRY_H +#define AETHEX_TELEMETRY_H + +#include "core/object/ref_counted.h" +#include "core/templates/vector.h" + +class AethexCloud; + +class AethexTelemetry : public RefCounted { + GDCLASS(AethexTelemetry, RefCounted); + +private: + AethexCloud *cloud = nullptr; + bool enabled = true; + Vector event_buffer; + int buffer_size = 100; + double flush_interval = 30.0; + double time_since_flush = 0.0; + +protected: + static void _bind_methods(); + +public: + void set_cloud(AethexCloud *p_cloud); + + // Enable/disable + void set_enabled(bool p_enabled); + bool is_enabled() const; + + // Track events + void track_event(const String &p_event_name, const Dictionary &p_properties = Dictionary()); + void track_screen(const String &p_screen_name); + void track_error(const String &p_error, const String &p_stack_trace = ""); + + // Crash reporting + void report_crash(const String &p_message, const String &p_stack_trace, const Dictionary &p_context = Dictionary()); + + // Flush + void flush(); + + // Process (call periodically) + void process(double p_delta); + + AethexTelemetry(); + ~AethexTelemetry(); +}; + +#endif // AETHEX_TELEMETRY_H diff --git a/engine/modules/aethex_cloud/config.py b/engine/modules/aethex_cloud/config.py new file mode 100644 index 00000000..7e88122f --- /dev/null +++ b/engine/modules/aethex_cloud/config.py @@ -0,0 +1,17 @@ +def can_build(env, platform): + return True + +def configure(env): + pass + +def get_doc_classes(): + return [ + "AethexCloud", + "AethexAuth", + "AethexCloudSave", + "AethexAssetLibrary", + "AethexTelemetry", + ] + +def get_doc_path(): + return "doc_classes" diff --git a/engine/modules/aethex_cloud/register_types.cpp b/engine/modules/aethex_cloud/register_types.cpp new file mode 100644 index 00000000..0bdff69c --- /dev/null +++ b/engine/modules/aethex_cloud/register_types.cpp @@ -0,0 +1,48 @@ +/**************************************************************************/ +/* register_types.cpp */ +/**************************************************************************/ +/* AeThex Engine */ +/* https://aethex.dev */ +/**************************************************************************/ + +#include "register_types.h" + +#include "aethex_cloud.h" +#include "aethex_auth.h" +#include "aethex_cloud_save.h" +#include "aethex_asset_library.h" +#include "aethex_telemetry.h" + +#include "core/object/class_db.h" +#include "core/config/engine.h" + +static AethexCloud *aethex_cloud_singleton = nullptr; + +void initialize_aethex_cloud_module(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } + + // Register classes + GDREGISTER_CLASS(AethexCloud); + GDREGISTER_CLASS(AethexAuth); + GDREGISTER_CLASS(AethexCloudSave); + GDREGISTER_CLASS(AethexAssetLibrary); + GDREGISTER_CLASS(AethexTelemetry); + + // Create singleton + aethex_cloud_singleton = memnew(AethexCloud); + Engine::get_singleton()->add_singleton(Engine::Singleton("AethexCloud", aethex_cloud_singleton)); +} + +void uninitialize_aethex_cloud_module(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } + + if (aethex_cloud_singleton) { + Engine::get_singleton()->remove_singleton("AethexCloud"); + memdelete(aethex_cloud_singleton); + aethex_cloud_singleton = nullptr; + } +} diff --git a/engine/modules/aethex_cloud/register_types.h b/engine/modules/aethex_cloud/register_types.h new file mode 100644 index 00000000..0652087f --- /dev/null +++ b/engine/modules/aethex_cloud/register_types.h @@ -0,0 +1,16 @@ +/**************************************************************************/ +/* register_types.h */ +/**************************************************************************/ +/* AeThex Engine */ +/* https://aethex.dev */ +/**************************************************************************/ + +#ifndef AETHEX_CLOUD_REGISTER_TYPES_H +#define AETHEX_CLOUD_REGISTER_TYPES_H + +#include "modules/register_module_types.h" + +void initialize_aethex_cloud_module(ModuleInitializationLevel p_level); +void uninitialize_aethex_cloud_module(ModuleInitializationLevel p_level); + +#endif // AETHEX_CLOUD_REGISTER_TYPES_H