AeThex-Engine-Core/engine/modules/aethex_launcher/aethex_launcher.cpp
mrpiglr 190b6b2eab
Some checks are pending
Build AeThex Engine / build-windows (push) Waiting to run
Build AeThex Engine / build-linux (push) Waiting to run
Build AeThex Engine / build-macos (push) Waiting to run
Build AeThex Engine / create-release (push) Blocked by required conditions
Deploy Docsify Documentation / build (push) Waiting to run
Deploy Docsify Documentation / deploy (push) Blocked by required conditions
chore: sync local changes to Forgejo
2026-03-13 00:37:06 -07:00

692 lines
22 KiB
C++

/**************************************************************************/
/* 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<HTTPClient> http = Ref<HTTPClient>(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<String> 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<GameLibrary> AethexLauncher::get_game_library() const {
return game_library;
}
Ref<DownloadManager> AethexLauncher::get_download_manager() const {
return download_manager;
}
Ref<LauncherStore> AethexLauncher::get_store() const {
return store;
}
Ref<FriendSystem> AethexLauncher::get_friend_system() const {
return friend_system;
}
Ref<LauncherProfile> 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<GameEntry> 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<String> 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<StoreItem> 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<GameEntry> 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<DirAccess> 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();
}
}