/**************************************************************************/ /* aethex_launcher.cpp */ /**************************************************************************/ /* AeThex Engine */ /* https://aethex.dev */ /**************************************************************************/ #include "aethex_launcher.h" #include "core/io/json.h" #include "core/io/dir_access.h" #include "core/io/file_access.h" #include "core/io/http_client.h" #include "core/os/os.h" #include "core/os/time.h" #include "core/config/project_settings.h" #include "core/crypto/crypto.h" AethexLauncher *AethexLauncher::singleton = nullptr; AethexLauncher *AethexLauncher::get_singleton() { return singleton; } void AethexLauncher::_bind_methods() { // Initialization ClassDB::bind_method(D_METHOD("initialize"), &AethexLauncher::initialize); ClassDB::bind_method(D_METHOD("shutdown"), &AethexLauncher::shutdown); // API Configuration ClassDB::bind_method(D_METHOD("set_api_base_url", "url"), &AethexLauncher::set_api_base_url); ClassDB::bind_method(D_METHOD("get_api_base_url"), &AethexLauncher::get_api_base_url); ClassDB::bind_method(D_METHOD("set_supabase_config", "url", "anon_key"), &AethexLauncher::set_supabase_config); // Authentication ClassDB::bind_method(D_METHOD("sign_in_with_email", "email", "password"), &AethexLauncher::sign_in_with_email); ClassDB::bind_method(D_METHOD("sign_up_with_email", "email", "password", "username"), &AethexLauncher::sign_up_with_email); ClassDB::bind_method(D_METHOD("sign_in_with_oauth", "provider"), &AethexLauncher::sign_in_with_oauth); ClassDB::bind_method(D_METHOD("sign_out"), &AethexLauncher::sign_out); ClassDB::bind_method(D_METHOD("is_authenticated"), &AethexLauncher::is_authenticated); ClassDB::bind_method(D_METHOD("get_oauth_url", "provider"), &AethexLauncher::get_oauth_url); ClassDB::bind_method(D_METHOD("handle_oauth_callback", "code", "provider"), &AethexLauncher::handle_oauth_callback); // User info ClassDB::bind_method(D_METHOD("get_user_id"), &AethexLauncher::get_user_id); ClassDB::bind_method(D_METHOD("get_username"), &AethexLauncher::get_username); ClassDB::bind_method(D_METHOD("get_email"), &AethexLauncher::get_email); ClassDB::bind_method(D_METHOD("get_avatar_url"), &AethexLauncher::get_avatar_url); // Sub-systems ClassDB::bind_method(D_METHOD("get_game_library"), &AethexLauncher::get_game_library); ClassDB::bind_method(D_METHOD("get_download_manager"), &AethexLauncher::get_download_manager); ClassDB::bind_method(D_METHOD("get_store"), &AethexLauncher::get_store); ClassDB::bind_method(D_METHOD("get_friend_system"), &AethexLauncher::get_friend_system); ClassDB::bind_method(D_METHOD("get_current_profile"), &AethexLauncher::get_current_profile); // Profile ClassDB::bind_method(D_METHOD("fetch_launcher_profile"), &AethexLauncher::fetch_launcher_profile); ClassDB::bind_method(D_METHOD("update_launcher_profile", "data"), &AethexLauncher::update_launcher_profile); ClassDB::bind_method(D_METHOD("create_launcher_profile", "gamertag"), &AethexLauncher::create_launcher_profile); // Quick actions ClassDB::bind_method(D_METHOD("launch_game", "game_id"), &AethexLauncher::launch_game); ClassDB::bind_method(D_METHOD("install_game", "game_id"), &AethexLauncher::install_game); ClassDB::bind_method(D_METHOD("uninstall_game", "game_id"), &AethexLauncher::uninstall_game); // Paths ClassDB::bind_method(D_METHOD("get_games_directory"), &AethexLauncher::get_games_directory); ClassDB::bind_method(D_METHOD("get_downloads_directory"), &AethexLauncher::get_downloads_directory); ClassDB::bind_method(D_METHOD("get_cache_directory"), &AethexLauncher::get_cache_directory); ClassDB::bind_method(D_METHOD("set_games_directory", "path"), &AethexLauncher::set_games_directory); // Signals ADD_SIGNAL(MethodInfo("authenticated", PropertyInfo(Variant::DICTIONARY, "user_data"))); ADD_SIGNAL(MethodInfo("authentication_failed", PropertyInfo(Variant::STRING, "error"))); ADD_SIGNAL(MethodInfo("signed_out")); ADD_SIGNAL(MethodInfo("profile_updated", PropertyInfo(Variant::DICTIONARY, "profile"))); ADD_SIGNAL(MethodInfo("profile_created", PropertyInfo(Variant::DICTIONARY, "profile"))); ADD_SIGNAL(MethodInfo("game_launched", PropertyInfo(Variant::STRING, "game_id"))); ADD_SIGNAL(MethodInfo("game_installed", PropertyInfo(Variant::STRING, "game_id"))); ADD_SIGNAL(MethodInfo("game_uninstalled", PropertyInfo(Variant::STRING, "game_id"))); } AethexLauncher::AethexLauncher() { singleton = this; // Initialize sub-systems game_library.instantiate(); download_manager.instantiate(); store.instantiate(); friend_system.instantiate(); current_profile.instantiate(); // Default config path config_path = OS::get_singleton()->get_user_data_dir() + "/aethex_launcher.cfg"; } AethexLauncher::~AethexLauncher() { shutdown(); singleton = nullptr; } void AethexLauncher::initialize() { _load_config(); _load_cached_auth(); // Initialize sub-systems if (game_library.is_valid()) { game_library->set_launcher(this); game_library->load_library(); } if (download_manager.is_valid()) { download_manager->set_launcher(this); } if (store.is_valid()) { store->set_launcher(this); } if (friend_system.is_valid()) { friend_system->set_launcher(this); } } void AethexLauncher::shutdown() { _save_config(); if (download_manager.is_valid()) { download_manager->cancel_all(); } } void AethexLauncher::_load_config() { config.instantiate(); if (FileAccess::exists(config_path)) { config->load(config_path); api_base_url = config->get_value("api", "base_url", "https://api.aethex.dev"); supabase_url = config->get_value("api", "supabase_url", ""); supabase_anon_key = config->get_value("api", "supabase_key", ""); } } void AethexLauncher::_save_config() { if (config.is_valid()) { config->set_value("api", "base_url", api_base_url); config->set_value("api", "supabase_url", supabase_url); config->set_value("api", "supabase_key", supabase_anon_key); // Save auth if logged in if (authenticated) { config->set_value("auth", "token", auth_token); config->set_value("auth", "user_id", user_id); config->set_value("auth", "username", username); config->set_value("auth", "email", email); } else { config->erase_section("auth"); } config->save(config_path); } } void AethexLauncher::_load_cached_auth() { if (config.is_valid() && config->has_section("auth")) { auth_token = config->get_value("auth", "token", ""); user_id = config->get_value("auth", "user_id", ""); username = config->get_value("auth", "username", ""); email = config->get_value("auth", "email", ""); if (!auth_token.is_empty()) { authenticated = true; // Verify token is still valid refresh_auth_token(); } } } Dictionary AethexLauncher::_make_api_request(const String &p_endpoint, int p_method, const Dictionary &p_data) { Dictionary result; result["success"] = false; // Use Supabase if configured String url = supabase_url.is_empty() ? api_base_url : supabase_url; // This is a simplified sync request - in production, use HTTPRequest node for async Ref http = Ref(HTTPClient::create()); Error err = http->connect_to_host(url, 443, TLSOptions::client()); if (err != OK) { result["error"] = "Failed to connect to server"; 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); } if (http->get_status() != HTTPClient::STATUS_CONNECTED) { result["error"] = "Connection failed"; return result; } // Build headers Vector headers; headers.push_back("Content-Type: application/json"); if (!auth_token.is_empty()) { headers.push_back("Authorization: Bearer " + auth_token); } if (!supabase_anon_key.is_empty()) { headers.push_back("apikey: " + supabase_anon_key); } // Build body String body = ""; if (!p_data.is_empty()) { body = JSON::stringify(p_data); } // Make request CharString body_bytes = body.utf8(); err = http->request((HTTPClient::Method)p_method, p_endpoint, headers, (const uint8_t *)body_bytes.get_data(), body_bytes.length()); 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 received"; 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); } } int response_code = http->get_response_code(); String response_str = String::utf8((const char *)response_body.ptr(), response_body.size()); if (response_code >= 200 && response_code < 300) { result["success"] = true; if (!response_str.is_empty()) { Variant parsed = JSON::parse_string(response_str); if (parsed.get_type() != Variant::NIL) { result["data"] = parsed; } } } else { result["error"] = "Request failed with code: " + String::num_int64(response_code); if (!response_str.is_empty()) { Variant parsed = JSON::parse_string(response_str); if (parsed.get_type() == Variant::DICTIONARY) { Dictionary err_data = parsed; if (err_data.has("message")) { result["error"] = err_data["message"]; } } } } return result; } // API Configuration void AethexLauncher::set_api_base_url(const String &p_url) { api_base_url = p_url; } String AethexLauncher::get_api_base_url() const { return api_base_url; } void AethexLauncher::set_supabase_config(const String &p_url, const String &p_anon_key) { supabase_url = p_url; supabase_anon_key = p_anon_key; } // Authentication Error AethexLauncher::sign_in_with_email(const String &p_email, const String &p_password) { Dictionary data; data["email"] = p_email; data["password"] = p_password; String endpoint = supabase_url.is_empty() ? "/api/v1/auth/login" : "/auth/v1/token?grant_type=password"; Dictionary response = _make_api_request(endpoint, HTTPClient::METHOD_POST, data); if (response.get("success", false)) { Dictionary resp_data = response.get("data", Dictionary()); // Handle both our API and Supabase response formats if (resp_data.has("accessToken")) { auth_token = resp_data["accessToken"]; } else if (resp_data.has("access_token")) { auth_token = resp_data["access_token"]; } else if (resp_data.has("token")) { auth_token = resp_data["token"]; } // Handle our API's user format if (resp_data.has("user")) { Dictionary user = resp_data["user"]; user_id = user.get("id", ""); email = user.get("email", ""); username = user.get("username", email.get_slice("@", 0)); avatar_url = user.get("avatarUrl", ""); // Fallback for Supabase metadata format if (username.is_empty()) { Dictionary meta = user.get("user_metadata", Dictionary()); username = meta.get("username", email.get_slice("@", 0)); avatar_url = meta.get("avatar_url", ""); } } authenticated = true; _save_config(); Dictionary user_data; user_data["id"] = user_id; user_data["email"] = email; user_data["username"] = username; user_data["avatar_url"] = avatar_url; emit_signal("authenticated", user_data); // Fetch launcher profile fetch_launcher_profile(); return OK; } String error = response.get("error", "Authentication failed"); emit_signal("authentication_failed", error); return ERR_UNAUTHORIZED; } Error AethexLauncher::sign_up_with_email(const String &p_email, const String &p_password, const String &p_username) { Dictionary data; data["email"] = p_email; data["password"] = p_password; data["username"] = p_username; String endpoint = supabase_url.is_empty() ? "/api/v1/auth/register" : "/auth/v1/signup"; // For Supabase, put username in metadata if (!supabase_url.is_empty()) { Dictionary meta; meta["username"] = p_username; data["data"] = meta; } Dictionary response = _make_api_request(endpoint, HTTPClient::METHOD_POST, data); if (response.get("success", false)) { // Auto sign in after registration return sign_in_with_email(p_email, p_password); } String error = response.get("error", "Registration failed"); emit_signal("authentication_failed", error); return ERR_CANT_CREATE; } Error AethexLauncher::sign_in_with_oauth(const String &p_provider) { // Get OAuth URL and open in browser String url = get_oauth_url(p_provider); if (url.is_empty()) { emit_signal("authentication_failed", "Invalid OAuth provider"); return ERR_INVALID_PARAMETER; } OS::get_singleton()->shell_open(url); return OK; } String AethexLauncher::get_oauth_url(const String &p_provider) const { if (supabase_url.is_empty()) { return ""; } String redirect_uri = "aethex://auth/callback"; return supabase_url + "/auth/v1/authorize?provider=" + p_provider + "&redirect_to=" + redirect_uri; } void AethexLauncher::handle_oauth_callback(const String &p_code, const String &p_provider) { // Exchange code for token Dictionary data; data["code"] = p_code; String endpoint = "/auth/v1/token?grant_type=authorization_code"; Dictionary response = _make_api_request(endpoint, HTTPClient::METHOD_POST, data); if (response.get("success", false)) { Dictionary resp_data = response.get("data", Dictionary()); auth_token = resp_data.get("access_token", ""); if (resp_data.has("user")) { Dictionary user = resp_data["user"]; user_id = user.get("id", ""); email = user.get("email", ""); Dictionary meta = user.get("user_metadata", Dictionary()); username = meta.get("username", email.get_slice("@", 0)); avatar_url = meta.get("avatar_url", ""); } authenticated = true; _save_config(); Dictionary user_data; user_data["id"] = user_id; user_data["email"] = email; user_data["username"] = username; user_data["avatar_url"] = avatar_url; emit_signal("authenticated", user_data); fetch_launcher_profile(); } else { emit_signal("authentication_failed", response.get("error", "OAuth failed")); } } void AethexLauncher::sign_out() { auth_token = ""; user_id = ""; username = ""; email = ""; avatar_url = ""; authenticated = false; if (current_profile.is_valid()) { current_profile->clear(); } _save_config(); emit_signal("signed_out"); } bool AethexLauncher::is_authenticated() const { return authenticated; } void AethexLauncher::refresh_auth_token() { if (auth_token.is_empty()) { return; } // Verify token by fetching user Dictionary response = _make_api_request("/auth/v1/user", HTTPClient::METHOD_GET); if (!response.get("success", false)) { // Token expired, sign out sign_out(); } } // User info String AethexLauncher::get_user_id() const { return user_id; } String AethexLauncher::get_username() const { return username; } String AethexLauncher::get_email() const { return email; } String AethexLauncher::get_avatar_url() const { return avatar_url; } String AethexLauncher::get_auth_token() const { return auth_token; } // Sub-systems Ref AethexLauncher::get_game_library() const { return game_library; } Ref AethexLauncher::get_download_manager() const { return download_manager; } Ref AethexLauncher::get_store() const { return store; } Ref AethexLauncher::get_friend_system() const { return friend_system; } Ref AethexLauncher::get_current_profile() const { return current_profile; } // Launcher Profile Error AethexLauncher::fetch_launcher_profile() { if (!authenticated) { return ERR_UNAUTHORIZED; } String endpoint = "/rest/v1/launcher_profiles?user_id=eq." + user_id + "&select=*"; Dictionary response = _make_api_request(endpoint, HTTPClient::METHOD_GET); if (response.get("success", false)) { Variant data = response.get("data", Variant()); if (data.get_type() == Variant::ARRAY) { Array profiles = data; if (profiles.size() > 0) { Dictionary profile = profiles[0]; if (current_profile.is_valid()) { current_profile->from_dictionary(profile); } emit_signal("profile_updated", profile); return OK; } } } return ERR_DOES_NOT_EXIST; } Error AethexLauncher::update_launcher_profile(const Dictionary &p_data) { if (!authenticated || !current_profile.is_valid()) { return ERR_UNAUTHORIZED; } String endpoint = "/rest/v1/launcher_profiles?id=eq." + current_profile->get_id(); Dictionary response = _make_api_request(endpoint, HTTPClient::METHOD_PATCH, p_data); if (response.get("success", false)) { fetch_launcher_profile(); return OK; } return ERR_CANT_ACQUIRE_RESOURCE; } Error AethexLauncher::create_launcher_profile(const String &p_gamertag) { if (!authenticated) { return ERR_UNAUTHORIZED; } Dictionary data; data["user_id"] = user_id; data["display_name"] = p_gamertag; data["status"] = "online"; String endpoint = "/rest/v1/launcher_profiles"; Dictionary response = _make_api_request(endpoint, HTTPClient::METHOD_POST, data); if (response.get("success", false)) { Dictionary profile = response.get("data", Dictionary()); if (current_profile.is_valid()) { current_profile->from_dictionary(profile); } emit_signal("profile_created", profile); return OK; } return ERR_CANT_CREATE; } // Quick actions Error AethexLauncher::launch_game(const String &p_game_id) { if (!game_library.is_valid()) { return ERR_UNCONFIGURED; } Ref game = game_library->get_game(p_game_id); if (!game.is_valid()) { return ERR_DOES_NOT_EXIST; } String exe_path = game->get_executable_path(); if (exe_path.is_empty() || !FileAccess::exists(exe_path)) { return ERR_FILE_NOT_FOUND; } List args; String output; int exit_code; Error err = OS::get_singleton()->execute(exe_path, args, &output, &exit_code, false); if (err == OK) { game->set_last_played(Time::get_singleton()->get_datetime_string_from_system()); game_library->save_library(); emit_signal("game_launched", p_game_id); } return err; } Error AethexLauncher::install_game(const String &p_game_id) { if (!store.is_valid() || !download_manager.is_valid()) { return ERR_UNCONFIGURED; } Ref item = store->get_item(p_game_id); if (!item.is_valid()) { return ERR_DOES_NOT_EXIST; } String download_url = item->get_download_url(); if (download_url.is_empty()) { return ERR_INVALID_DATA; } String dest_path = get_downloads_directory() + "/" + p_game_id + ".zip"; download_manager->start_download(p_game_id, download_url, dest_path); return OK; } Error AethexLauncher::uninstall_game(const String &p_game_id) { if (!game_library.is_valid()) { return ERR_UNCONFIGURED; } Ref game = game_library->get_game(p_game_id); if (!game.is_valid()) { return ERR_DOES_NOT_EXIST; } String install_path = game->get_install_path(); if (install_path.is_empty()) { return ERR_FILE_NOT_FOUND; } // Remove directory Ref dir = DirAccess::open(install_path.get_base_dir()); if (dir.is_valid()) { dir->remove(install_path); } game->set_status(GameEntry::STATUS_NOT_INSTALLED); game->set_install_path(""); game_library->save_library(); emit_signal("game_uninstalled", p_game_id); return OK; } // Paths String AethexLauncher::get_games_directory() const { if (config.is_valid() && config->has_section_key("paths", "games")) { return config->get_value("paths", "games", ""); } return OS::get_singleton()->get_user_data_dir() + "/games"; } String AethexLauncher::get_downloads_directory() const { return OS::get_singleton()->get_user_data_dir() + "/downloads"; } String AethexLauncher::get_cache_directory() const { return OS::get_singleton()->get_user_data_dir() + "/cache"; } void AethexLauncher::set_games_directory(const String &p_path) { if (config.is_valid()) { config->set_value("paths", "games", p_path); _save_config(); } }