chore: sync local changes to Forgejo
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

This commit is contained in:
Anderson 2026-03-13 00:37:06 -07:00
parent 7c95244e3e
commit 190b6b2eab
8179 changed files with 1384369 additions and 94 deletions

View file

@ -510,7 +510,7 @@ spec:
### REST API Standards ### REST API Standards
**Base URL:** `https://api.aethex.io/v1` **Base URL:** `https://api.aethex.dev/v1`
**Request Format:** **Request Format:**
```json ```json
@ -559,7 +559,7 @@ spec:
### WebSocket Protocol ### WebSocket Protocol
**Connection URL:** `wss://ws.aethex.io/v1/multiplayer` **Connection URL:** `wss://ws.aethex.dev/v1/multiplayer`
**Authentication:** **Authentication:**
```json ```json

View file

@ -20,7 +20,7 @@ website = "https://godotengine.org"
```python ```python
short_name = "aethex" short_name = "aethex"
name = "AeThex Engine" name = "AeThex Engine"
website = "https://aethex.io" # or your domain website = "https://aethex.dev" # or your domain
``` ```
**Impact:** Changes all version strings, window titles, splash screens **Impact:** Changes all version strings, window titles, splash screens
@ -102,7 +102,7 @@ engine/editor/editor_themes.cpp
```cpp ```cpp
// Add AeThex default settings // Add AeThex default settings
EDITOR_SETTING(Variant::STRING, PROPERTY_HINT_NONE, "network/cloud_api_url", EDITOR_SETTING(Variant::STRING, PROPERTY_HINT_NONE, "network/cloud_api_url",
"https://api.aethex.io", "") "https://api.aethex.dev", "")
EDITOR_SETTING(Variant::BOOL, PROPERTY_HINT_NONE, "aethex/enable_ai_assist", EDITOR_SETTING(Variant::BOOL, PROPERTY_HINT_NONE, "aethex/enable_ai_assist",
true, "") true, "")
EDITOR_SETTING(Variant::STRING, PROPERTY_HINT_NONE, "aethex/theme", EDITOR_SETTING(Variant::STRING, PROPERTY_HINT_NONE, "aethex/theme",

View file

@ -88,7 +88,7 @@ Download export templates for your target platform:
# Via Studio IDE: Editor → Export Templates → Download # Via Studio IDE: Editor → Export Templates → Download
# Or manually: # Or manually:
wget https://aethex.io/downloads/export-templates-[version].zip wget https://aethex.dev/downloads/export-templates-[version].zip
unzip export-templates-[version].zip -d ~/.local/share/aethex/templates/ unzip export-templates-[version].zip -d ~/.local/share/aethex/templates/
``` ```

View file

@ -0,0 +1,375 @@
# AeThex Studio Integration Plan
## Overview
This document outlines the strategy to merge the **AeThex Launcher** (Tauri/React app) with the **AeThex Engine** (Godot 4.7 fork) into a unified **AeThex Studio** experience.
---
## Current State Analysis
### AeThex Launcher Features (60+ components)
| Category | Features |
|----------|----------|
| **Core** | Game Library, Store, Download Manager, Auto-Update |
| **Social** | Friends, Activity Feed, AeThex Connect (messaging), Streaming, Replays |
| **Developer** | Gameforge, My Projects, AeThex Lang, Bot Dev IDE, Warden Security |
| **Community** | Forum, News Hub, Socials, Roadmap |
| **Economy** | Dev Exchange, Rewards Center, Gift System |
| **Account** | Auth (OAuth), Profile Hub, Cloud Saves, Achievements |
| **Settings** | Theme Engine, Sound System, Keyboard Shortcuts, Controller Mode |
| **Family** | Family Sharing, Parental Controls |
| **Testing** | Beta Testing Hub, Version Channels, Labs, Preview |
| **Modes** | Player, Creator, Corp, Foundation (different home screens) |
### AeThex Engine (Current)
- Godot 4.7 fork with custom modules
- Project Manager with new tabs (Terminal, Community, Cloud, Launcher)
- Custom modules: `aethex_cloud`, `aethex_telemetry`, `discord_rpc`
---
## Integration Options
### Option A: Embedded WebView (Recommended - Phase 1)
**Concept**: Embed the React launcher UI inside the engine using a WebView/HTML component.
**Pros**:
- Fastest to implement (weeks, not months)
- Reuses 100% of existing launcher code
- Unified window experience
- Backend/API code unchanged
**Cons**:
- Requires bundling Chromium or using system WebView
- Adds ~50MB to engine size
- Two rendering systems (Godot + Web)
**Implementation**:
```
┌─────────────────────────────────────────────────────────────┐
│ AeThex Studio Window │
├──────────────┬──────────────────────────────────────────────┤
│ │ │
│ Sidebar │ Tab: Launcher (WebView) │
│ (Native) │ ├── React App renders here │
│ │ └── Full launcher functionality │
│ - Editor │ │
│ - Projects │ Tab: Editor (Native Godot) │
│ - Launcher │ ├── Scene editor │
│ - Settings │ └── Script editor │
│ │ │
└──────────────┴──────────────────────────────────────────────┘
```
### Option B: Full Native Port (Phase 2+)
**Concept**: Rewrite all launcher features as native C++ Godot editor plugins.
**Pros**:
- Fully native, consistent UI
- No web dependencies
- Better performance
- Offline-first
**Cons**:
- 6-12 months development time
- 60+ React components → C++ translation
- Ongoing maintenance burden
**Priority Order for Native Port**:
1. Auth System (critical)
2. Game Library
3. Cloud Saves
4. Download Manager
5. Store
6. Social/Friends
7. Everything else
### Option C: Hybrid Progressive (Recommended Long-term)
**Concept**: Start with WebView, progressively replace critical features with native.
**Phase 1** (1-2 weeks): WebView integration
**Phase 2** (1 month): Native auth + game library
**Phase 3** (2 months): Native store + downloads
**Phase 4** (ongoing): Port remaining features as needed
---
## Recommended Architecture
```
AeThex Studio
├── engine/ # Godot 4.7 fork (C++)
│ ├── editor/
│ │ ├── project_manager/ # Enhanced project manager
│ │ └── launcher/ # NEW: Native launcher module
│ │ ├── auth_ui.cpp # Native auth screens
│ │ ├── game_library.cpp # Native game browser
│ │ ├── webview_panel.cpp # WebView for web features
│ │ └── launcher_api.cpp # Communication layer
│ └── modules/
│ ├── aethex_cloud/ # Cloud saves, sync
│ ├── aethex_auth/ # NEW: Native auth (Supabase)
│ └── aethex_store/ # NEW: Store API client
├── launcher-web/ # Embedded React app
│ ├── dist/ # Built React app
│ └── src/ # Source (from current launcher)
└── platform/
└── windows/
└── webview/ # Platform-specific WebView
```
---
## Phase 1 Implementation Plan (WebView)
### Step 1: Add WebView Support to Engine
```cpp
// modules/webview/webview_panel.h
class WebViewPanel : public Control {
GDCLASS(WebViewPanel, Control);
void navigate(const String &url);
void load_html(const String &html);
void execute_javascript(const String &script);
void set_communication_callback(Callable callback);
};
```
### Step 2: Create Launcher Tab in Project Manager
The launcher tab will host a WebView that loads the React app:
```cpp
// In ProjectManager
void _create_launcher_tab() {
WebViewPanel *webview = memnew(WebViewPanel);
webview->navigate("file:///launcher/index.html");
// or
webview->navigate("https://launcher.aethex.dev");
}
```
### Step 3: Bridge Communication
JavaScript ↔ C++ communication for:
- Launching games (JS calls C++ to spawn processes)
- File system access (downloads, installations)
- Native OS features (notifications, file dialogs)
```cpp
// launcher_bridge.cpp
void LauncherBridge::handle_message(const String &message) {
Dictionary data = JSON::parse_string(message);
String action = data.get("action", "");
if (action == "launch_game") {
String exe_path = data.get("path", "");
OS::get_singleton()->execute(exe_path, args);
}
else if (action == "open_editor") {
EditorNode::get_singleton()->open_request(data.get("project_path", ""));
}
}
```
### Step 4: Bundle React App
Build the React launcher and include it in the engine distribution:
```
aethex-studio/
├── bin/
│ └── aethex.windows.editor.x86_64.exe
└── launcher/
├── index.html
├── assets/
└── *.js
```
---
## Phase 2 Implementation Plan (Native Critical Features)
### Native Auth Module
```cpp
// modules/aethex_auth/aethex_auth.h
class AeThexAuth : public Object {
GDCLASS(AeThexAuth, Object);
static AeThexAuth *singleton;
String supabase_url;
String supabase_anon_key;
String current_token;
Dictionary user_profile;
public:
Error sign_in_with_email(const String &email, const String &password);
Error sign_in_with_oauth(const String &provider); // google, discord, github
Error sign_out();
Error refresh_token();
bool is_authenticated() const;
Dictionary get_user() const;
Dictionary get_launcher_profile() const;
Signal user_signed_in;
Signal user_signed_out;
Signal profile_updated;
};
```
### Native Game Library
```cpp
// modules/aethex_launcher/game_library.h
class GameLibrary : public Control {
GDCLASS(GameLibrary, Control);
struct GameEntry {
String id;
String title;
String cover_image;
String install_path;
String status; // installed, downloading, available
float download_progress;
String version;
};
Vector<GameEntry> games;
void refresh_library();
void launch_game(const String &game_id);
void install_game(const String &game_id);
void uninstall_game(const String &game_id);
void update_game(const String &game_id);
};
```
---
## Backend Integration
The launcher uses Supabase for:
| Service | Purpose |
|---------|---------|
| **Auth** | User authentication, OAuth providers |
| **Database** | Profiles, games, achievements, friends |
| **Storage** | Avatars, cloud saves, game assets |
| **Realtime** | Friend status, notifications |
| **Edge Functions** | Store transactions, webhooks |
The engine will need HTTP clients to call these APIs:
```cpp
// Example: Fetch user profile
HTTPRequest *http = memnew(HTTPRequest);
http->connect("request_completed", callable_mp(this, &AeThexAuth::_on_profile_fetched));
http->request(
supabase_url + "/rest/v1/launcher_profiles?user_id=eq." + user_id,
PackedStringArray({"apikey: " + anon_key, "Authorization: Bearer " + token})
);
```
---
## File Structure After Integration
```
AeThex-Engine-Core/
├── engine/
│ ├── editor/
│ │ ├── project_manager/
│ │ │ ├── project_manager.cpp # Has launcher tab with WebView
│ │ │ └── project_manager.h
│ │ └── launcher/ # NEW directory
│ │ ├── launcher_bridge.cpp # JS ↔ C++ bridge
│ │ ├── launcher_bridge.h
│ │ ├── native_auth_panel.cpp # Native auth UI
│ │ ├── native_auth_panel.h
│ │ └── SCsub
│ ├── modules/
│ │ ├── aethex_auth/ # NEW module
│ │ │ ├── aethex_auth.cpp
│ │ │ ├── aethex_auth.h
│ │ │ ├── config.py
│ │ │ ├── register_types.cpp
│ │ │ ├── register_types.h
│ │ │ └── SCsub
│ │ ├── aethex_cloud/ # Existing (enhanced)
│ │ ├── webview/ # NEW module
│ │ │ ├── webview_panel.cpp
│ │ │ ├── webview_panel.h
│ │ │ └── platform/
│ │ │ ├── webview_windows.cpp # WebView2 on Windows
│ │ │ ├── webview_macos.mm # WKWebView on macOS
│ │ │ └── webview_linux.cpp # WebKitGTK on Linux
│ │ └── ...
│ └── platform/
└── launcher-web/ # Bundled React app
├── build.sh
├── package.json
└── dist/ # Built output
```
---
## Timeline Estimate
| Phase | Duration | Deliverable |
|-------|----------|-------------|
| **Phase 1a** | 1 week | WebView module for Windows (WebView2) |
| **Phase 1b** | 1 week | Launcher tab integration + JS bridge |
| **Phase 1c** | 3 days | Bundle React app + build pipeline |
| **Phase 2a** | 2 weeks | Native auth module |
| **Phase 2b** | 2 weeks | Native game library |
| **Phase 2c** | 1 week | Native download manager |
| **Phase 3+** | Ongoing | Port remaining features as needed |
---
## Decision Points
### Question 1: WebView Library
- **Windows**: WebView2 (Edge/Chromium) - recommended
- **Alternative**: CEF (Chromium Embedded Framework) - more control, larger size
### Question 2: Initial Distribution
- **Option A**: Ship with embedded web app (offline-capable)
- **Option B**: Load from `https://launcher.aethex.dev` (always latest)
- **Recommended**: Option A with update check
### Question 3: Auth Flow
- **Option A**: WebView handles auth entirely
- **Option B**: Native OAuth browser popup → callback to engine
- **Recommended**: Option A for Phase 1, Option B for Phase 2
---
## Next Steps
1. **Decide on approach** (WebView vs Native-first)
2. **Set up WebView2 in engine** (if WebView approach)
3. **Build React launcher** for embedding
4. **Create JS↔C++ bridge** for native features
5. **Test end-to-end flow**
---
## Notes
- The current launcher at `A:\AeThex Launcher\AeThex-Landing-Page` has 40+ pages and 60+ components
- Full native port would be ~10,000+ lines of C++ code
- WebView approach allows immediate integration with gradual native migration
- Consider keeping WebView for "web-heavy" features (forum, store, socials) even after native port

View file

@ -143,7 +143,7 @@ Test if your Godot project works in AeThex without changes.
```bash ```bash
# 1. Download AeThex # 1. Download AeThex
wget https://aethex.io/download/aethex-linux.tar.gz wget https://aethex.dev/download/aethex-linux.tar.gz
tar -xzf aethex-linux.tar.gz tar -xzf aethex-linux.tar.gz
# 2. Open your Godot project # 2. Open your Godot project
@ -769,7 +769,7 @@ git checkout -b aethex-migration
### Q: Does AeThex collect any data? ### Q: Does AeThex collect any data?
**A:** Only if you use analytics features. See [Privacy Policy](https://aethex.io/privacy). **A:** Only if you use analytics features. See [Privacy Policy](https://aethex.dev/privacy).
--- ---
@ -782,12 +782,12 @@ git checkout -b aethex-migration
**Community:** **Community:**
- [Discord](https://discord.gg/aethex) - Real-time chat - [Discord](https://discord.gg/aethex) - Real-time chat
- [Forum](https://forum.aethex.io) - Discussions - [Forum](https://forum.aethex.dev) - Discussions
- [GitHub Issues](https://github.com/aethex/engine/issues) - Bug reports - [GitHub Issues](https://github.com/aethex/engine/issues) - Bug reports
**Support:** **Support:**
- [Email Support](mailto:support@aethex.io) - Response within 24h - [Email Support](mailto:support@aethex.dev) - Response within 24h
- [Pro Support](https://aethex.io/pro) - Priority support for paying customers - [Pro Support](https://aethex.dev/pro) - Priority support for paying customers
--- ---

View file

@ -734,7 +734,7 @@ Create a community server:
### Tools ### Tools
- **Analytics:** [AeThex Analytics](https://studio.aethex.io/analytics) - **Analytics:** [AeThex Analytics](https://studio.aethex.dev/analytics)
- **Marketing:** [Presskit()](https://dopresskit.com/) - **Marketing:** [Presskit()](https://dopresskit.com/)
- **Community:** [Discord](https://discord.com) - **Community:** [Discord](https://discord.com)
- **Email:** [Mailchimp](https://mailchimp.com) - **Email:** [Mailchimp](https://mailchimp.com)
@ -748,9 +748,9 @@ Create a community server:
### Support ### Support
- **Email:** [support@aethex.io](mailto:support@aethex.io) - **Email:** [support@aethex.dev](mailto:support@aethex.dev)
- **Discord:** [AeThex Community](https://discord.gg/aethex) - **Discord:** [AeThex Community](https://discord.gg/aethex)
- **Docs:** [docs.aethex.io](https://docs.aethex.io) - **Docs:** [docs.aethex.dev](https://docs.aethex.dev)
--- ---

View file

@ -42,7 +42,7 @@ website = "https://godotengine.org"
**To:** **To:**
```python ```python
website = "https://aethex.io" # or your domain website = "https://aethex.dev" # or your domain
``` ```
**Result:** Help → Visit Website will go to your site **Result:** Help → Visit Website will go to your site

View file

@ -256,30 +256,30 @@ See [API Reference](API_REFERENCE.md) for complete documentation.
### Official ### Official
- **Website:** [https://aethex.io](https://aethex.io) - **Website:** [https://aethex.dev](https://aethex.dev)
- **Download:** [https://aethex.io/download](https://aethex.io/download) - **Download:** [https://aethex.dev/download](https://aethex.dev/download)
- **GitHub:** [https://github.com/aethex/engine](https://github.com/aethex/engine) - **GitHub:** [https://github.com/aethex/engine](https://github.com/aethex/engine)
- **Asset Store:** [https://aethex.io/assets](https://aethex.io/assets) - **Asset Store:** [https://aethex.dev/assets](https://aethex.dev/assets)
### Community ### Community
- **Discord:** [https://discord.gg/aethex](https://discord.gg/aethex) - **Discord:** [https://discord.gg/aethex](https://discord.gg/aethex)
- **Forum:** [https://forum.aethex.io](https://forum.aethex.io) - **Forum:** [https://forum.aethex.dev](https://forum.aethex.dev)
- **Reddit:** [https://reddit.com/r/aethex](https://reddit.com/r/aethex) - **Reddit:** [https://reddit.com/r/aethex](https://reddit.com/r/aethex)
- **Twitter:** [@AeThexEngine](https://twitter.com/AeThexEngine) - **Twitter:** [@AeThexEngine](https://twitter.com/AeThexEngine)
### Learning ### Learning
- **YouTube:** [AeThex Tutorials](https://youtube.com/@aethex) - **YouTube:** [AeThex Tutorials](https://youtube.com/@aethex)
- **Blog:** [https://aethex.io/blog](https://aethex.io/blog) - **Blog:** [https://aethex.dev/blog](https://aethex.dev/blog)
- **Examples:** [https://github.com/aethex/examples](https://github.com/aethex/examples) - **Examples:** [https://github.com/aethex/examples](https://github.com/aethex/examples)
### Support ### Support
- **Documentation:** You're here! - **Documentation:** You're here!
- **FAQ:** [https://aethex.io/faq](https://aethex.io/faq) - **FAQ:** [https://aethex.dev/faq](https://aethex.dev/faq)
- **Email:** [support@aethex.io](mailto:support@aethex.io) - **Email:** [support@aethex.dev](mailto:support@aethex.dev)
- **Pro Support:** [https://aethex.io/pro](https://aethex.io/pro) - **Pro Support:** [https://aethex.dev/pro](https://aethex.dev/pro)
--- ---
@ -345,7 +345,7 @@ Want to improve the docs?
1. **Search:** Use Ctrl+F in this index or search GitHub 1. **Search:** Use Ctrl+F in this index or search GitHub
2. **Ask Community:** Discord is very active 2. **Ask Community:** Discord is very active
3. **Check Examples:** [GitHub examples repo](https://github.com/aethex/examples) 3. **Check Examples:** [GitHub examples repo](https://github.com/aethex/examples)
4. **Contact Support:** [support@aethex.io](mailto:support@aethex.io) 4. **Contact Support:** [support@aethex.dev](mailto:support@aethex.dev)
**Found a bug in documentation?** **Found a bug in documentation?**
- Report on [GitHub Issues](https://github.com/aethex/engine/issues) - Report on [GitHub Issues](https://github.com/aethex/engine/issues)
@ -380,7 +380,7 @@ Want to improve the docs?
2. Read [Cloud Services Architecture](CLOUD_SERVICES_ARCHITECTURE.md) (30 min) 2. Read [Cloud Services Architecture](CLOUD_SERVICES_ARCHITECTURE.md) (30 min)
3. Study [Studio Integration](STUDIO_INTEGRATION.md) (20 min) 3. Study [Studio Integration](STUDIO_INTEGRATION.md) (20 min)
4. Plan deployment strategy (varies) 4. Plan deployment strategy (varies)
5. Contact [sales@aethex.io](mailto:sales@aethex.io) for enterprise support 5. Contact [sales@aethex.dev](mailto:sales@aethex.dev) for enterprise support
**Total time:** ~2 hours + implementation **Total time:** ~2 hours + implementation

View file

@ -0,0 +1,263 @@
# AeThex Unified Architecture
**Everything in one place, connected.**
## Overview
The AeThex platform is a unified game engine + launcher that lets users:
- Build games with the AeThex Engine (Godot fork)
- Discover and purchase games in the Store
- Manage their game library
- Connect with friends
## Architecture
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ AeThex Desktop Application │
│ (aethex.windows.editor.exe) │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Project Manager / Launcher ││
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││
│ │ │ Home │ │ Library │ │ Store │ │ Downloads│ │ Friends │ ││
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Game Editor ││
│ │ (Opens when editing a project) ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘
│ HTTPS API
┌─────────────────────────────────────────────────────────────────────────────┐
│ AeThex Cloud Services │
│ (api.aethex.dev) │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ ││
│ │ Auth Service │ │ Games Service │ │ Social Service │ ││
│ │ /api/v1/auth │ │ /api/v1/games │ │ /api/v1/users │ ││
│ └────────────────┘ └────────────────┘ └────────────────┘ ││
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ PostgreSQL │ │
│ └────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Components
### 1. Desktop Application (C++)
**Location:** `engine/`
The AeThex Desktop Application is a custom Godot 4.7 fork that includes:
- **Game Editor** - Full Godot editor for building games
- **Project Manager** - Manages Godot projects
- **Launcher Module** - Custom module for game library, store, and social features
The launcher module is located at:
```
engine/modules/aethex_launcher/
├── aethex_launcher.cpp/h - Main singleton, handles auth + API
├── data/
│ ├── game_library.cpp/h - User's game library
│ ├── launcher_store.cpp/h - Store catalog
│ ├── friend_system.cpp/h - Friends/social
│ ├── download_manager.cpp/h - Downloads
│ └── launcher_profile.cpp/h - User profile
├── ui/
│ ├── launcher_panel.cpp/h - Main UI with sidebar
│ ├── library_panel.cpp/h - Game library grid
│ ├── store_panel.cpp/h - Store browser
│ ├── friends_panel.cpp/h - Friends list
│ ├── downloads_panel.cpp/h - Download queue
│ ├── profile_panel.cpp/h - User profile
│ └── auth_panel.cpp/h - Login/signup
└── editor/
└── launcher_editor_plugin.cpp - Editor integration
```
### 2. Backend API (TypeScript/Node.js)
**Location:** `services/auth-service/`
The backend runs at `api.aethex.dev` (locally at `localhost:3001`):
```
services/auth-service/
├── src/
│ ├── index.ts - Express server
│ ├── controllers/
│ │ ├── authController.ts - Auth logic
│ │ ├── userController.ts - User/profile logic
│ │ └── gamesController.ts - Games/store logic
│ ├── routes/
│ │ ├── auth.ts - Auth endpoints
│ │ ├── user.ts - User endpoints
│ │ └── games.ts - Games endpoints
│ ├── middleware/
│ │ ├── authenticateToken.ts - JWT verification
│ │ ├── validateRequest.ts - Input validation
│ │ └── errorHandler.ts - Error handling
│ └── utils/
│ └── logger.ts - Logging
├── prisma/
│ └── schema.prisma - Database schema
├── package.json
└── docker-compose.yml - PostgreSQL container
```
### 3. Database (PostgreSQL)
**Schema:**
- **users** - Account info
- **sessions** - Auth sessions
- **launcher_profiles** - Gamertag, level, playtime
- **games** - Store catalog
- **user_games** - Owned games
- **friends** - Friend relationships
## API Endpoints
### Authentication
```
POST /api/v1/auth/register - Create account
POST /api/v1/auth/login - Login
POST /api/v1/auth/refresh - Refresh token
POST /api/v1/auth/logout - Logout
GET /api/v1/auth/me - Get current user
```
### Games & Store
```
GET /api/v1/games - List all games
GET /api/v1/games/featured - Get featured games
GET /api/v1/games/:slug - Get game details
POST /api/v1/games/:id/purchase - Add to library
```
### Users
```
GET /api/v1/users/profile - Get profile
PATCH /api/v1/users/profile - Update profile
GET /api/v1/users/library - User's games
GET /api/v1/users/friends - Friends list
POST /api/v1/users/friends - Send friend request
```
## Data Flow
### 1. Login
```
User enters email/password
→ C++ auth_panel.cpp calls AethexLauncher::sign_in_with_email()
→ Makes POST to /api/v1/auth/login
→ Backend validates credentials
→ Returns JWT token + user data
→ C++ stores token, emits "authenticated" signal
→ UI updates to show logged in state
```
### 2. Library
```
LibraryPanel opens
→ Calls GameLibrary::load_library()
→ Loads from local cache (library.json)
→ If empty, populates demo games
→ If online, calls refresh_from_server()
→ Creates GameCard for each game
```
### 3. Store
```
StorePanel opens
→ Calls LauncherStore::fetch_featured()
→ Makes GET to /api/v1/games/featured
→ Backend returns game list
→ UI populates store cards
User clicks "Add to Cart"
→ Local cart state updated
→ Checkout calls /api/v1/games/:id/purchase
→ Game added to user's library
```
## Development Setup
### Quick Start
```powershell
# Start everything
.\start-dev.ps1
```
### Manual Setup
```powershell
# 1. Start database (requires Docker)
cd services/auth-service
docker-compose up -d postgres
# 2. Setup backend
npm install
npx prisma generate
npx prisma migrate dev --name init
npm run seed
npm run dev # Runs on localhost:3001
# 3. Build engine
cd engine
scons platform=windows target=editor -j8
# 4. Run
.\bin\aethex.windows.editor.x86_64.exe
```
### Demo Account
- **Email:** demo@aethex.dev
- **Password:** DemoPass123
## Offline Mode
The launcher works offline with demo data:
- **Library:** Has 4 demo games (AeThex Adventure, Neon Racer, etc.)
- **Store:** Shows 6 demo items with prices
- **Profile:** Works in guest mode
When online, data syncs with the backend.
## Configuration
The launcher stores config at:
- **Windows:** `%APPDATA%/Godot/aethex_launcher.cfg`
- **macOS:** `~/Library/Application Support/Godot/aethex_launcher.cfg`
- **Linux:** `~/.config/godot/aethex_launcher.cfg`
Config includes:
- API base URL
- Auth tokens
- User preferences
## Domain
All services use `aethex.dev`:
- **Website:** https://aethex.dev
- **API:** https://api.aethex.dev
- **Docs:** https://docs.aethex.dev
## Summary
Everything connects in ONE flow:
```
AeThex Launcher (C++)
┌──────────┼──────────┐
▼ ▼ ▼
Library Store Friends
\ │ /
\ │ /
v v v
api.aethex.dev (Node.js)
v
PostgreSQL
```
**One executable. One backend. Everything connected.**

View file

@ -528,7 +528,7 @@ func _on_button_pressed():
View your analytics data: View your analytics data:
1. **Go to:** [https://studio.aethex.io/analytics](https://studio.aethex.io/analytics) 1. **Go to:** [https://studio.aethex.dev/analytics](https://studio.aethex.dev/analytics)
2. **Select your project** 2. **Select your project**
3. **View dashboards:** 3. **View dashboards:**
- Overview: DAU, MAU, retention - Overview: DAU, MAU, retention
@ -758,7 +758,7 @@ func test_analytics():
- **[Publishing Guide](../PUBLISHING_GUIDE.md)** - Deploy your game with analytics - **[Publishing Guide](../PUBLISHING_GUIDE.md)** - Deploy your game with analytics
- **[API Reference](../API_REFERENCE.md#aethexanalytics-singleton)** - Complete analytics API - **[API Reference](../API_REFERENCE.md#aethexanalytics-singleton)** - Complete analytics API
- **Dashboard:** View your data at [studio.aethex.io/analytics](https://studio.aethex.io/analytics) - **Dashboard:** View your data at [studio.aethex.dev/analytics](https://studio.aethex.dev/analytics)
--- ---

View file

@ -284,7 +284,7 @@ Want to write a tutorial? See [CONTRIBUTING.md](../../engine/CONTRIBUTING.md) fo
- Check the [API Reference](../API_REFERENCE.md) - Check the [API Reference](../API_REFERENCE.md)
- Review [GDScript Basics](../GDSCRIPT_BASICS.md) - Review [GDScript Basics](../GDSCRIPT_BASICS.md)
- Ask in [Discord](https://discord.gg/aethex) - Ask in [Discord](https://discord.gg/aethex)
- Search [docs.aethex.io](https://docs.aethex.io) - Search [docs.aethex.dev](https://docs.aethex.dev)
**Found a bug in tutorial?** **Found a bug in tutorial?**
- Report on [GitHub Issues](https://github.com/aethex/engine/issues) - Report on [GitHub Issues](https://github.com/aethex/engine/issues)
@ -298,6 +298,6 @@ After completing tutorials:
- Read [Architecture Overview](../ARCHITECTURE_OVERVIEW.md) *(coming soon)* - Read [Architecture Overview](../ARCHITECTURE_OVERVIEW.md) *(coming soon)*
- Explore [Advanced Topics](../ADVANCED_TOPICS.md) *(coming soon)* - Explore [Advanced Topics](../ADVANCED_TOPICS.md) *(coming soon)*
- Build your own game! - Build your own game!
- Share in the [Community Showcase](https://aethex.io/showcase) - Share in the [Community Showcase](https://aethex.dev/showcase)
Happy building! 🚀 Happy building! 🚀

BIN
engine/build_output.txt Normal file

Binary file not shown.

View file

@ -56,6 +56,7 @@
#include "main/main.h" #include "main/main.h"
#include "scene/gui/check_box.h" #include "scene/gui/check_box.h"
#include "scene/gui/flow_container.h" #include "scene/gui/flow_container.h"
#include "scene/gui/item_list.h"
#include "scene/gui/line_edit.h" #include "scene/gui/line_edit.h"
#include "scene/gui/margin_container.h" #include "scene/gui/margin_container.h"
#include "scene/gui/menu_bar.h" #include "scene/gui/menu_bar.h"
@ -1315,6 +1316,107 @@ void ProjectManager::_open_donate_page() {
OS::get_singleton()->shell_open("https://fund.godotengine.org/?ref=project_manager"); OS::get_singleton()->shell_open("https://fund.godotengine.org/?ref=project_manager");
} }
// ===== AETHEX STUDIO FUNCTIONS =====
void ProjectManager::_terminal_command_submitted(const String &p_command) {
if (p_command.strip_edges().is_empty()) {
return;
}
terminal_input->clear();
terminal_output->append_text("[color=#00aaff]> " + p_command + "[/color]\n");
String cmd_lower = p_command.strip_edges().to_lower();
if (cmd_lower == "help") {
terminal_output->append_text("[color=#ffff88]Available Commands:[/color]\n");
terminal_output->append_text(" help - Show this help\n");
terminal_output->append_text(" clear - Clear terminal\n");
terminal_output->append_text(" version - Show AeThex version\n");
terminal_output->append_text(" projects - List project directories\n");
terminal_output->append_text(" [cmd] - Execute system command\n\n");
} else if (cmd_lower == "clear" || cmd_lower == "cls") {
_terminal_clear();
} else if (cmd_lower == "version") {
terminal_output->append_text("AeThex Engine " + String(AETHEX_VERSION_NAME) + "\n");
terminal_output->append_text("Based on Godot Engine\n\n");
} else if (cmd_lower == "projects") {
terminal_output->append_text("[color=#00ff88]Scanning A:\\ for AeThex projects...[/color]\n");
Ref<DirAccess> dir = DirAccess::open("A:/");
if (dir.is_valid()) {
dir->list_dir_begin();
String folder = dir->get_next();
while (!folder.is_empty()) {
if (dir->current_is_dir() && !folder.begins_with(".")) {
if (folder.to_lower().contains("aethex") || folder.to_lower().contains("trinity")) {
terminal_output->append_text(" [color=#88ff88]" + folder + "[/color]\n");
}
}
folder = dir->get_next();
}
}
terminal_output->append_text("\n");
} else {
// Execute system command
String output;
int exit_code = 0;
List<String> args;
args.push_back("-Command");
args.push_back(p_command);
OS::get_singleton()->execute("powershell", args, &output, &exit_code, true);
if (!output.is_empty()) {
terminal_output->append_text(output);
}
if (exit_code != 0) {
terminal_output->append_text("[color=#ff6666]Exit code: " + itos(exit_code) + "[/color]\n");
}
terminal_output->append_text("\n");
}
}
void ProjectManager::_terminal_clear() {
terminal_output->clear();
terminal_output->append_text("[color=#00ff88]AeThex Terminal v1.0[/color]\n");
terminal_output->append_text("[color=#666666]Terminal cleared.[/color]\n\n");
}
void ProjectManager::_open_forums() {
OS::get_singleton()->shell_open("https://aethex.dev/community");
}
void ProjectManager::_open_discord() {
OS::get_singleton()->shell_open("https://discord.gg/aethex");
}
void ProjectManager::_open_github() {
OS::get_singleton()->shell_open("https://github.com/AeThex-LABS");
}
void ProjectManager::_sync_cloud() {
// TODO: Implement actual Supabase sync
print_line("[AeThex] Syncing to cloud...");
}
void ProjectManager::_open_supabase() {
OS::get_singleton()->shell_open("https://supabase.com/dashboard/project/kmdeisowhtsalsekkzqd");
}
void ProjectManager::_launch_launcher() {
List<String> args;
OS::get_singleton()->create_process("A:/AeThex Launcher/aethex-launcher.exe", args);
}
void ProjectManager::_open_launcher_folder() {
OS::get_singleton()->shell_open("A:/AeThex Launcher");
}
void ProjectManager::_open_new_powershell() {
List<String> args;
OS::get_singleton()->create_process("powershell", args);
}
// Object methods. // Object methods.
ProjectManager::ProjectManager() { ProjectManager::ProjectManager() {
@ -1507,6 +1609,31 @@ ProjectManager::ProjectManager() {
main_view_container->set_v_size_flags(Control::SIZE_EXPAND_FILL); main_view_container->set_v_size_flags(Control::SIZE_EXPAND_FILL);
main_vbox->add_child(main_view_container); main_vbox->add_child(main_view_container);
// AeThex Launcher view - FIRST TAB!
#ifdef MODULE_AETHEX_LAUNCHER_ENABLED
{
launcher_panel = memnew(LauncherPanel);
launcher_panel->set_name("AeThexTab");
_add_main_view(MAIN_VIEW_LAUNCHER, TTRC("AeThex"), Ref<Texture2D>(), launcher_panel);
// Set up the launcher singleton connection
AethexLauncher *launcher = AethexLauncher::get_singleton();
if (launcher) {
launcher_panel->set_launcher(launcher);
}
}
#else
{
launcher_vb = memnew(VBoxContainer);
launcher_vb->set_name("LauncherTab");
_add_main_view(MAIN_VIEW_LAUNCHER, TTRC("Launcher"), Ref<Texture2D>(), launcher_vb);
Label *launcher_title = memnew(Label);
launcher_title->set_text(TTRC("AeThex Launcher module not enabled"));
launcher_vb->add_child(launcher_title);
}
#endif
// Project list view. // Project list view.
{ {
local_projects_vb = memnew(VBoxContainer); local_projects_vb = memnew(VBoxContainer);
@ -1735,6 +1862,165 @@ ProjectManager::ProjectManager() {
asset_library_toggle->set_tooltip_text(TTRC("Asset Library not available (due to using Web editor, or because SSL support disabled).")); asset_library_toggle->set_tooltip_text(TTRC("Asset Library not available (due to using Web editor, or because SSL support disabled)."));
} }
// ===== AETHEX STUDIO TABS =====
// Terminal view.
{
terminal_vb = memnew(VBoxContainer);
terminal_vb->set_name("TerminalTab");
_add_main_view(MAIN_VIEW_TERMINAL, TTRC("Terminal"), Ref<Texture2D>(), terminal_vb);
// Terminal header
HBoxContainer *term_header = memnew(HBoxContainer);
terminal_vb->add_child(term_header);
Label *term_title = memnew(Label);
term_title->set_text(TTRC("AeThex Terminal"));
term_header->add_child(term_title);
Control *term_spacer = memnew(Control);
term_spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);
term_header->add_child(term_spacer);
Button *clear_btn = memnew(Button);
clear_btn->set_text(TTRC("Clear"));
clear_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_terminal_clear));
term_header->add_child(clear_btn);
Button *new_term_btn = memnew(Button);
new_term_btn->set_text(TTRC("New PowerShell"));
new_term_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_open_new_powershell));
term_header->add_child(new_term_btn);
// Terminal output
ScrollContainer *term_scroll = memnew(ScrollContainer);
term_scroll->set_v_size_flags(Control::SIZE_EXPAND_FILL);
terminal_vb->add_child(term_scroll);
terminal_output = memnew(RichTextLabel);
terminal_output->set_use_bbcode(true);
terminal_output->set_scroll_follow(true);
terminal_output->set_selection_enabled(true);
terminal_output->set_h_size_flags(Control::SIZE_EXPAND_FILL);
terminal_output->set_v_size_flags(Control::SIZE_EXPAND_FILL);
terminal_output->append_text("[color=#00ff88]AeThex Terminal v1.0[/color]\n");
terminal_output->append_text("[color=#666666]Type commands below. Use 'help' for built-in commands.[/color]\n\n");
term_scroll->add_child(terminal_output);
// Terminal input
HBoxContainer *term_input_box = memnew(HBoxContainer);
terminal_vb->add_child(term_input_box);
Label *prompt = memnew(Label);
prompt->set_text(">");
term_input_box->add_child(prompt);
terminal_input = memnew(LineEdit);
terminal_input->set_h_size_flags(Control::SIZE_EXPAND_FILL);
terminal_input->set_placeholder(TTRC("Enter command..."));
terminal_input->connect("text_submitted", callable_mp(this, &ProjectManager::_terminal_command_submitted));
term_input_box->add_child(terminal_input);
}
// Community view.
{
community_vb = memnew(VBoxContainer);
community_vb->set_name("CommunityTab");
_add_main_view(MAIN_VIEW_COMMUNITY, TTRC("Community"), Ref<Texture2D>(), community_vb);
Label *comm_title = memnew(Label);
comm_title->set_text(TTRC("AeThex Community Hub"));
community_vb->add_child(comm_title);
HBoxContainer *comm_buttons = memnew(HBoxContainer);
community_vb->add_child(comm_buttons);
Button *forums_btn = memnew(Button);
forums_btn->set_text(TTRC("Forums"));
forums_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_open_forums));
comm_buttons->add_child(forums_btn);
Button *discord_btn = memnew(Button);
discord_btn->set_text(TTRC("Discord"));
discord_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_open_discord));
comm_buttons->add_child(discord_btn);
Button *github_btn = memnew(Button);
github_btn->set_text(TTRC("GitHub"));
github_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_open_github));
comm_buttons->add_child(github_btn);
HSeparator *sep = memnew(HSeparator);
community_vb->add_child(sep);
Label *activity_label = memnew(Label);
activity_label->set_text(TTRC("Recent Activity"));
community_vb->add_child(activity_label);
RichTextLabel *activity_feed = memnew(RichTextLabel);
activity_feed->set_use_bbcode(true);
activity_feed->set_v_size_flags(Control::SIZE_EXPAND_FILL);
activity_feed->append_text("[color=#ffff88]Loading activity feed from Supabase...[/color]\n\n");
activity_feed->append_text("• [color=#00ff88]NeonDev99[/color] completed Neon Runner\n");
activity_feed->append_text("• [color=#00aaff]mrpiglr[/color] unlocked 'First Steps' achievement\n");
activity_feed->append_text("• [color=#ff88ff]GameMaster42[/color] published a new project\n");
activity_feed->append_text("• [color=#ffaa00]CircuitPro[/color] rated Circuit Logic 5 stars\n");
community_vb->add_child(activity_feed);
}
// Cloud view.
{
cloud_vb = memnew(VBoxContainer);
cloud_vb->set_name("CloudTab");
_add_main_view(MAIN_VIEW_CLOUD, TTRC("Cloud"), Ref<Texture2D>(), cloud_vb);
Label *cloud_title = memnew(Label);
cloud_title->set_text(TTRC("AeThex Cloud Services"));
cloud_vb->add_child(cloud_title);
HBoxContainer *cloud_status = memnew(HBoxContainer);
cloud_vb->add_child(cloud_status);
Label *status_label = memnew(Label);
status_label->set_text(TTRC("Status: Connected to Supabase"));
cloud_status->add_child(status_label);
HBoxContainer *cloud_buttons = memnew(HBoxContainer);
cloud_vb->add_child(cloud_buttons);
Button *sync_btn = memnew(Button);
sync_btn->set_text(TTRC("Sync All"));
sync_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_sync_cloud));
cloud_buttons->add_child(sync_btn);
Button *backup_btn = memnew(Button);
backup_btn->set_text(TTRC("Backup"));
cloud_buttons->add_child(backup_btn);
Button *restore_btn = memnew(Button);
restore_btn->set_text(TTRC("Restore"));
cloud_buttons->add_child(restore_btn);
Button *supabase_btn = memnew(Button);
supabase_btn->set_text(TTRC("Open Supabase Dashboard"));
supabase_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_open_supabase));
cloud_buttons->add_child(supabase_btn);
HSeparator *sep = memnew(HSeparator);
cloud_vb->add_child(sep);
Label *saves_label = memnew(Label);
saves_label->set_text(TTRC("Cloud Saves"));
cloud_vb->add_child(saves_label);
ItemList *saves_list = memnew(ItemList);
saves_list->set_v_size_flags(Control::SIZE_EXPAND_FILL);
saves_list->add_item(TTRC("Circuit Logic - Level 5"));
saves_list->add_item(TTRC("Neon Runner - High Score: 15420"));
saves_list->add_item(TTRC("Void Explorer - Station Alpha"));
cloud_vb->add_child(saves_list);
}
// Footer bar. // Footer bar.
{ {
HBoxContainer *footer_bar = memnew(HBoxContainer); HBoxContainer *footer_bar = memnew(HBoxContainer);

View file

@ -30,9 +30,15 @@
#pragma once #pragma once
#include "modules/modules_enabled.gen.h" // For aethex_launcher module detection
#include "scene/gui/dialogs.h" #include "scene/gui/dialogs.h"
#include "scene/gui/scroll_container.h" #include "scene/gui/scroll_container.h"
#ifdef MODULE_AETHEX_LAUNCHER_ENABLED
#include "modules/aethex_launcher/aethex_launcher.h"
#include "modules/aethex_launcher/ui/launcher_panel.h"
#endif
class CheckBox; class CheckBox;
class EditorAbout; class EditorAbout;
class EditorAssetLibrary; class EditorAssetLibrary;
@ -95,12 +101,16 @@ class ProjectManager : public Control {
Button *quick_settings_button = nullptr; Button *quick_settings_button = nullptr;
enum MainViewTab { enum MainViewTab {
MAIN_VIEW_LAUNCHER, // AeThex tab - first!
MAIN_VIEW_PROJECTS, MAIN_VIEW_PROJECTS,
MAIN_VIEW_ASSETLIB, MAIN_VIEW_ASSETLIB,
MAIN_VIEW_TERMINAL,
MAIN_VIEW_COMMUNITY,
MAIN_VIEW_CLOUD,
MAIN_VIEW_MAX MAIN_VIEW_MAX
}; };
MainViewTab current_main_view = MAIN_VIEW_PROJECTS; MainViewTab current_main_view = MAIN_VIEW_LAUNCHER;
HashMap<MainViewTab, Control *> main_view_map; HashMap<MainViewTab, Control *> main_view_map;
HashMap<MainViewTab, Button *> main_view_toggle_map; HashMap<MainViewTab, Button *> main_view_toggle_map;
@ -114,6 +124,36 @@ class ProjectManager : public Control {
VBoxContainer *local_projects_vb = nullptr; VBoxContainer *local_projects_vb = nullptr;
EditorAssetLibrary *asset_library = nullptr; EditorAssetLibrary *asset_library = nullptr;
// AeThex Studio tabs
VBoxContainer *terminal_vb = nullptr;
VBoxContainer *community_vb = nullptr;
VBoxContainer *cloud_vb = nullptr;
#ifdef MODULE_AETHEX_LAUNCHER_ENABLED
LauncherPanel *launcher_panel = nullptr;
#else
VBoxContainer *launcher_vb = nullptr;
#endif
// Terminal tab components
RichTextLabel *terminal_output = nullptr;
LineEdit *terminal_input = nullptr;
void _terminal_command_submitted(const String &p_command);
void _terminal_clear();
// Community tab components
void _open_forums();
void _open_discord();
void _open_github();
// Cloud tab components
void _sync_cloud();
void _open_supabase();
// Launcher tab components
void _launch_launcher();
void _open_launcher_folder();
void _open_new_powershell();
EditorAbout *about_dialog = nullptr; EditorAbout *about_dialog = nullptr;
void _show_about(); void _show_about();

View file

@ -3,7 +3,7 @@
/**************************************************************************/ /**************************************************************************/
/* This file is part of: */ /* This file is part of: */
/* AETHEX ENGINE */ /* AETHEX ENGINE */
/* https://aethex.io */ /* https://aethex.dev */
/**************************************************************************/ /**************************************************************************/
#ifndef AI_ASSISTANT_H #ifndef AI_ASSISTANT_H

View file

@ -3,7 +3,7 @@
/**************************************************************************/ /**************************************************************************/
/* This file is part of: */ /* This file is part of: */
/* AETHEX ENGINE */ /* AETHEX ENGINE */
/* https://aethex.io */ /* https://aethex.dev */
/**************************************************************************/ /**************************************************************************/
/* Copyright (c) 2026-present AeThex Labs. */ /* Copyright (c) 2026-present AeThex Labs. */
/**************************************************************************/ /**************************************************************************/

View file

@ -3,7 +3,7 @@
/**************************************************************************/ /**************************************************************************/
/* This file is part of: */ /* This file is part of: */
/* AETHEX ENGINE */ /* AETHEX ENGINE */
/* https://aethex.io */ /* https://aethex.dev */
/**************************************************************************/ /**************************************************************************/
/* Copyright (c) 2026-present AeThex Labs. */ /* Copyright (c) 2026-present AeThex Labs. */
/* */ /* */

View file

@ -32,7 +32,7 @@ void AethexCloud::_bind_methods() {
ClassDB::bind_method(D_METHOD("connect_to_gateway"), &AethexCloud::connect_to_gateway); 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("disconnect_from_gateway"), &AethexCloud::disconnect_from_gateway);
ClassDB::bind_method(D_METHOD("get_status"), &AethexCloud::get_status); ClassDB::bind_method(D_METHOD("get_status"), &AethexCloud::get_status);
ClassDB::bind_method(D_METHOD("is_connected"), &AethexCloud::is_connected); ClassDB::bind_method(D_METHOD("is_cloud_connected"), &AethexCloud::is_cloud_connected);
// Auth token // Auth token
ClassDB::bind_method(D_METHOD("set_auth_token", "token"), &AethexCloud::set_auth_token); ClassDB::bind_method(D_METHOD("set_auth_token", "token"), &AethexCloud::set_auth_token);
@ -92,8 +92,8 @@ AethexCloud::AethexCloud() {
asset_library->set_cloud(this); asset_library->set_cloud(this);
telemetry->set_cloud(this); telemetry->set_cloud(this);
// Initialize WebSocket // Initialize WebSocket using factory method (abstract class)
websocket.instantiate(); websocket = Ref<WebSocketPeer>(WebSocketPeer::create());
} }
AethexCloud::~AethexCloud() { AethexCloud::~AethexCloud() {
@ -158,7 +158,7 @@ AethexCloud::ConnectionStatus AethexCloud::get_status() const {
return status; return status;
} }
bool AethexCloud::is_connected() const { bool AethexCloud::is_cloud_connected() const {
return status == STATUS_CONNECTED; return status == STATUS_CONNECTED;
} }
@ -182,8 +182,12 @@ Dictionary AethexCloud::make_request(const String &p_endpoint, HTTPClient::Metho
Dictionary result; Dictionary result;
result["success"] = false; result["success"] = false;
Ref<HTTPClient> http; // Use factory method for abstract HTTPClient class
http.instantiate(); Ref<HTTPClient> http = Ref<HTTPClient>(HTTPClient::create());
if (http.is_null()) {
result["error"] = "Failed to create HTTP client";
return result;
}
// Parse gateway URL // Parse gateway URL
String host = gateway_url.replace("https://", "").replace("http://", ""); String host = gateway_url.replace("https://", "").replace("http://", "");
@ -212,7 +216,7 @@ Dictionary AethexCloud::make_request(const String &p_endpoint, HTTPClient::Metho
// Prepare headers // Prepare headers
Vector<String> headers; Vector<String> headers;
headers.push_back("Content-Type: application/json"); headers.push_back("Content-Type: application/json");
headers.push_back("X-Engine-Version: " + String(VERSION_FULL_CONFIG)); headers.push_back("X-Engine-Version: " + String(AETHEX_VERSION_FULL_CONFIG));
if (!auth_token.is_empty()) { if (!auth_token.is_empty()) {
headers.push_back("Authorization: Bearer " + auth_token); headers.push_back("Authorization: Bearer " + auth_token);
@ -227,8 +231,9 @@ Dictionary AethexCloud::make_request(const String &p_endpoint, HTTPClient::Metho
body_str = JSON::stringify(p_data); body_str = JSON::stringify(p_data);
} }
// Make request // Make request - convert String body to bytes for the API
err = http->request(p_method, p_endpoint, headers, body_str); CharString body_utf8 = body_str.utf8();
err = http->request(p_method, p_endpoint, headers, (const uint8_t *)body_utf8.get_data(), body_utf8.length());
if (err != OK) { if (err != OK) {
result["error"] = "Request failed"; result["error"] = "Request failed";
return result; return result;
@ -302,7 +307,7 @@ void AethexCloud::_on_websocket_message(const PackedByteArray &p_data) {
Dictionary auth_msg; Dictionary auth_msg;
auth_msg["type"] = "auth"; auth_msg["type"] = "auth";
auth_msg["token"] = auth_token; auth_msg["token"] = auth_token;
auth_msg["engineVersion"] = VERSION_FULL_CONFIG; auth_msg["engineVersion"] = AETHEX_VERSION_FULL_CONFIG;
websocket->send_text(JSON::stringify(auth_msg)); websocket->send_text(JSON::stringify(auth_msg));
} }
} else if (type == "auth_success") { } else if (type == "auth_success") {
@ -327,7 +332,8 @@ void AethexCloud::process(double p_delta) {
switch (ws_state) { switch (ws_state) {
case WebSocketPeer::STATE_OPEN: { case WebSocketPeer::STATE_OPEN: {
while (websocket->get_available_packet_count() > 0) { while (websocket->get_available_packet_count() > 0) {
PackedByteArray packet = websocket->get_packet(); Vector<uint8_t> packet;
websocket->get_packet_buffer(packet);
_on_websocket_message(packet); _on_websocket_message(packet);
} }
} break; } break;

View file

@ -66,7 +66,7 @@ public:
Error connect_to_gateway(); Error connect_to_gateway();
void disconnect_from_gateway(); void disconnect_from_gateway();
ConnectionStatus get_status() const; ConnectionStatus get_status() const;
bool is_connected() const; bool is_cloud_connected() const;
// Auth token (set after login) // Auth token (set after login)
void set_auth_token(const String &p_token); void set_auth_token(const String &p_token);

View file

@ -9,6 +9,7 @@
#include "aethex_cloud.h" #include "aethex_cloud.h"
#include "core/io/http_client.h" #include "core/io/http_client.h"
#include "core/os/os.h" #include "core/os/os.h"
#include "core/os/time.h"
#include "core/version.h" #include "core/version.h"
void AethexTelemetry::_bind_methods() { void AethexTelemetry::_bind_methods() {
@ -57,7 +58,7 @@ void AethexTelemetry::track_event(const String &p_event_name, const Dictionary &
event["properties"] = p_properties; event["properties"] = p_properties;
event["timestamp"] = Time::get_singleton()->get_datetime_string_from_system(true); event["timestamp"] = Time::get_singleton()->get_datetime_string_from_system(true);
event["platform"] = OS::get_singleton()->get_name(); event["platform"] = OS::get_singleton()->get_name();
event["engineVersion"] = VERSION_FULL_CONFIG; event["engineVersion"] = AETHEX_VERSION_FULL_CONFIG;
event_buffer.push_back(event); event_buffer.push_back(event);
@ -90,7 +91,7 @@ void AethexTelemetry::report_crash(const String &p_message, const String &p_stac
data["stackTrace"] = p_stack_trace; data["stackTrace"] = p_stack_trace;
data["context"] = p_context; data["context"] = p_context;
data["platform"] = OS::get_singleton()->get_name(); data["platform"] = OS::get_singleton()->get_name();
data["engineVersion"] = VERSION_FULL_CONFIG; data["engineVersion"] = AETHEX_VERSION_FULL_CONFIG;
data["timestamp"] = Time::get_singleton()->get_datetime_string_from_system(true); data["timestamp"] = Time::get_singleton()->get_datetime_string_from_system(true);
// Crash reports are sent immediately, not buffered // Crash reports are sent immediately, not buffered

View file

@ -0,0 +1,12 @@
# aethex_launcher module
Import("env")
Import("env_modules")
env_launcher = env_modules.Clone()
# Source files
env_launcher.add_source_files(env.modules_sources, "*.cpp")
env_launcher.add_source_files(env.modules_sources, "ui/*.cpp")
env_launcher.add_source_files(env.modules_sources, "data/*.cpp")
env_launcher.add_source_files(env.modules_sources, "editor/*.cpp")

View file

@ -0,0 +1,692 @@
/**************************************************************************/
/* 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();
}
}

View file

@ -0,0 +1,117 @@
/**************************************************************************/
/* aethex_launcher.h */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifndef AETHEX_LAUNCHER_H
#define AETHEX_LAUNCHER_H
#include "core/object/object.h"
#include "core/object/ref_counted.h"
#include "core/io/config_file.h"
#include "data/game_library.h"
#include "data/download_manager.h"
#include "data/launcher_store.h"
#include "data/friend_system.h"
#include "data/launcher_profile.h"
class AethexLauncher : public Object {
GDCLASS(AethexLauncher, Object);
private:
static AethexLauncher *singleton;
// API Configuration
String api_base_url = "https://api.aethex.dev";
String supabase_url = "";
String supabase_anon_key = "";
String auth_token = "";
// Sub-systems
Ref<GameLibrary> game_library;
Ref<DownloadManager> download_manager;
Ref<LauncherStore> store;
Ref<FriendSystem> friend_system;
Ref<LauncherProfile> current_profile;
// Auth state
bool authenticated = false;
String user_id;
String username;
String email;
String avatar_url;
// Config
Ref<ConfigFile> config;
String config_path;
// Internal methods
void _load_config();
void _save_config();
void _load_cached_auth();
Dictionary _make_api_request(const String &p_endpoint, int p_method, const Dictionary &p_data = Dictionary());
protected:
static void _bind_methods();
public:
static AethexLauncher *get_singleton();
// Initialization
void initialize();
void shutdown();
// API Configuration
void set_api_base_url(const String &p_url);
String get_api_base_url() const;
void set_supabase_config(const String &p_url, const String &p_anon_key);
// Authentication
Error sign_in_with_email(const String &p_email, const String &p_password);
Error sign_up_with_email(const String &p_email, const String &p_password, const String &p_username);
Error sign_in_with_oauth(const String &p_provider); // google, discord, github
void sign_out();
bool is_authenticated() const;
void refresh_auth_token();
// OAuth callback handling
void handle_oauth_callback(const String &p_code, const String &p_provider);
String get_oauth_url(const String &p_provider) const;
// User info
String get_user_id() const;
String get_username() const;
String get_email() const;
String get_avatar_url() const;
String get_auth_token() const;
// Sub-systems access
Ref<GameLibrary> get_game_library() const;
Ref<DownloadManager> get_download_manager() const;
Ref<LauncherStore> get_store() const;
Ref<FriendSystem> get_friend_system() const;
Ref<LauncherProfile> get_current_profile() const;
// Launcher Profile
Error fetch_launcher_profile();
Error update_launcher_profile(const Dictionary &p_data);
Error create_launcher_profile(const String &p_gamertag);
// Quick actions
Error launch_game(const String &p_game_id);
Error install_game(const String &p_game_id);
Error uninstall_game(const String &p_game_id);
// Paths
String get_games_directory() const;
String get_downloads_directory() const;
String get_cache_directory() const;
void set_games_directory(const String &p_path);
AethexLauncher();
~AethexLauncher();
};
#endif // AETHEX_LAUNCHER_H

View file

@ -0,0 +1,18 @@
def can_build(env, platform):
return True
def configure(env):
pass
def get_doc_classes():
return [
"AethexLauncher",
"GameLibrary",
"DownloadManager",
"LauncherStore",
"FriendSystem",
"LauncherProfile",
]
def get_doc_path():
return "doc_classes"

View file

@ -0,0 +1,723 @@
/**************************************************************************/
/* download_manager.cpp */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#include "download_manager.h"
#include "../aethex_launcher.h"
#include "core/io/file_access.h"
#include "core/io/dir_access.h"
#include "core/os/os.h"
// ============================================================================
// DownloadTask
// ============================================================================
void DownloadTask::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_id"), &DownloadTask::get_id);
ClassDB::bind_method(D_METHOD("get_url"), &DownloadTask::get_url);
ClassDB::bind_method(D_METHOD("get_destination_path"), &DownloadTask::get_destination_path);
ClassDB::bind_method(D_METHOD("get_filename"), &DownloadTask::get_filename);
ClassDB::bind_method(D_METHOD("get_status"), &DownloadTask::get_status);
ClassDB::bind_method(D_METHOD("get_status_string"), &DownloadTask::get_status_string);
ClassDB::bind_method(D_METHOD("get_total_bytes"), &DownloadTask::get_total_bytes);
ClassDB::bind_method(D_METHOD("get_downloaded_bytes"), &DownloadTask::get_downloaded_bytes);
ClassDB::bind_method(D_METHOD("get_progress"), &DownloadTask::get_progress);
ClassDB::bind_method(D_METHOD("get_progress_string"), &DownloadTask::get_progress_string);
ClassDB::bind_method(D_METHOD("get_speed"), &DownloadTask::get_speed);
ClassDB::bind_method(D_METHOD("get_speed_formatted"), &DownloadTask::get_speed_formatted);
ClassDB::bind_method(D_METHOD("get_eta_formatted"), &DownloadTask::get_eta_formatted);
ClassDB::bind_method(D_METHOD("get_eta_seconds"), &DownloadTask::get_eta_seconds);
ClassDB::bind_method(D_METHOD("get_error_message"), &DownloadTask::get_error_message);
ClassDB::bind_method(D_METHOD("can_retry"), &DownloadTask::can_retry);
ClassDB::bind_method(D_METHOD("get_metadata"), &DownloadTask::get_metadata);
BIND_ENUM_CONSTANT(STATUS_PENDING);
BIND_ENUM_CONSTANT(STATUS_DOWNLOADING);
BIND_ENUM_CONSTANT(STATUS_PAUSED);
BIND_ENUM_CONSTANT(STATUS_COMPLETED);
BIND_ENUM_CONSTANT(STATUS_FAILED);
BIND_ENUM_CONSTANT(STATUS_CANCELLED);
BIND_ENUM_CONSTANT(STATUS_INSTALLING);
}
DownloadTask::DownloadTask() {
}
DownloadTask::~DownloadTask() {
}
void DownloadTask::set_id(const String &p_id) { id = p_id; }
String DownloadTask::get_id() const { return id; }
void DownloadTask::set_url(const String &p_url) { url = p_url; }
String DownloadTask::get_url() const { return url; }
void DownloadTask::set_destination_path(const String &p_path) { destination_path = p_path; }
String DownloadTask::get_destination_path() const { return destination_path; }
void DownloadTask::set_filename(const String &p_filename) { filename = p_filename; }
String DownloadTask::get_filename() const { return filename.is_empty() ? destination_path.get_file() : filename; }
void DownloadTask::set_status(Status p_status) { status = p_status; }
DownloadTask::Status DownloadTask::get_status() const { return status; }
String DownloadTask::get_status_string() const {
switch (status) {
case STATUS_PENDING: return "Pending";
case STATUS_DOWNLOADING: return "Downloading";
case STATUS_PAUSED: return "Paused";
case STATUS_COMPLETED: return "Completed";
case STATUS_FAILED: return "Failed";
case STATUS_CANCELLED: return "Cancelled";
default: return "Unknown";
}
}
void DownloadTask::set_total_bytes(int64_t p_bytes) { total_bytes = p_bytes; }
int64_t DownloadTask::get_total_bytes() const { return total_bytes; }
void DownloadTask::set_downloaded_bytes(int64_t p_bytes) {
downloaded_bytes = p_bytes;
if (total_bytes > 0) {
progress = (float)downloaded_bytes / (float)total_bytes * 100.0f;
}
}
int64_t DownloadTask::get_downloaded_bytes() const { return downloaded_bytes; }
void DownloadTask::add_downloaded_bytes(int64_t p_bytes) {
set_downloaded_bytes(downloaded_bytes + p_bytes);
}
float DownloadTask::get_progress() const { return progress; }
String DownloadTask::get_progress_string() const {
return String::num(progress, 1) + "%";
}
void DownloadTask::set_speed(float p_speed) { speed_bytes_per_sec = p_speed; }
float DownloadTask::get_speed() const { return speed_bytes_per_sec; }
String DownloadTask::get_speed_formatted() const {
if (speed_bytes_per_sec < 1024) return String::num(speed_bytes_per_sec, 0) + " B/s";
if (speed_bytes_per_sec < 1024 * 1024) return String::num(speed_bytes_per_sec / 1024.0, 1) + " KB/s";
return String::num(speed_bytes_per_sec / (1024.0 * 1024.0), 1) + " MB/s";
}
String DownloadTask::get_eta_formatted() const {
if (speed_bytes_per_sec <= 0 || total_bytes <= 0) return "--:--";
int64_t remaining = total_bytes - downloaded_bytes;
int64_t seconds = (int64_t)(remaining / speed_bytes_per_sec);
if (seconds < 60) return String::num_int64(seconds) + "s";
if (seconds < 3600) return String::num_int64(seconds / 60) + "m " + String::num_int64(seconds % 60) + "s";
return String::num_int64(seconds / 3600) + "h " + String::num_int64((seconds % 3600) / 60) + "m";
}
int64_t DownloadTask::get_eta_seconds() const {
if (speed_bytes_per_sec <= 0 || total_bytes <= 0) return -1;
int64_t remaining = total_bytes - downloaded_bytes;
return (int64_t)(remaining / speed_bytes_per_sec);
}
void DownloadTask::set_error_message(const String &p_message) { error_message = p_message; }
String DownloadTask::get_error_message() const { return error_message; }
void DownloadTask::increment_retry() { retry_count++; }
int DownloadTask::get_retry_count() const { return retry_count; }
bool DownloadTask::can_retry() const { return retry_count < max_retries; }
void DownloadTask::set_metadata(const Dictionary &p_metadata) { metadata = p_metadata; }
Dictionary DownloadTask::get_metadata() const { return metadata; }
Dictionary DownloadTask::to_dictionary() const {
Dictionary dict;
dict["id"] = id;
dict["url"] = url;
dict["destination_path"] = destination_path;
dict["filename"] = filename;
dict["status"] = (int)status;
dict["total_bytes"] = total_bytes;
dict["downloaded_bytes"] = downloaded_bytes;
dict["error_message"] = error_message;
dict["retry_count"] = retry_count;
dict["metadata"] = metadata;
return dict;
}
void DownloadTask::from_dictionary(const Dictionary &p_dict) {
id = p_dict.get("id", "");
url = p_dict.get("url", "");
destination_path = p_dict.get("destination_path", "");
filename = p_dict.get("filename", "");
status = (Status)(int)p_dict.get("status", STATUS_PENDING);
total_bytes = p_dict.get("total_bytes", 0);
downloaded_bytes = p_dict.get("downloaded_bytes", 0);
error_message = p_dict.get("error_message", "");
retry_count = p_dict.get("retry_count", 0);
metadata = p_dict.get("metadata", Dictionary());
if (total_bytes > 0) {
progress = (float)downloaded_bytes / (float)total_bytes * 100.0f;
}
}
// ============================================================================
// DownloadManager
// ============================================================================
void DownloadManager::_bind_methods() {
ClassDB::bind_method(D_METHOD("start_download", "id", "url", "destination"), &DownloadManager::start_download);
ClassDB::bind_method(D_METHOD("pause_download", "id"), &DownloadManager::pause_download);
ClassDB::bind_method(D_METHOD("resume_download", "id"), &DownloadManager::resume_download);
ClassDB::bind_method(D_METHOD("cancel_download", "id"), &DownloadManager::cancel_download);
ClassDB::bind_method(D_METHOD("retry_download", "id"), &DownloadManager::retry_download);
ClassDB::bind_method(D_METHOD("remove_download", "id"), &DownloadManager::remove_download);
ClassDB::bind_method(D_METHOD("pause_all"), &DownloadManager::pause_all);
ClassDB::bind_method(D_METHOD("resume_all"), &DownloadManager::resume_all);
ClassDB::bind_method(D_METHOD("cancel_all"), &DownloadManager::cancel_all);
ClassDB::bind_method(D_METHOD("clear_completed"), &DownloadManager::clear_completed);
ClassDB::bind_method(D_METHOD("get_task", "id"), &DownloadManager::get_task);
ClassDB::bind_method(D_METHOD("get_download", "id"), &DownloadManager::get_download);
ClassDB::bind_method(D_METHOD("get_all_tasks"), &DownloadManager::get_all_tasks);
ClassDB::bind_method(D_METHOD("get_active_tasks"), &DownloadManager::get_active_tasks);
ClassDB::bind_method(D_METHOD("get_active_downloads"), &DownloadManager::get_active_downloads);
ClassDB::bind_method(D_METHOD("get_pending_tasks"), &DownloadManager::get_pending_tasks);
ClassDB::bind_method(D_METHOD("get_pending_downloads"), &DownloadManager::get_pending_downloads);
ClassDB::bind_method(D_METHOD("get_completed_tasks"), &DownloadManager::get_completed_tasks);
ClassDB::bind_method(D_METHOD("get_completed_downloads"), &DownloadManager::get_completed_downloads);
ClassDB::bind_method(D_METHOD("has_active_downloads"), &DownloadManager::has_active_downloads);
ClassDB::bind_method(D_METHOD("get_active_count"), &DownloadManager::get_active_count);
ClassDB::bind_method(D_METHOD("get_pending_count"), &DownloadManager::get_pending_count);
ClassDB::bind_method(D_METHOD("set_max_concurrent", "max"), &DownloadManager::set_max_concurrent);
ClassDB::bind_method(D_METHOD("get_max_concurrent"), &DownloadManager::get_max_concurrent);
ADD_SIGNAL(MethodInfo("download_started", PropertyInfo(Variant::STRING, "id")));
ADD_SIGNAL(MethodInfo("download_progress", PropertyInfo(Variant::STRING, "id"), PropertyInfo(Variant::FLOAT, "progress")));
ADD_SIGNAL(MethodInfo("download_completed", PropertyInfo(Variant::STRING, "id")));
ADD_SIGNAL(MethodInfo("download_failed", PropertyInfo(Variant::STRING, "id"), PropertyInfo(Variant::STRING, "error")));
ADD_SIGNAL(MethodInfo("download_cancelled", PropertyInfo(Variant::STRING, "id")));
ADD_SIGNAL(MethodInfo("download_paused", PropertyInfo(Variant::STRING, "id")));
ADD_SIGNAL(MethodInfo("download_resumed", PropertyInfo(Variant::STRING, "id")));
}
DownloadManager::DownloadManager() {
downloads_directory = OS::get_singleton()->get_user_data_dir() + "/downloads";
}
DownloadManager::~DownloadManager() {
stop_processing();
}
void DownloadManager::set_launcher(AethexLauncher *p_launcher) {
launcher = p_launcher;
if (launcher) {
downloads_directory = launcher->get_downloads_directory();
}
}
Ref<DownloadTask> DownloadManager::start_download(const String &p_id, const String &p_url, const String &p_destination) {
// Check if already exists
Ref<DownloadTask> existing = get_task(p_id);
if (existing.is_valid()) {
if (existing->get_status() == DownloadTask::STATUS_PAUSED ||
existing->get_status() == DownloadTask::STATUS_FAILED) {
resume_download(p_id);
return existing;
}
return existing;
}
Ref<DownloadTask> task;
task.instantiate();
task->set_id(p_id);
task->set_url(p_url);
task->set_destination_path(p_destination);
task->set_status(DownloadTask::STATUS_PENDING);
mutex.lock();
tasks.push_back(task);
mutex.unlock();
emit_signal("download_started", p_id);
// Start processing if not already running
start_processing();
return task;
}
void DownloadManager::pause_download(const String &p_id) {
Ref<DownloadTask> task = get_task(p_id);
if (task.is_valid() && task->get_status() == DownloadTask::STATUS_DOWNLOADING) {
task->set_status(DownloadTask::STATUS_PAUSED);
emit_signal("download_paused", p_id);
}
}
void DownloadManager::resume_download(const String &p_id) {
Ref<DownloadTask> task = get_task(p_id);
if (task.is_valid() &&
(task->get_status() == DownloadTask::STATUS_PAUSED ||
task->get_status() == DownloadTask::STATUS_FAILED)) {
task->set_status(DownloadTask::STATUS_PENDING);
emit_signal("download_resumed", p_id);
start_processing();
}
}
void DownloadManager::cancel_download(const String &p_id) {
Ref<DownloadTask> task = get_task(p_id);
if (task.is_valid()) {
task->set_status(DownloadTask::STATUS_CANCELLED);
emit_signal("download_cancelled", p_id);
}
}
void DownloadManager::retry_download(const String &p_id) {
Ref<DownloadTask> task = get_task(p_id);
if (task.is_valid() && task->can_retry()) {
task->increment_retry();
task->set_status(DownloadTask::STATUS_PENDING);
task->set_error_message("");
start_processing();
}
}
void DownloadManager::remove_download(const String &p_id) {
mutex.lock();
for (int i = 0; i < tasks.size(); i++) {
if (tasks[i].is_valid() && tasks[i]->get_id() == p_id) {
tasks.remove_at(i);
break;
}
}
mutex.unlock();
}
void DownloadManager::pause_all() {
paused_all = true;
for (int i = 0; i < tasks.size(); i++) {
if (tasks[i].is_valid() && tasks[i]->get_status() == DownloadTask::STATUS_DOWNLOADING) {
tasks[i]->set_status(DownloadTask::STATUS_PAUSED);
}
}
}
void DownloadManager::resume_all() {
paused_all = false;
for (int i = 0; i < tasks.size(); i++) {
if (tasks[i].is_valid() && tasks[i]->get_status() == DownloadTask::STATUS_PAUSED) {
tasks[i]->set_status(DownloadTask::STATUS_PENDING);
}
}
start_processing();
}
void DownloadManager::cancel_all() {
for (int i = 0; i < tasks.size(); i++) {
if (tasks[i].is_valid()) {
DownloadTask::Status status = tasks[i]->get_status();
if (status == DownloadTask::STATUS_DOWNLOADING ||
status == DownloadTask::STATUS_PENDING ||
status == DownloadTask::STATUS_PAUSED) {
tasks[i]->set_status(DownloadTask::STATUS_CANCELLED);
}
}
}
}
void DownloadManager::clear_completed() {
mutex.lock();
for (int i = tasks.size() - 1; i >= 0; i--) {
if (tasks[i].is_valid()) {
DownloadTask::Status status = tasks[i]->get_status();
if (status == DownloadTask::STATUS_COMPLETED ||
status == DownloadTask::STATUS_CANCELLED) {
tasks.remove_at(i);
}
}
}
mutex.unlock();
}
Ref<DownloadTask> DownloadManager::get_task(const String &p_id) const {
for (int i = 0; i < tasks.size(); i++) {
if (tasks[i].is_valid() && tasks[i]->get_id() == p_id) {
return tasks[i];
}
}
return Ref<DownloadTask>();
}
TypedArray<DownloadTask> DownloadManager::get_all_tasks() const {
TypedArray<DownloadTask> result;
for (int i = 0; i < tasks.size(); i++) {
if (tasks[i].is_valid()) {
result.push_back(tasks[i]);
}
}
return result;
}
TypedArray<DownloadTask> DownloadManager::get_active_tasks() const {
TypedArray<DownloadTask> result;
for (int i = 0; i < tasks.size(); i++) {
if (tasks[i].is_valid() && tasks[i]->get_status() == DownloadTask::STATUS_DOWNLOADING) {
result.push_back(tasks[i]);
}
}
return result;
}
TypedArray<DownloadTask> DownloadManager::get_pending_tasks() const {
TypedArray<DownloadTask> result;
for (int i = 0; i < tasks.size(); i++) {
if (tasks[i].is_valid() && tasks[i]->get_status() == DownloadTask::STATUS_PENDING) {
result.push_back(tasks[i]);
}
}
return result;
}
TypedArray<DownloadTask> DownloadManager::get_completed_tasks() const {
TypedArray<DownloadTask> result;
for (int i = 0; i < tasks.size(); i++) {
if (tasks[i].is_valid() && tasks[i]->get_status() == DownloadTask::STATUS_COMPLETED) {
result.push_back(tasks[i]);
}
}
return result;
}
// Alias methods for backward compatibility
Ref<DownloadTask> DownloadManager::get_download(const String &p_id) const {
return get_task(p_id);
}
TypedArray<DownloadTask> DownloadManager::get_active_downloads() const {
return get_active_tasks();
}
TypedArray<DownloadTask> DownloadManager::get_pending_downloads() const {
return get_pending_tasks();
}
TypedArray<DownloadTask> DownloadManager::get_completed_downloads() const {
return get_completed_tasks();
}
bool DownloadManager::has_active_downloads() const {
return get_active_count() > 0;
}
int DownloadManager::get_active_count() const {
int count = 0;
for (int i = 0; i < tasks.size(); i++) {
if (tasks[i].is_valid() && tasks[i]->get_status() == DownloadTask::STATUS_DOWNLOADING) {
count++;
}
}
return count;
}
int DownloadManager::get_pending_count() const {
int count = 0;
for (int i = 0; i < tasks.size(); i++) {
if (tasks[i].is_valid() && tasks[i]->get_status() == DownloadTask::STATUS_PENDING) {
count++;
}
}
return count;
}
void DownloadManager::set_max_concurrent(int p_max) {
max_concurrent_downloads = CLAMP(p_max, 1, 10);
}
int DownloadManager::get_max_concurrent() const {
return max_concurrent_downloads;
}
void DownloadManager::set_downloads_directory(const String &p_path) {
downloads_directory = p_path;
}
String DownloadManager::get_downloads_directory() const {
return downloads_directory;
}
void DownloadManager::start_processing() {
if (thread_running) return;
thread_running = true;
download_thread = memnew(Thread);
download_thread->start(_thread_callback, this);
}
void DownloadManager::_thread_callback(void *p_userdata) {
DownloadManager *dm = static_cast<DownloadManager *>(p_userdata);
dm->_download_thread_func();
}
void DownloadManager::stop_processing() {
thread_running = false;
if (download_thread) {
download_thread->wait_to_finish();
memdelete(download_thread);
download_thread = nullptr;
}
}
void DownloadManager::_download_thread_func() {
while (thread_running) {
if (paused_all) {
OS::get_singleton()->delay_usec(100000);
continue;
}
// Find pending tasks
Ref<DownloadTask> task_to_process;
mutex.lock();
int active_count = 0;
for (int i = 0; i < tasks.size(); i++) {
if (tasks[i].is_valid()) {
if (tasks[i]->get_status() == DownloadTask::STATUS_DOWNLOADING) {
active_count++;
}
}
}
if (active_count < max_concurrent_downloads) {
for (int i = 0; i < tasks.size(); i++) {
if (tasks[i].is_valid() && tasks[i]->get_status() == DownloadTask::STATUS_PENDING) {
task_to_process = tasks[i];
task_to_process->set_status(DownloadTask::STATUS_DOWNLOADING);
break;
}
}
}
mutex.unlock();
if (task_to_process.is_valid()) {
_process_download(task_to_process);
} else {
// No work to do, check if we should stop
if (get_pending_count() == 0 && get_active_count() == 0) {
thread_running = false;
} else {
OS::get_singleton()->delay_usec(100000);
}
}
}
}
void DownloadManager::_process_download(Ref<DownloadTask> p_task) {
String url = p_task->get_url();
String dest_path = p_task->get_destination_path();
// Ensure destination directory exists
String dest_dir = dest_path.get_base_dir();
DirAccess::make_dir_recursive_absolute(dest_dir);
// Parse URL
String host;
int port = 443;
String path;
bool use_ssl = url.begins_with("https://");
url = url.replace("https://", "").replace("http://", "");
int slash_pos = url.find("/");
if (slash_pos >= 0) {
host = url.substr(0, slash_pos);
path = url.substr(slash_pos);
} else {
host = url;
path = "/";
}
// Check for port in host
int colon_pos = host.find(":");
if (colon_pos >= 0) {
port = host.substr(colon_pos + 1).to_int();
host = host.substr(0, colon_pos);
}
// Connect
HTTPClient *http = HTTPClient::create();
Ref<TLSOptions> tls_options;
if (use_ssl) {
tls_options = TLSOptions::client();
}
Error err = http->connect_to_host(host, port, tls_options);
if (err != OK) {
p_task->set_status(DownloadTask::STATUS_FAILED);
p_task->set_error_message("Failed to connect to server");
call_deferred("emit_signal", "download_failed", p_task->get_id(), "Connection failed");
memdelete(http);
return;
}
// Wait for connection
while (http->get_status() == HTTPClient::STATUS_CONNECTING ||
http->get_status() == HTTPClient::STATUS_RESOLVING) {
http->poll();
OS::get_singleton()->delay_usec(50000);
if (p_task->get_status() != DownloadTask::STATUS_DOWNLOADING) {
memdelete(http);
return; // Cancelled or paused
}
}
if (http->get_status() != HTTPClient::STATUS_CONNECTED) {
p_task->set_status(DownloadTask::STATUS_FAILED);
p_task->set_error_message("Connection failed");
call_deferred("emit_signal", "download_failed", p_task->get_id(), "Connection failed");
memdelete(http);
return;
}
// Make request
Vector<String> headers;
// Support resume
if (FileAccess::exists(dest_path)) {
Ref<FileAccess> existing = FileAccess::open(dest_path, FileAccess::READ);
if (existing.is_valid()) {
int64_t existing_size = existing->get_length();
existing->close();
if (existing_size > 0) {
headers.push_back("Range: bytes=" + String::num_int64(existing_size) + "-");
p_task->set_downloaded_bytes(existing_size);
}
}
}
err = http->request(HTTPClient::METHOD_GET, path, headers, nullptr, 0);
if (err != OK) {
p_task->set_status(DownloadTask::STATUS_FAILED);
p_task->set_error_message("Request failed");
call_deferred("emit_signal", "download_failed", p_task->get_id(), "Request failed");
memdelete(http);
return;
}
// Wait for response
while (http->get_status() == HTTPClient::STATUS_REQUESTING) {
http->poll();
OS::get_singleton()->delay_usec(50000);
if (p_task->get_status() != DownloadTask::STATUS_DOWNLOADING) {
memdelete(http);
return;
}
}
if (!http->has_response()) {
p_task->set_status(DownloadTask::STATUS_FAILED);
p_task->set_error_message("No response");
call_deferred("emit_signal", "download_failed", p_task->get_id(), "No response");
memdelete(http);
return;
}
int response_code = http->get_response_code();
if (response_code != 200 && response_code != 206) {
p_task->set_status(DownloadTask::STATUS_FAILED);
p_task->set_error_message("HTTP " + String::num_int64(response_code));
call_deferred("emit_signal", "download_failed", p_task->get_id(), "HTTP " + String::num_int64(response_code));
memdelete(http);
return;
}
// Get content length
int64_t content_length = http->get_response_body_length();
if (content_length > 0) {
p_task->set_total_bytes(p_task->get_downloaded_bytes() + content_length);
}
// Open file for writing
Ref<FileAccess> file = FileAccess::open(dest_path, response_code == 206 ? FileAccess::READ_WRITE : FileAccess::WRITE);
if (file.is_null()) {
p_task->set_status(DownloadTask::STATUS_FAILED);
p_task->set_error_message("Failed to open file");
call_deferred("emit_signal", "download_failed", p_task->get_id(), "Failed to open file");
memdelete(http);
return;
}
if (response_code == 206) {
file->seek_end();
}
// Download loop
uint64_t last_time = OS::get_singleton()->get_ticks_msec();
int64_t bytes_this_second = 0;
while (http->get_status() == HTTPClient::STATUS_BODY) {
http->poll();
if (p_task->get_status() != DownloadTask::STATUS_DOWNLOADING) {
file->close();
memdelete(http);
return;
}
PackedByteArray chunk = http->read_response_body_chunk();
if (chunk.size() > 0) {
file->store_buffer(chunk.ptr(), chunk.size());
p_task->add_downloaded_bytes(chunk.size());
bytes_this_second += chunk.size();
// Calculate speed
uint64_t current_time = OS::get_singleton()->get_ticks_msec();
if (current_time - last_time >= 1000) {
p_task->set_speed((float)bytes_this_second);
bytes_this_second = 0;
last_time = current_time;
// Emit progress signal
call_deferred("emit_signal", "download_progress", p_task->get_id(), p_task->get_progress());
}
}
OS::get_singleton()->delay_usec(1000);
}
file->close();
memdelete(http);
// Verify download
if (p_task->get_total_bytes() > 0 && p_task->get_downloaded_bytes() < p_task->get_total_bytes()) {
p_task->set_status(DownloadTask::STATUS_FAILED);
p_task->set_error_message("Incomplete download");
call_deferred("emit_signal", "download_failed", p_task->get_id(), "Incomplete download");
return;
}
p_task->set_status(DownloadTask::STATUS_COMPLETED);
call_deferred("emit_signal", "download_completed", p_task->get_id());
// Auto-extract if archive
String ext = dest_path.get_extension().to_lower();
if (ext == "zip" || ext == "7z" || ext == "tar" || ext == "gz") {
String extract_dir = dest_path.get_basename();
_extract_archive(dest_path, extract_dir);
}
}
void DownloadManager::_extract_archive(const String &p_archive_path, const String &p_destination) {
// TODO: Implement archive extraction
// This would use minizip or similar
}

View file

@ -0,0 +1,179 @@
/**************************************************************************/
/* download_manager.h */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifndef DOWNLOAD_MANAGER_H
#define DOWNLOAD_MANAGER_H
#include "core/object/ref_counted.h"
#include "core/io/http_client.h"
#include "core/os/thread.h"
#include "core/os/mutex.h"
#include "core/variant/typed_array.h"
class AethexLauncher;
class DownloadTask : public RefCounted {
GDCLASS(DownloadTask, RefCounted);
public:
enum Status {
STATUS_PENDING,
STATUS_DOWNLOADING,
STATUS_INSTALLING,
STATUS_PAUSED,
STATUS_COMPLETED,
STATUS_FAILED,
STATUS_CANCELLED
};
private:
String id;
String url;
String destination_path;
String filename;
Status status = STATUS_PENDING;
int64_t total_bytes = 0;
int64_t downloaded_bytes = 0;
float progress = 0.0f;
float speed_bytes_per_sec = 0.0f;
String error_message;
int retry_count = 0;
int max_retries = 3;
Dictionary metadata;
protected:
static void _bind_methods();
public:
void set_id(const String &p_id);
String get_id() const;
void set_url(const String &p_url);
String get_url() const;
void set_destination_path(const String &p_path);
String get_destination_path() const;
void set_filename(const String &p_filename);
String get_filename() const;
void set_status(Status p_status);
Status get_status() const;
String get_status_string() const;
void set_total_bytes(int64_t p_bytes);
int64_t get_total_bytes() const;
void set_downloaded_bytes(int64_t p_bytes);
int64_t get_downloaded_bytes() const;
void add_downloaded_bytes(int64_t p_bytes);
float get_progress() const;
String get_progress_string() const;
void set_speed(float p_speed);
float get_speed() const;
String get_speed_formatted() const;
int64_t get_eta_seconds() const;
String get_eta_formatted() const;
void set_error_message(const String &p_message);
String get_error_message() const;
void increment_retry();
int get_retry_count() const;
bool can_retry() const;
void set_metadata(const Dictionary &p_metadata);
Dictionary get_metadata() const;
Dictionary to_dictionary() const;
void from_dictionary(const Dictionary &p_dict);
DownloadTask();
~DownloadTask();
};
VARIANT_ENUM_CAST(DownloadTask::Status);
class DownloadManager : public RefCounted {
GDCLASS(DownloadManager, RefCounted);
private:
AethexLauncher *launcher = nullptr;
Vector<Ref<DownloadTask>> tasks;
Vector<Ref<DownloadTask>> active_downloads;
int max_concurrent_downloads = 3;
bool paused_all = false;
Thread *download_thread = nullptr;
Mutex mutex;
bool thread_running = false;
String downloads_directory;
static void _thread_callback(void *p_userdata);
void _download_thread_func();
void _process_download(Ref<DownloadTask> p_task);
void _extract_archive(const String &p_archive_path, const String &p_destination);
protected:
static void _bind_methods();
public:
void set_launcher(AethexLauncher *p_launcher);
// Download management
Ref<DownloadTask> start_download(const String &p_id, const String &p_url, const String &p_destination);
void pause_download(const String &p_id);
void resume_download(const String &p_id);
void cancel_download(const String &p_id);
void retry_download(const String &p_id);
void remove_download(const String &p_id);
// Bulk operations
void pause_all();
void resume_all();
void cancel_all();
void clear_completed();
// Queries
Ref<DownloadTask> get_task(const String &p_id) const;
Ref<DownloadTask> get_download(const String &p_id) const; // Alias for get_task
TypedArray<DownloadTask> get_all_tasks() const;
TypedArray<DownloadTask> get_active_tasks() const;
TypedArray<DownloadTask> get_active_downloads() const; // Alias for get_active_tasks
TypedArray<DownloadTask> get_pending_tasks() const;
TypedArray<DownloadTask> get_pending_downloads() const; // Alias for get_pending_tasks
TypedArray<DownloadTask> get_completed_tasks() const;
TypedArray<DownloadTask> get_completed_downloads() const; // Alias for get_completed_tasks
bool has_active_downloads() const;
int get_active_count() const;
int get_pending_count() const;
// Settings
void set_max_concurrent(int p_max);
int get_max_concurrent() const;
void set_downloads_directory(const String &p_path);
String get_downloads_directory() const;
// Thread control
void start_processing();
void stop_processing();
DownloadManager();
~DownloadManager();
};
#endif // DOWNLOAD_MANAGER_H

View file

@ -0,0 +1,402 @@
/**************************************************************************/
/* friend_system.cpp */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#include "friend_system.h"
#include "../aethex_launcher.h"
#include "core/io/json.h"
// ============================================================================
// Friend
// ============================================================================
void Friend::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_id"), &Friend::get_id);
ClassDB::bind_method(D_METHOD("get_user_id"), &Friend::get_user_id);
ClassDB::bind_method(D_METHOD("get_username"), &Friend::get_username);
ClassDB::bind_method(D_METHOD("get_display_name"), &Friend::get_display_name);
ClassDB::bind_method(D_METHOD("get_effective_name"), &Friend::get_effective_name);
ClassDB::bind_method(D_METHOD("get_avatar_url"), &Friend::get_avatar_url);
ClassDB::bind_method(D_METHOD("get_status"), &Friend::get_status);
ClassDB::bind_method(D_METHOD("is_online"), &Friend::is_online);
ClassDB::bind_method(D_METHOD("get_custom_status"), &Friend::get_custom_status);
ClassDB::bind_method(D_METHOD("get_current_game"), &Friend::get_current_game);
ClassDB::bind_method(D_METHOD("get_is_playing"), &Friend::get_is_playing);
ClassDB::bind_method(D_METHOD("get_friendship_since"), &Friend::get_friendship_since);
ClassDB::bind_method(D_METHOD("get_nickname"), &Friend::get_nickname);
ClassDB::bind_method(D_METHOD("get_is_favorite"), &Friend::get_is_favorite);
ClassDB::bind_method(D_METHOD("get_is_blocked"), &Friend::get_is_blocked);
ClassDB::bind_method(D_METHOD("to_dictionary"), &Friend::to_dictionary);
ClassDB::bind_method(D_METHOD("from_dictionary", "dict"), &Friend::from_dictionary);
BIND_ENUM_CONSTANT(STATUS_OFFLINE);
BIND_ENUM_CONSTANT(STATUS_ONLINE);
BIND_ENUM_CONSTANT(STATUS_AWAY);
BIND_ENUM_CONSTANT(STATUS_BUSY);
BIND_ENUM_CONSTANT(STATUS_INVISIBLE);
}
Friend::Friend() {}
Friend::~Friend() {}
void Friend::set_id(const String &p_id) { id = p_id; }
String Friend::get_id() const { return id; }
void Friend::set_user_id(const String &p_id) { user_id = p_id; }
String Friend::get_user_id() const { return user_id; }
void Friend::set_username(const String &p_username) { username = p_username; }
String Friend::get_username() const { return username; }
void Friend::set_display_name(const String &p_name) { display_name = p_name; }
String Friend::get_display_name() const { return display_name; }
String Friend::get_effective_name() const {
if (!nickname.is_empty()) return nickname;
if (!display_name.is_empty()) return display_name;
return username;
}
void Friend::set_avatar_url(const String &p_url) { avatar_url = p_url; }
String Friend::get_avatar_url() const { return avatar_url; }
void Friend::set_status(Status p_status) { status = p_status; }
Friend::Status Friend::get_status() const { return status; }
bool Friend::is_online() const {
return status == STATUS_ONLINE || status == STATUS_AWAY || status == STATUS_BUSY;
}
void Friend::set_custom_status(const String &p_status) { custom_status = p_status; }
String Friend::get_custom_status() const { return custom_status; }
void Friend::set_current_game(const String &p_game, const String &p_game_id) {
current_game = p_game;
current_game_id = p_game_id;
is_playing = !p_game.is_empty();
}
String Friend::get_current_game() const { return current_game; }
String Friend::get_current_game_id() const { return current_game_id; }
void Friend::set_is_playing(bool p_playing) { is_playing = p_playing; }
bool Friend::get_is_playing() const { return is_playing; }
void Friend::set_friendship_id(const String &p_id) { friendship_id = p_id; }
String Friend::get_friendship_id() const { return friendship_id; }
void Friend::set_friendship_since(const String &p_date) { friendship_since = p_date; }
String Friend::get_friendship_since() const { return friendship_since; }
void Friend::set_nickname(const String &p_nickname) { nickname = p_nickname; }
String Friend::get_nickname() const { return nickname; }
void Friend::set_is_favorite(bool p_favorite) { is_favorite = p_favorite; }
bool Friend::get_is_favorite() const { return is_favorite; }
void Friend::set_is_blocked(bool p_blocked) { is_blocked = p_blocked; }
bool Friend::get_is_blocked() const { return is_blocked; }
Dictionary Friend::to_dictionary() const {
Dictionary dict;
dict["id"] = id;
dict["user_id"] = user_id;
dict["username"] = username;
dict["display_name"] = display_name;
dict["avatar_url"] = avatar_url;
dict["status"] = status;
dict["custom_status"] = custom_status;
dict["current_game"] = current_game;
dict["current_game_id"] = current_game_id;
dict["is_playing"] = is_playing;
dict["friendship_id"] = friendship_id;
dict["friendship_since"] = friendship_since;
dict["nickname"] = nickname;
dict["is_favorite"] = is_favorite;
dict["is_blocked"] = is_blocked;
return dict;
}
void Friend::from_dictionary(const Dictionary &p_dict) {
id = p_dict.get("id", "");
user_id = p_dict.get("user_id", "");
username = p_dict.get("username", "");
display_name = p_dict.get("display_name", "");
avatar_url = p_dict.get("avatar_url", "");
status = p_dict.get("status", "offline");
custom_status = p_dict.get("custom_status", "");
current_game = p_dict.get("current_game", "");
current_game_id = p_dict.get("current_game_id", "");
is_playing = p_dict.get("is_playing", false);
friendship_id = p_dict.get("friendship_id", "");
friendship_since = p_dict.get("friendship_since", "");
nickname = p_dict.get("nickname", "");
is_favorite = p_dict.get("is_favorite", false);
is_blocked = p_dict.get("is_blocked", false);
}
// ============================================================================
// FriendSystem
// ============================================================================
void FriendSystem::_bind_methods() {
ClassDB::bind_method(D_METHOD("fetch_friends"), &FriendSystem::fetch_friends);
ClassDB::bind_method(D_METHOD("fetch_pending_requests"), &FriendSystem::fetch_pending_requests);
ClassDB::bind_method(D_METHOD("fetch_blocked"), &FriendSystem::fetch_blocked);
ClassDB::bind_method(D_METHOD("send_friend_request", "user_id"), &FriendSystem::send_friend_request);
ClassDB::bind_method(D_METHOD("send_friend_request_by_username", "username"), &FriendSystem::send_friend_request_by_username);
ClassDB::bind_method(D_METHOD("accept_friend_request", "request_id"), &FriendSystem::accept_friend_request);
ClassDB::bind_method(D_METHOD("decline_friend_request", "request_id"), &FriendSystem::decline_friend_request);
ClassDB::bind_method(D_METHOD("remove_friend", "friend_id"), &FriendSystem::remove_friend);
ClassDB::bind_method(D_METHOD("block_user", "user_id"), &FriendSystem::block_user);
ClassDB::bind_method(D_METHOD("unblock_user", "user_id"), &FriendSystem::unblock_user);
ClassDB::bind_method(D_METHOD("set_friend_nickname", "friend_id", "nickname"), &FriendSystem::set_friend_nickname);
ClassDB::bind_method(D_METHOD("set_friend_favorite", "friend_id", "favorite"), &FriendSystem::set_friend_favorite);
ClassDB::bind_method(D_METHOD("get_friends"), &FriendSystem::get_friends);
ClassDB::bind_method(D_METHOD("get_online_friends"), &FriendSystem::get_online_friends);
ClassDB::bind_method(D_METHOD("get_friends_playing"), &FriendSystem::get_friends_playing);
ClassDB::bind_method(D_METHOD("get_favorites"), &FriendSystem::get_favorites);
ClassDB::bind_method(D_METHOD("get_pending_requests"), &FriendSystem::get_pending_requests);
ClassDB::bind_method(D_METHOD("get_sent_requests"), &FriendSystem::get_sent_requests);
ClassDB::bind_method(D_METHOD("get_blocked"), &FriendSystem::get_blocked);
ClassDB::bind_method(D_METHOD("get_friend", "friend_id"), &FriendSystem::get_friend);
ClassDB::bind_method(D_METHOD("is_friend", "user_id"), &FriendSystem::is_friend);
ClassDB::bind_method(D_METHOD("is_blocked_user", "user_id"), &FriendSystem::is_blocked_user);
ClassDB::bind_method(D_METHOD("get_friend_count"), &FriendSystem::get_friend_count);
ClassDB::bind_method(D_METHOD("get_online_count"), &FriendSystem::get_online_count);
ClassDB::bind_method(D_METHOD("get_pending_count"), &FriendSystem::get_pending_count);
ClassDB::bind_method(D_METHOD("search_users", "query"), &FriendSystem::search_users);
ADD_SIGNAL(MethodInfo("friends_loaded"));
ADD_SIGNAL(MethodInfo("friend_added", PropertyInfo(Variant::OBJECT, "friend")));
ADD_SIGNAL(MethodInfo("friend_removed", PropertyInfo(Variant::STRING, "friend_id")));
ADD_SIGNAL(MethodInfo("friend_status_changed", PropertyInfo(Variant::STRING, "friend_id"), PropertyInfo(Variant::STRING, "status")));
ADD_SIGNAL(MethodInfo("friend_request_received", PropertyInfo(Variant::OBJECT, "request")));
ADD_SIGNAL(MethodInfo("friend_request_sent", PropertyInfo(Variant::STRING, "user_id")));
ADD_SIGNAL(MethodInfo("user_blocked", PropertyInfo(Variant::STRING, "user_id")));
ADD_SIGNAL(MethodInfo("user_unblocked", PropertyInfo(Variant::STRING, "user_id")));
ADD_SIGNAL(MethodInfo("users_found", PropertyInfo(Variant::ARRAY, "users")));
}
FriendSystem::FriendSystem() {}
FriendSystem::~FriendSystem() {}
void FriendSystem::set_launcher(AethexLauncher *p_launcher) {
launcher = p_launcher;
}
void FriendSystem::_parse_friends_response(const Dictionary &p_response) {
friends.clear();
if (p_response.has("friends")) {
Array friends_array = p_response["friends"];
for (int i = 0; i < friends_array.size(); i++) {
Ref<Friend> f;
f.instantiate();
f->from_dictionary(friends_array[i]);
friends.push_back(f);
}
}
emit_signal("friends_loaded");
}
void FriendSystem::fetch_friends() {
// TODO: Make API call
emit_signal("friends_loaded");
}
void FriendSystem::fetch_pending_requests() {
// TODO: Make API call
}
void FriendSystem::fetch_blocked() {
// TODO: Make API call
}
Error FriendSystem::send_friend_request(const String &p_user_id) {
// TODO: Make API call
emit_signal("friend_request_sent", p_user_id);
return OK;
}
Error FriendSystem::send_friend_request_by_username(const String &p_username) {
// TODO: Make API call to lookup user then send request
return OK;
}
Error FriendSystem::accept_friend_request(const String &p_request_id) {
// TODO: Make API call
return OK;
}
Error FriendSystem::decline_friend_request(const String &p_request_id) {
// TODO: Make API call
return OK;
}
Error FriendSystem::remove_friend(const String &p_friend_id) {
for (int i = 0; i < friends.size(); i++) {
if (friends[i].is_valid() && friends[i]->get_id() == p_friend_id) {
friends.remove_at(i);
emit_signal("friend_removed", p_friend_id);
return OK;
}
}
return ERR_DOES_NOT_EXIST;
}
Error FriendSystem::block_user(const String &p_user_id) {
// TODO: Make API call
emit_signal("user_blocked", p_user_id);
return OK;
}
Error FriendSystem::unblock_user(const String &p_user_id) {
// TODO: Make API call
emit_signal("user_unblocked", p_user_id);
return OK;
}
Error FriendSystem::set_friend_nickname(const String &p_friend_id, const String &p_nickname) {
Ref<Friend> f = get_friend(p_friend_id);
if (f.is_valid()) {
f->set_nickname(p_nickname);
return OK;
}
return ERR_DOES_NOT_EXIST;
}
Error FriendSystem::set_friend_favorite(const String &p_friend_id, bool p_favorite) {
Ref<Friend> f = get_friend(p_friend_id);
if (f.is_valid()) {
f->set_is_favorite(p_favorite);
return OK;
}
return ERR_DOES_NOT_EXIST;
}
TypedArray<Friend> FriendSystem::get_friends() const {
TypedArray<Friend> result;
for (int i = 0; i < friends.size(); i++) {
if (friends[i].is_valid()) {
result.push_back(friends[i]);
}
}
return result;
}
TypedArray<Friend> FriendSystem::get_online_friends() const {
TypedArray<Friend> result;
for (int i = 0; i < friends.size(); i++) {
if (friends[i].is_valid() && friends[i]->is_online()) {
result.push_back(friends[i]);
}
}
return result;
}
TypedArray<Friend> FriendSystem::get_friends_playing() const {
TypedArray<Friend> result;
for (int i = 0; i < friends.size(); i++) {
if (friends[i].is_valid() && friends[i]->get_is_playing()) {
result.push_back(friends[i]);
}
}
return result;
}
TypedArray<Friend> FriendSystem::get_favorites() const {
TypedArray<Friend> result;
for (int i = 0; i < friends.size(); i++) {
if (friends[i].is_valid() && friends[i]->get_is_favorite()) {
result.push_back(friends[i]);
}
}
return result;
}
TypedArray<Friend> FriendSystem::get_pending_requests() const {
TypedArray<Friend> result;
for (int i = 0; i < pending_requests.size(); i++) {
result.push_back(pending_requests[i]);
}
return result;
}
TypedArray<Friend> FriendSystem::get_sent_requests() const {
TypedArray<Friend> result;
for (int i = 0; i < sent_requests.size(); i++) {
result.push_back(sent_requests[i]);
}
return result;
}
TypedArray<Friend> FriendSystem::get_blocked() const {
TypedArray<Friend> result;
for (int i = 0; i < blocked.size(); i++) {
result.push_back(blocked[i]);
}
return result;
}
Ref<Friend> FriendSystem::get_friend(const String &p_friend_id) const {
for (int i = 0; i < friends.size(); i++) {
if (friends[i].is_valid() && friends[i]->get_id() == p_friend_id) {
return friends[i];
}
}
return Ref<Friend>();
}
Ref<Friend> FriendSystem::get_friend_by_username(const String &p_username) const {
for (int i = 0; i < friends.size(); i++) {
if (friends[i].is_valid() && friends[i]->get_username() == p_username) {
return friends[i];
}
}
return Ref<Friend>();
}
bool FriendSystem::is_friend(const String &p_user_id) const {
for (int i = 0; i < friends.size(); i++) {
if (friends[i].is_valid() && friends[i]->get_user_id() == p_user_id) {
return true;
}
}
return false;
}
bool FriendSystem::is_blocked_user(const String &p_user_id) const {
for (int i = 0; i < blocked.size(); i++) {
if (blocked[i].is_valid() && blocked[i]->get_user_id() == p_user_id) {
return true;
}
}
return false;
}
int FriendSystem::get_friend_count() const { return friends.size(); }
int FriendSystem::get_online_count() const {
int count = 0;
for (int i = 0; i < friends.size(); i++) {
if (friends[i].is_valid() && friends[i]->is_online()) count++;
}
return count;
}
int FriendSystem::get_pending_count() const { return pending_requests.size(); }
void FriendSystem::search_users(const String &p_query) {
// TODO: Make API call
emit_signal("users_found", Array());
}

View file

@ -0,0 +1,165 @@
/**************************************************************************/
/* friend_system.h */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifndef FRIEND_SYSTEM_H
#define FRIEND_SYSTEM_H
#include "core/object/ref_counted.h"
#include "core/variant/typed_array.h"
class AethexLauncher;
class Friend : public RefCounted {
GDCLASS(Friend, RefCounted);
public:
enum Status {
STATUS_OFFLINE,
STATUS_ONLINE,
STATUS_AWAY,
STATUS_BUSY,
STATUS_INVISIBLE
};
private:
String id;
String user_id;
String username;
String display_name;
String avatar_url;
Status status = STATUS_OFFLINE;
String custom_status;
String current_game;
String current_game_id;
bool is_playing = false;
String friendship_id;
String friendship_since;
String nickname;
bool is_favorite = false;
bool is_blocked = false;
protected:
static void _bind_methods();
public:
void set_id(const String &p_id);
String get_id() const;
void set_user_id(const String &p_id);
String get_user_id() const;
void set_username(const String &p_username);
String get_username() const;
void set_display_name(const String &p_name);
String get_display_name() const;
String get_effective_name() const; // Returns nickname if set, else display_name
void set_avatar_url(const String &p_url);
String get_avatar_url() const;
void set_status(Status p_status);
Status get_status() const;
bool is_online() const;
void set_custom_status(const String &p_status);
String get_custom_status() const;
void set_current_game(const String &p_game, const String &p_game_id = "");
String get_current_game() const;
String get_current_game_id() const;
void set_is_playing(bool p_playing);
bool get_is_playing() const;
void set_friendship_id(const String &p_id);
String get_friendship_id() const;
void set_friendship_since(const String &p_date);
String get_friendship_since() const;
void set_nickname(const String &p_nickname);
String get_nickname() const;
void set_is_favorite(bool p_favorite);
bool get_is_favorite() const;
void set_is_blocked(bool p_blocked);
bool get_is_blocked() const;
Dictionary to_dictionary() const;
void from_dictionary(const Dictionary &p_dict);
Friend();
~Friend();
};
class FriendSystem : public RefCounted {
GDCLASS(FriendSystem, RefCounted);
private:
AethexLauncher *launcher = nullptr;
Vector<Ref<Friend>> friends;
Vector<Ref<Friend>> pending_requests; // Incoming
Vector<Ref<Friend>> sent_requests; // Outgoing
Vector<Ref<Friend>> blocked;
void _parse_friends_response(const Dictionary &p_response);
protected:
static void _bind_methods();
public:
void set_launcher(AethexLauncher *p_launcher);
// Fetching
void fetch_friends();
void fetch_pending_requests();
void fetch_blocked();
// Friend management
Error send_friend_request(const String &p_user_id);
Error send_friend_request_by_username(const String &p_username);
Error accept_friend_request(const String &p_request_id);
Error decline_friend_request(const String &p_request_id);
Error remove_friend(const String &p_friend_id);
Error block_user(const String &p_user_id);
Error unblock_user(const String &p_user_id);
// Favorites/nicknames
Error set_friend_nickname(const String &p_friend_id, const String &p_nickname);
Error set_friend_favorite(const String &p_friend_id, bool p_favorite);
// Queries
TypedArray<Friend> get_friends() const;
TypedArray<Friend> get_online_friends() const;
TypedArray<Friend> get_friends_playing() const;
TypedArray<Friend> get_favorites() const;
TypedArray<Friend> get_pending_requests() const;
TypedArray<Friend> get_sent_requests() const;
TypedArray<Friend> get_blocked() const;
Ref<Friend> get_friend(const String &p_friend_id) const;
Ref<Friend> get_friend_by_username(const String &p_username) const;
bool is_friend(const String &p_user_id) const;
bool is_blocked_user(const String &p_user_id) const;
int get_friend_count() const;
int get_online_count() const;
int get_pending_count() const;
// User search
void search_users(const String &p_query);
FriendSystem();
~FriendSystem();
};
VARIANT_ENUM_CAST(Friend::Status);
#endif // FRIEND_SYSTEM_H

View file

@ -0,0 +1,623 @@
/**************************************************************************/
/* game_library.cpp */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#include "game_library.h"
#include "../aethex_launcher.h"
#include "core/io/json.h"
#include "core/io/dir_access.h"
#include "core/io/file_access.h"
#include "core/os/os.h"
// ============================================================================
// GameEntry
// ============================================================================
void GameEntry::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_id", "id"), &GameEntry::set_id);
ClassDB::bind_method(D_METHOD("get_id"), &GameEntry::get_id);
ClassDB::bind_method(D_METHOD("set_title", "title"), &GameEntry::set_title);
ClassDB::bind_method(D_METHOD("get_title"), &GameEntry::get_title);
ClassDB::bind_method(D_METHOD("set_description", "description"), &GameEntry::set_description);
ClassDB::bind_method(D_METHOD("get_description"), &GameEntry::get_description);
ClassDB::bind_method(D_METHOD("set_developer", "developer"), &GameEntry::set_developer);
ClassDB::bind_method(D_METHOD("get_developer"), &GameEntry::get_developer);
ClassDB::bind_method(D_METHOD("set_publisher", "publisher"), &GameEntry::set_publisher);
ClassDB::bind_method(D_METHOD("get_publisher"), &GameEntry::get_publisher);
ClassDB::bind_method(D_METHOD("set_cover_image", "path"), &GameEntry::set_cover_image);
ClassDB::bind_method(D_METHOD("get_cover_image"), &GameEntry::get_cover_image);
ClassDB::bind_method(D_METHOD("set_executable_path", "path"), &GameEntry::set_executable_path);
ClassDB::bind_method(D_METHOD("get_executable_path"), &GameEntry::get_executable_path);
ClassDB::bind_method(D_METHOD("set_install_path", "path"), &GameEntry::set_install_path);
ClassDB::bind_method(D_METHOD("get_install_path"), &GameEntry::get_install_path);
ClassDB::bind_method(D_METHOD("set_version", "version"), &GameEntry::set_version);
ClassDB::bind_method(D_METHOD("get_version"), &GameEntry::get_version);
ClassDB::bind_method(D_METHOD("set_status", "status"), &GameEntry::set_status);
ClassDB::bind_method(D_METHOD("get_status"), &GameEntry::get_status);
ClassDB::bind_method(D_METHOD("set_category", "category"), &GameEntry::set_category);
ClassDB::bind_method(D_METHOD("get_category"), &GameEntry::get_category);
ClassDB::bind_method(D_METHOD("set_download_progress", "progress"), &GameEntry::set_download_progress);
ClassDB::bind_method(D_METHOD("get_download_progress"), &GameEntry::get_download_progress);
ClassDB::bind_method(D_METHOD("get_size_formatted"), &GameEntry::get_size_formatted);
ClassDB::bind_method(D_METHOD("get_installed_size_formatted"), &GameEntry::get_installed_size_formatted);
ClassDB::bind_method(D_METHOD("set_last_played", "time"), &GameEntry::set_last_played);
ClassDB::bind_method(D_METHOD("get_last_played"), &GameEntry::get_last_played);
ClassDB::bind_method(D_METHOD("get_total_playtime_formatted"), &GameEntry::get_total_playtime_formatted);
ClassDB::bind_method(D_METHOD("add_playtime", "seconds"), &GameEntry::add_playtime);
ClassDB::bind_method(D_METHOD("set_rating", "rating"), &GameEntry::set_rating);
ClassDB::bind_method(D_METHOD("get_rating"), &GameEntry::get_rating);
ClassDB::bind_method(D_METHOD("is_installed"), &GameEntry::is_installed);
ClassDB::bind_method(D_METHOD("is_downloading"), &GameEntry::is_downloading);
ClassDB::bind_method(D_METHOD("has_update"), &GameEntry::has_update);
ClassDB::bind_method(D_METHOD("to_dictionary"), &GameEntry::to_dictionary);
ClassDB::bind_method(D_METHOD("from_dictionary", "dict"), &GameEntry::from_dictionary);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "id"), "set_id", "get_id");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "title"), "set_title", "get_title");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "description"), "set_description", "get_description");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "developer"), "set_developer", "get_developer");
ADD_PROPERTY(PropertyInfo(Variant::INT, "status"), "set_status", "get_status");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "category"), "set_category", "get_category");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "rating"), "set_rating", "get_rating");
BIND_ENUM_CONSTANT(STATUS_NOT_INSTALLED);
BIND_ENUM_CONSTANT(STATUS_DOWNLOADING);
BIND_ENUM_CONSTANT(STATUS_INSTALLING);
BIND_ENUM_CONSTANT(STATUS_INSTALLED);
BIND_ENUM_CONSTANT(STATUS_UPDATE_AVAILABLE);
BIND_ENUM_CONSTANT(STATUS_RUNNING);
}
GameEntry::GameEntry() {
}
GameEntry::~GameEntry() {
}
void GameEntry::set_id(const String &p_id) { id = p_id; }
String GameEntry::get_id() const { return id; }
void GameEntry::set_title(const String &p_title) { title = p_title; }
String GameEntry::get_title() const { return title; }
void GameEntry::set_description(const String &p_description) { description = p_description; }
String GameEntry::get_description() const { return description; }
void GameEntry::set_developer(const String &p_developer) { developer = p_developer; }
String GameEntry::get_developer() const { return developer; }
void GameEntry::set_publisher(const String &p_publisher) { publisher = p_publisher; }
String GameEntry::get_publisher() const { return publisher; }
void GameEntry::set_cover_image(const String &p_path) { cover_image = p_path; }
String GameEntry::get_cover_image() const { return cover_image; }
void GameEntry::set_background_image(const String &p_path) { background_image = p_path; }
String GameEntry::get_background_image() const { return background_image; }
void GameEntry::set_icon_path(const String &p_path) { icon_path = p_path; }
String GameEntry::get_icon_path() const { return icon_path; }
void GameEntry::set_executable_path(const String &p_path) { executable_path = p_path; }
String GameEntry::get_executable_path() const { return executable_path; }
void GameEntry::set_install_path(const String &p_path) { install_path = p_path; }
String GameEntry::get_install_path() const { return install_path; }
void GameEntry::set_version(const String &p_version) { version = p_version; }
String GameEntry::get_version() const { return version; }
void GameEntry::set_status(Status p_status) { status = p_status; }
GameEntry::Status GameEntry::get_status() const { return status; }
void GameEntry::set_category(const String &p_category) { category = p_category; }
String GameEntry::get_category() const { return category; }
void GameEntry::set_download_progress(float p_progress) { download_progress = CLAMP(p_progress, 0.0f, 100.0f); }
float GameEntry::get_download_progress() const { return download_progress; }
void GameEntry::set_size_bytes(int64_t p_size) { size_bytes = p_size; }
int64_t GameEntry::get_size_bytes() const { return size_bytes; }
String GameEntry::get_size_formatted() const {
if (size_bytes < 1024) return String::num_int64(size_bytes) + " B";
if (size_bytes < 1024 * 1024) return String::num(size_bytes / 1024.0, 1) + " KB";
if (size_bytes < 1024 * 1024 * 1024) return String::num(size_bytes / (1024.0 * 1024.0), 1) + " MB";
return String::num(size_bytes / (1024.0 * 1024.0 * 1024.0), 2) + " GB";
}
void GameEntry::set_installed_size_bytes(int64_t p_size) { installed_size_bytes = p_size; }
int64_t GameEntry::get_installed_size_bytes() const { return installed_size_bytes; }
String GameEntry::get_installed_size_formatted() const {
if (installed_size_bytes < 1024) return String::num_int64(installed_size_bytes) + " B";
if (installed_size_bytes < 1024 * 1024) return String::num(installed_size_bytes / 1024.0, 1) + " KB";
if (installed_size_bytes < 1024 * 1024 * 1024) return String::num(installed_size_bytes / (1024.0 * 1024.0), 1) + " MB";
return String::num(installed_size_bytes / (1024.0 * 1024.0 * 1024.0), 2) + " GB";
}
void GameEntry::set_last_played(const String &p_time) { last_played = p_time; }
String GameEntry::get_last_played() const { return last_played; }
void GameEntry::set_total_playtime_seconds(int64_t p_seconds) { total_playtime_seconds = p_seconds; }
int64_t GameEntry::get_total_playtime_seconds() const { return total_playtime_seconds; }
String GameEntry::get_total_playtime_formatted() const {
if (total_playtime_seconds < 60) return String::num_int64(total_playtime_seconds) + "s";
if (total_playtime_seconds < 3600) return String::num_int64(total_playtime_seconds / 60) + "m";
int64_t hours = total_playtime_seconds / 3600;
int64_t minutes = (total_playtime_seconds % 3600) / 60;
return String::num_int64(hours) + "h " + String::num_int64(minutes) + "m";
}
void GameEntry::add_playtime(int64_t p_seconds) { total_playtime_seconds += p_seconds; }
int64_t GameEntry::get_playtime_minutes() const { return total_playtime_seconds / 60; }
void GameEntry::set_rating(float p_rating) { rating = CLAMP(p_rating, 0.0f, 5.0f); }
float GameEntry::get_rating() const { return rating; }
void GameEntry::set_achievements(int p_total, int p_unlocked) {
achievements_total = p_total;
achievements_unlocked = MIN(p_unlocked, p_total);
}
int GameEntry::get_achievements_total() const { return achievements_total; }
int GameEntry::get_achievements_unlocked() const { return achievements_unlocked; }
void GameEntry::set_cloud_saves_enabled(bool p_enabled) { cloud_saves_enabled = p_enabled; }
bool GameEntry::is_cloud_saves_enabled() const { return cloud_saves_enabled; }
void GameEntry::set_cloud_saves_last_sync(const String &p_time) { cloud_saves_last_sync = p_time; }
String GameEntry::get_cloud_saves_last_sync() const { return cloud_saves_last_sync; }
void GameEntry::set_launch_arguments(const String &p_args) { launch_arguments = p_args; }
String GameEntry::get_launch_arguments() const { return launch_arguments; }
void GameEntry::set_custom_data(const Dictionary &p_data) { custom_data = p_data; }
Dictionary GameEntry::get_custom_data() const { return custom_data; }
bool GameEntry::is_installed() const { return status == STATUS_INSTALLED; }
bool GameEntry::is_downloading() const { return status == STATUS_DOWNLOADING; }
bool GameEntry::has_update() const { return status == STATUS_UPDATE_AVAILABLE; }
Dictionary GameEntry::to_dictionary() const {
Dictionary dict;
dict["id"] = id;
dict["title"] = title;
dict["description"] = description;
dict["developer"] = developer;
dict["publisher"] = publisher;
dict["cover_image"] = cover_image;
dict["background_image"] = background_image;
dict["icon_path"] = icon_path;
dict["executable_path"] = executable_path;
dict["install_path"] = install_path;
dict["version"] = version;
dict["status"] = status;
dict["category"] = category;
dict["size_bytes"] = size_bytes;
dict["installed_size_bytes"] = installed_size_bytes;
dict["last_played"] = last_played;
dict["total_playtime_seconds"] = total_playtime_seconds;
dict["rating"] = rating;
dict["achievements_total"] = achievements_total;
dict["achievements_unlocked"] = achievements_unlocked;
dict["cloud_saves_enabled"] = cloud_saves_enabled;
dict["cloud_saves_last_sync"] = cloud_saves_last_sync;
dict["launch_arguments"] = launch_arguments;
dict["custom_data"] = custom_data;
return dict;
}
void GameEntry::from_dictionary(const Dictionary &p_dict) {
id = p_dict.get("id", "");
title = p_dict.get("title", "");
description = p_dict.get("description", "");
developer = p_dict.get("developer", "");
publisher = p_dict.get("publisher", "");
cover_image = p_dict.get("cover_image", "");
background_image = p_dict.get("background_image", "");
icon_path = p_dict.get("icon_path", "");
executable_path = p_dict.get("executable_path", "");
install_path = p_dict.get("install_path", "");
version = p_dict.get("version", "");
status = p_dict.get("status", "not_installed");
category = p_dict.get("category", "Game");
size_bytes = p_dict.get("size_bytes", 0);
installed_size_bytes = p_dict.get("installed_size_bytes", 0);
last_played = p_dict.get("last_played", "");
total_playtime_seconds = p_dict.get("total_playtime_seconds", 0);
rating = p_dict.get("rating", 0.0f);
achievements_total = p_dict.get("achievements_total", 0);
achievements_unlocked = p_dict.get("achievements_unlocked", 0);
cloud_saves_enabled = p_dict.get("cloud_saves_enabled", false);
cloud_saves_last_sync = p_dict.get("cloud_saves_last_sync", "");
launch_arguments = p_dict.get("launch_arguments", "");
custom_data = p_dict.get("custom_data", Dictionary());
}
// ============================================================================
// GameLibrary
// ============================================================================
void GameLibrary::_bind_methods() {
ClassDB::bind_method(D_METHOD("load_library"), &GameLibrary::load_library);
ClassDB::bind_method(D_METHOD("save_library"), &GameLibrary::save_library);
ClassDB::bind_method(D_METHOD("refresh_from_server"), &GameLibrary::refresh_from_server);
ClassDB::bind_method(D_METHOD("add_game", "game"), &GameLibrary::add_game);
ClassDB::bind_method(D_METHOD("remove_game", "id"), &GameLibrary::remove_game);
ClassDB::bind_method(D_METHOD("get_game", "id"), &GameLibrary::get_game);
ClassDB::bind_method(D_METHOD("has_game", "id"), &GameLibrary::has_game);
ClassDB::bind_method(D_METHOD("get_all_games"), &GameLibrary::get_all_games);
ClassDB::bind_method(D_METHOD("get_installed_games"), &GameLibrary::get_installed_games);
ClassDB::bind_method(D_METHOD("get_games_by_category", "category"), &GameLibrary::get_games_by_category);
ClassDB::bind_method(D_METHOD("get_games_by_status", "status"), &GameLibrary::get_games_by_status);
ClassDB::bind_method(D_METHOD("search_games", "query"), &GameLibrary::search_games);
ClassDB::bind_method(D_METHOD("get_recently_played", "limit"), &GameLibrary::get_recently_played, DEFVAL(10));
ClassDB::bind_method(D_METHOD("get_game_count"), &GameLibrary::get_game_count);
ClassDB::bind_method(D_METHOD("get_installed_count"), &GameLibrary::get_installed_count);
ClassDB::bind_method(D_METHOD("scan_directory", "path"), &GameLibrary::scan_directory);
ClassDB::bind_method(D_METHOD("add_external_game", "executable_path", "title"), &GameLibrary::add_external_game, DEFVAL(""));
ADD_SIGNAL(MethodInfo("library_loaded"));
ADD_SIGNAL(MethodInfo("game_added", PropertyInfo(Variant::OBJECT, "game", PROPERTY_HINT_RESOURCE_TYPE, "GameEntry")));
ADD_SIGNAL(MethodInfo("game_removed", PropertyInfo(Variant::STRING, "id")));
ADD_SIGNAL(MethodInfo("game_updated", PropertyInfo(Variant::OBJECT, "game", PROPERTY_HINT_RESOURCE_TYPE, "GameEntry")));
}
GameLibrary::GameLibrary() {
library_path = OS::get_singleton()->get_user_data_dir() + "/library.json";
}
GameLibrary::~GameLibrary() {
save_library();
}
void GameLibrary::set_launcher(AethexLauncher *p_launcher) {
launcher = p_launcher;
}
void GameLibrary::_ensure_directories() {
String base_dir = library_path.get_base_dir();
Ref<DirAccess> dir = DirAccess::open(base_dir);
if (dir.is_null()) {
DirAccess::make_dir_recursive_absolute(base_dir);
}
}
void GameLibrary::load_library() {
_ensure_directories();
if (!FileAccess::exists(library_path)) {
// No library file - populate with demo games
_populate_demo_games();
emit_signal("library_loaded");
return;
}
Ref<FileAccess> file = FileAccess::open(library_path, FileAccess::READ);
if (file.is_null()) {
_populate_demo_games();
emit_signal("library_loaded");
return;
}
String content = file->get_as_text();
file->close();
Variant parsed = JSON::parse_string(content);
if (parsed.get_type() != Variant::DICTIONARY) {
emit_signal("library_loaded");
return;
}
Dictionary data = parsed;
Array games_array = data.get("games", Array());
games.clear();
for (int i = 0; i < games_array.size(); i++) {
Dictionary game_dict = games_array[i];
Ref<GameEntry> game;
game.instantiate();
game->from_dictionary(game_dict);
games.push_back(game);
}
// If library is empty, populate with demo games
if (games.is_empty()) {
_populate_demo_games();
}
emit_signal("library_loaded");
}
void GameLibrary::save_library() {
_ensure_directories();
Array games_array;
for (int i = 0; i < games.size(); i++) {
if (games[i].is_valid()) {
games_array.push_back(games[i]->to_dictionary());
}
}
Dictionary data;
data["games"] = games_array;
data["version"] = 1;
Ref<FileAccess> file = FileAccess::open(library_path, FileAccess::WRITE);
if (file.is_valid()) {
file->store_string(JSON::stringify(data, "\t"));
file->close();
}
}
void GameLibrary::refresh_from_server() {
// TODO: Fetch user's library from server
// This would merge server data with local data
}
void GameLibrary::_populate_demo_games() {
// Add demo games for development/testing
games.clear();
// Demo Game 1: AeThex Adventure
{
Ref<GameEntry> game;
game.instantiate();
game->set_id("aethex-adventure");
game->set_title("AeThex Adventure");
game->set_description("Embark on an epic journey through the AeThex multiverse. Explore procedurally generated worlds, battle fierce creatures, and uncover ancient secrets.");
game->set_developer("AeThex Studios");
game->set_publisher("AeThex Labs");
game->set_category("Adventure");
game->set_status(GameEntry::STATUS_INSTALLED);
game->set_version("1.0.0");
game->set_size_bytes(2500000000); // 2.5 GB
game->add_playtime(7200); // 2 hours played
games.push_back(game);
}
// Demo Game 2: Neon Racer
{
Ref<GameEntry> game;
game.instantiate();
game->set_id("neon-racer");
game->set_title("Neon Racer");
game->set_description("High-octane racing through cyberpunk cityscapes. Customize your ride and dominate the neon-lit streets.");
game->set_developer("Velocity Games");
game->set_publisher("AeThex Labs");
game->set_category("Racing");
game->set_status(GameEntry::STATUS_INSTALLED);
game->set_version("2.1.0");
game->set_size_bytes(4200000000); // 4.2 GB
game->add_playtime(14400); // 4 hours played
games.push_back(game);
}
// Demo Game 3: Stellar Colonies
{
Ref<GameEntry> game;
game.instantiate();
game->set_id("stellar-colonies");
game->set_title("Stellar Colonies");
game->set_description("Build and manage interstellar colonies across the galaxy. Balance resources and forge alliances.");
game->set_developer("Cosmic Forge");
game->set_publisher("AeThex Labs");
game->set_category("Strategy");
game->set_status(GameEntry::STATUS_NOT_INSTALLED);
game->set_version("1.5.2");
game->set_size_bytes(8000000000); // 8 GB
games.push_back(game);
}
// Demo Game 4: Pixel Dungeon
{
Ref<GameEntry> game;
game.instantiate();
game->set_id("pixel-dungeon");
game->set_title("Pixel Dungeon Quest");
game->set_description("A charming roguelike adventure with procedurally generated dungeons.");
game->set_developer("Retro Pixel Studios");
game->set_publisher("AeThex Labs");
game->set_category("Roguelike");
game->set_status(GameEntry::STATUS_UPDATE_AVAILABLE);
game->set_version("2.9.0");
game->set_size_bytes(500000000); // 500 MB
game->add_playtime(36000); // 10 hours played
games.push_back(game);
}
save_library();
}
void GameLibrary::add_game(const Ref<GameEntry> &p_game) {
if (!p_game.is_valid()) return;
// Check for duplicates
for (int i = 0; i < games.size(); i++) {
if (games[i].is_valid() && games[i]->get_id() == p_game->get_id()) {
// Update existing
games.write[i] = p_game;
emit_signal("game_updated", p_game);
save_library();
return;
}
}
games.push_back(p_game);
emit_signal("game_added", p_game);
save_library();
}
void GameLibrary::remove_game(const String &p_id) {
for (int i = 0; i < games.size(); i++) {
if (games[i].is_valid() && games[i]->get_id() == p_id) {
games.remove_at(i);
emit_signal("game_removed", p_id);
save_library();
return;
}
}
}
Ref<GameEntry> GameLibrary::get_game(const String &p_id) const {
for (int i = 0; i < games.size(); i++) {
if (games[i].is_valid() && games[i]->get_id() == p_id) {
return games[i];
}
}
return Ref<GameEntry>();
}
bool GameLibrary::has_game(const String &p_id) const {
return get_game(p_id).is_valid();
}
TypedArray<GameEntry> GameLibrary::get_all_games() const {
TypedArray<GameEntry> result;
for (int i = 0; i < games.size(); i++) {
if (games[i].is_valid()) {
result.push_back(games[i]);
}
}
return result;
}
TypedArray<GameEntry> GameLibrary::get_installed_games() const {
TypedArray<GameEntry> result;
for (int i = 0; i < games.size(); i++) {
if (games[i].is_valid() && games[i]->is_installed()) {
result.push_back(games[i]);
}
}
return result;
}
TypedArray<GameEntry> GameLibrary::get_games_by_category(const String &p_category) const {
TypedArray<GameEntry> result;
for (int i = 0; i < games.size(); i++) {
if (games[i].is_valid() && games[i]->get_category() == p_category) {
result.push_back(games[i]);
}
}
return result;
}
TypedArray<GameEntry> GameLibrary::get_games_by_status(GameEntry::Status p_status) const {
TypedArray<GameEntry> result;
for (int i = 0; i < games.size(); i++) {
if (games[i].is_valid() && games[i]->get_status() == p_status) {
result.push_back(games[i]);
}
}
return result;
}
TypedArray<GameEntry> GameLibrary::search_games(const String &p_query) const {
TypedArray<GameEntry> result;
String query_lower = p_query.to_lower();
for (int i = 0; i < games.size(); i++) {
if (!games[i].is_valid()) continue;
if (games[i]->get_title().to_lower().contains(query_lower) ||
games[i]->get_developer().to_lower().contains(query_lower) ||
games[i]->get_description().to_lower().contains(query_lower)) {
result.push_back(games[i]);
}
}
return result;
}
TypedArray<GameEntry> GameLibrary::get_recently_played(int p_limit) const {
// Sort by last_played and return top N
Vector<Ref<GameEntry>> sorted;
for (int i = 0; i < games.size(); i++) {
if (games[i].is_valid() && !games[i]->get_last_played().is_empty()) {
sorted.push_back(games[i]);
}
}
// Sort (simple bubble sort for now)
for (int i = 0; i < sorted.size() - 1; i++) {
for (int j = 0; j < sorted.size() - i - 1; j++) {
if (sorted[j]->get_last_played() < sorted[j + 1]->get_last_played()) {
Ref<GameEntry> temp = sorted[j];
sorted.write[j] = sorted[j + 1];
sorted.write[j + 1] = temp;
}
}
}
TypedArray<GameEntry> result;
for (int i = 0; i < MIN(p_limit, sorted.size()); i++) {
result.push_back(sorted[i]);
}
return result;
}
int GameLibrary::get_game_count() const {
return games.size();
}
int GameLibrary::get_installed_count() const {
int count = 0;
for (int i = 0; i < games.size(); i++) {
if (games[i].is_valid() && games[i]->is_installed()) {
count++;
}
}
return count;
}
void GameLibrary::scan_directory(const String &p_path) {
Ref<DirAccess> dir = DirAccess::open(p_path);
if (dir.is_null()) return;
dir->list_dir_begin();
String file_name = dir->get_next();
while (!file_name.is_empty()) {
if (!dir->current_is_dir()) {
String ext = file_name.get_extension().to_lower();
if (ext == "exe" || ext == "x86_64" || ext == "") {
String full_path = p_path + "/" + file_name;
add_external_game(full_path, file_name.get_basename());
}
}
file_name = dir->get_next();
}
dir->list_dir_end();
}
void GameLibrary::add_external_game(const String &p_executable_path, const String &p_title) {
Ref<GameEntry> game;
game.instantiate();
String title = p_title.is_empty() ? p_executable_path.get_file().get_basename() : p_title;
String id = "external_" + title.to_lower().replace(" ", "_");
game->set_id(id);
game->set_title(title);
game->set_executable_path(p_executable_path);
game->set_install_path(p_executable_path.get_base_dir());
game->set_status(GameEntry::STATUS_INSTALLED);
game->set_category("External");
add_game(game);
}

View file

@ -0,0 +1,213 @@
/**************************************************************************/
/* game_library.h */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifndef GAME_LIBRARY_H
#define GAME_LIBRARY_H
#include "core/object/ref_counted.h"
#include "core/io/config_file.h"
#include "core/variant/typed_array.h"
class AethexLauncher;
class GameEntry : public RefCounted {
GDCLASS(GameEntry, RefCounted);
public:
enum Status {
STATUS_NOT_INSTALLED,
STATUS_DOWNLOADING,
STATUS_INSTALLING,
STATUS_INSTALLED,
STATUS_UPDATE_AVAILABLE,
STATUS_RUNNING
};
private:
String id;
String title;
String description;
String developer;
String publisher;
String cover_image;
String background_image;
String icon_path;
String executable_path;
String install_path;
String version;
Status status = STATUS_NOT_INSTALLED;
String category; // Game, Tool, Creative, Development
float download_progress = 0.0f;
int64_t size_bytes = 0;
int64_t installed_size_bytes = 0;
String last_played;
int64_t total_playtime_seconds = 0;
float rating = 0.0f;
int achievements_total = 0;
int achievements_unlocked = 0;
bool cloud_saves_enabled = false;
String cloud_saves_last_sync;
String launch_arguments;
Dictionary custom_data;
protected:
static void _bind_methods();
public:
// Basic properties
void set_id(const String &p_id);
String get_id() const;
void set_title(const String &p_title);
String get_title() const;
void set_description(const String &p_description);
String get_description() const;
void set_developer(const String &p_developer);
String get_developer() const;
void set_publisher(const String &p_publisher);
String get_publisher() const;
void set_cover_image(const String &p_path);
String get_cover_image() const;
void set_background_image(const String &p_path);
String get_background_image() const;
void set_icon_path(const String &p_path);
String get_icon_path() const;
void set_executable_path(const String &p_path);
String get_executable_path() const;
void set_install_path(const String &p_path);
String get_install_path() const;
void set_version(const String &p_version);
String get_version() const;
void set_status(Status p_status);
Status get_status() const;
void set_category(const String &p_category);
String get_category() const;
// Download/size
void set_download_progress(float p_progress);
float get_download_progress() const;
void set_size_bytes(int64_t p_size);
int64_t get_size_bytes() const;
String get_size_formatted() const;
void set_installed_size_bytes(int64_t p_size);
int64_t get_installed_size_bytes() const;
String get_installed_size_formatted() const;
// Playtime
void set_last_played(const String &p_time);
String get_last_played() const;
void set_total_playtime_seconds(int64_t p_seconds);
int64_t get_total_playtime_seconds() const;
int64_t get_playtime_minutes() const;
String get_total_playtime_formatted() const;
void add_playtime(int64_t p_seconds);
// Stats
void set_rating(float p_rating);
float get_rating() const;
void set_achievements(int p_total, int p_unlocked);
int get_achievements_total() const;
int get_achievements_unlocked() const;
// Cloud saves
void set_cloud_saves_enabled(bool p_enabled);
bool is_cloud_saves_enabled() const;
void set_cloud_saves_last_sync(const String &p_time);
String get_cloud_saves_last_sync() const;
// Launch
void set_launch_arguments(const String &p_args);
String get_launch_arguments() const;
// Custom data
void set_custom_data(const Dictionary &p_data);
Dictionary get_custom_data() const;
// Serialization
Dictionary to_dictionary() const;
void from_dictionary(const Dictionary &p_dict);
// Status helpers
bool is_installed() const;
bool is_downloading() const;
bool has_update() const;
GameEntry();
~GameEntry();
};
class GameLibrary : public RefCounted {
GDCLASS(GameLibrary, RefCounted);
private:
AethexLauncher *launcher = nullptr;
Vector<Ref<GameEntry>> games;
String library_path;
void _ensure_directories();
void _populate_demo_games();
protected:
static void _bind_methods();
public:
void set_launcher(AethexLauncher *p_launcher);
// Library management
void load_library();
void save_library();
void refresh_from_server();
// Game management
void add_game(const Ref<GameEntry> &p_game);
void remove_game(const String &p_id);
Ref<GameEntry> get_game(const String &p_id) const;
bool has_game(const String &p_id) const;
// Queries
TypedArray<GameEntry> get_all_games() const;
TypedArray<GameEntry> get_installed_games() const;
TypedArray<GameEntry> get_games_by_category(const String &p_category) const;
TypedArray<GameEntry> get_games_by_status(GameEntry::Status p_status) const;
TypedArray<GameEntry> search_games(const String &p_query) const;
TypedArray<GameEntry> get_recently_played(int p_limit = 10) const;
int get_game_count() const;
int get_installed_count() const;
// Scanning
void scan_directory(const String &p_path);
void add_external_game(const String &p_executable_path, const String &p_title = "");
GameLibrary();
~GameLibrary();
};
VARIANT_ENUM_CAST(GameEntry::Status);
#endif // GAME_LIBRARY_H

View file

@ -0,0 +1,299 @@
/**************************************************************************/
/* launcher_profile.cpp */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#include "launcher_profile.h"
void LauncherProfile::_bind_methods() {
// Identity
ClassDB::bind_method(D_METHOD("get_id"), &LauncherProfile::get_id);
ClassDB::bind_method(D_METHOD("get_user_id"), &LauncherProfile::get_user_id);
ClassDB::bind_method(D_METHOD("set_display_name", "name"), &LauncherProfile::set_display_name);
ClassDB::bind_method(D_METHOD("get_display_name"), &LauncherProfile::get_display_name);
ClassDB::bind_method(D_METHOD("set_bio", "bio"), &LauncherProfile::set_bio);
ClassDB::bind_method(D_METHOD("get_bio"), &LauncherProfile::get_bio);
ClassDB::bind_method(D_METHOD("get_avatar_url"), &LauncherProfile::get_avatar_url);
ClassDB::bind_method(D_METHOD("get_banner_url"), &LauncherProfile::get_banner_url);
// Status
ClassDB::bind_method(D_METHOD("set_status", "status"), &LauncherProfile::set_status);
ClassDB::bind_method(D_METHOD("get_status"), &LauncherProfile::get_status);
ClassDB::bind_method(D_METHOD("set_custom_status", "status"), &LauncherProfile::set_custom_status);
ClassDB::bind_method(D_METHOD("get_custom_status"), &LauncherProfile::get_custom_status);
// Personal
ClassDB::bind_method(D_METHOD("set_pronouns", "pronouns"), &LauncherProfile::set_pronouns);
ClassDB::bind_method(D_METHOD("get_pronouns"), &LauncherProfile::get_pronouns);
ClassDB::bind_method(D_METHOD("set_location", "location"), &LauncherProfile::set_location);
ClassDB::bind_method(D_METHOD("get_location"), &LauncherProfile::get_location);
ClassDB::bind_method(D_METHOD("set_website", "website"), &LauncherProfile::set_website);
ClassDB::bind_method(D_METHOD("get_website"), &LauncherProfile::get_website);
// Social
ClassDB::bind_method(D_METHOD("get_discord_username"), &LauncherProfile::get_discord_username);
ClassDB::bind_method(D_METHOD("get_twitter_handle"), &LauncherProfile::get_twitter_handle);
ClassDB::bind_method(D_METHOD("get_github_username"), &LauncherProfile::get_github_username);
ClassDB::bind_method(D_METHOD("get_twitch_username"), &LauncherProfile::get_twitch_username);
ClassDB::bind_method(D_METHOD("get_youtube_channel"), &LauncherProfile::get_youtube_channel);
// Privacy
ClassDB::bind_method(D_METHOD("get_profile_visibility"), &LauncherProfile::get_profile_visibility);
ClassDB::bind_method(D_METHOD("get_show_online_status"), &LauncherProfile::get_show_online_status);
ClassDB::bind_method(D_METHOD("get_show_activity_status"), &LauncherProfile::get_show_activity_status);
ClassDB::bind_method(D_METHOD("get_show_game_activity"), &LauncherProfile::get_show_game_activity);
ClassDB::bind_method(D_METHOD("get_allow_friend_requests"), &LauncherProfile::get_allow_friend_requests);
// Stats
ClassDB::bind_method(D_METHOD("get_total_playtime"), &LauncherProfile::get_total_playtime);
ClassDB::bind_method(D_METHOD("get_total_playtime_formatted"), &LauncherProfile::get_total_playtime_formatted);
ClassDB::bind_method(D_METHOD("get_games_owned"), &LauncherProfile::get_games_owned);
ClassDB::bind_method(D_METHOD("get_achievements_unlocked"), &LauncherProfile::get_achievements_unlocked);
// Level
ClassDB::bind_method(D_METHOD("get_level"), &LauncherProfile::get_level);
ClassDB::bind_method(D_METHOD("get_xp"), &LauncherProfile::get_xp);
ClassDB::bind_method(D_METHOD("add_xp", "xp"), &LauncherProfile::add_xp);
ClassDB::bind_method(D_METHOD("get_level_progress"), &LauncherProfile::get_level_progress);
// Customization
ClassDB::bind_method(D_METHOD("get_theme_accent"), &LauncherProfile::get_theme_accent);
ClassDB::bind_method(D_METHOD("get_featured_badge"), &LauncherProfile::get_featured_badge);
// Serialization
ClassDB::bind_method(D_METHOD("to_dictionary"), &LauncherProfile::to_dictionary);
ClassDB::bind_method(D_METHOD("from_dictionary", "dict"), &LauncherProfile::from_dictionary);
ClassDB::bind_method(D_METHOD("clear"), &LauncherProfile::clear);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "display_name"), "set_display_name", "get_display_name");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "bio"), "set_bio", "get_bio");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "status"), "set_status", "get_status");
ADD_PROPERTY(PropertyInfo(Variant::INT, "level"), "", "get_level");
ADD_PROPERTY(PropertyInfo(Variant::INT, "xp"), "", "get_xp");
}
LauncherProfile::LauncherProfile() {}
LauncherProfile::~LauncherProfile() {}
// Identity
void LauncherProfile::set_id(const String &p_id) { id = p_id; }
String LauncherProfile::get_id() const { return id; }
void LauncherProfile::set_user_id(const String &p_id) { user_id = p_id; }
String LauncherProfile::get_user_id() const { return user_id; }
void LauncherProfile::set_display_name(const String &p_name) { display_name = p_name; }
String LauncherProfile::get_display_name() const { return display_name; }
void LauncherProfile::set_bio(const String &p_bio) { bio = p_bio; }
String LauncherProfile::get_bio() const { return bio; }
void LauncherProfile::set_avatar_url(const String &p_url) { avatar_url = p_url; }
String LauncherProfile::get_avatar_url() const { return avatar_url; }
void LauncherProfile::set_banner_url(const String &p_url) { banner_url = p_url; }
String LauncherProfile::get_banner_url() const { return banner_url; }
// Status
void LauncherProfile::set_status(const String &p_status) { status = p_status; }
String LauncherProfile::get_status() const { return status; }
void LauncherProfile::set_custom_status(const String &p_status) { custom_status = p_status; }
String LauncherProfile::get_custom_status() const { return custom_status; }
// Personal
void LauncherProfile::set_pronouns(const String &p_pronouns) { pronouns = p_pronouns; }
String LauncherProfile::get_pronouns() const { return pronouns; }
void LauncherProfile::set_location(const String &p_location) { location = p_location; }
String LauncherProfile::get_location() const { return location; }
void LauncherProfile::set_website(const String &p_website) { website = p_website; }
String LauncherProfile::get_website() const { return website; }
// Social
void LauncherProfile::set_discord_username(const String &p_username) { discord_username = p_username; }
String LauncherProfile::get_discord_username() const { return discord_username; }
void LauncherProfile::set_twitter_handle(const String &p_handle) { twitter_handle = p_handle; }
String LauncherProfile::get_twitter_handle() const { return twitter_handle; }
void LauncherProfile::set_github_username(const String &p_username) { github_username = p_username; }
String LauncherProfile::get_github_username() const { return github_username; }
void LauncherProfile::set_twitch_username(const String &p_username) { twitch_username = p_username; }
String LauncherProfile::get_twitch_username() const { return twitch_username; }
void LauncherProfile::set_youtube_channel(const String &p_channel) { youtube_channel = p_channel; }
String LauncherProfile::get_youtube_channel() const { return youtube_channel; }
// Privacy
void LauncherProfile::set_profile_visibility(const String &p_visibility) { profile_visibility = p_visibility; }
String LauncherProfile::get_profile_visibility() const { return profile_visibility; }
void LauncherProfile::set_show_online_status(bool p_show) { show_online_status = p_show; }
bool LauncherProfile::get_show_online_status() const { return show_online_status; }
void LauncherProfile::set_show_activity_status(bool p_show) { show_activity_status = p_show; }
bool LauncherProfile::get_show_activity_status() const { return show_activity_status; }
void LauncherProfile::set_show_game_activity(bool p_show) { show_game_activity = p_show; }
bool LauncherProfile::get_show_game_activity() const { return show_game_activity; }
void LauncherProfile::set_allow_friend_requests(bool p_allow) { allow_friend_requests = p_allow; }
bool LauncherProfile::get_allow_friend_requests() const { return allow_friend_requests; }
// Stats
void LauncherProfile::set_total_playtime(int64_t p_seconds) { total_playtime = p_seconds; }
int64_t LauncherProfile::get_total_playtime() const { return total_playtime; }
String LauncherProfile::get_total_playtime_formatted() const {
if (total_playtime < 60) return String::num_int64(total_playtime) + "s";
if (total_playtime < 3600) return String::num_int64(total_playtime / 60) + "m";
int64_t hours = total_playtime / 3600;
return String::num_int64(hours) + "h";
}
void LauncherProfile::set_games_owned(int p_count) { games_owned = p_count; }
int LauncherProfile::get_games_owned() const { return games_owned; }
void LauncherProfile::set_achievements_unlocked(int p_count) { achievements_unlocked = p_count; }
int LauncherProfile::get_achievements_unlocked() const { return achievements_unlocked; }
// Level
void LauncherProfile::set_level(int p_level) { level = MAX(1, p_level); }
int LauncherProfile::get_level() const { return level; }
void LauncherProfile::set_xp(int p_xp) { xp = MAX(0, p_xp); }
int LauncherProfile::get_xp() const { return xp; }
void LauncherProfile::add_xp(int p_xp) {
xp += p_xp;
while (xp >= xp_to_next_level) {
xp -= xp_to_next_level;
level++;
xp_to_next_level = level * 100; // Simple formula
}
}
float LauncherProfile::get_level_progress() const {
if (xp_to_next_level <= 0) return 0.0f;
return (float)xp / (float)xp_to_next_level;
}
// Customization
void LauncherProfile::set_theme_accent(const String &p_color) { theme_accent = p_color; }
String LauncherProfile::get_theme_accent() const { return theme_accent; }
void LauncherProfile::set_featured_badge(const String &p_badge) { featured_badge = p_badge; }
String LauncherProfile::get_featured_badge() const { return featured_badge; }
void LauncherProfile::set_showcase_items(const Dictionary &p_items) { showcase_items = p_items; }
Dictionary LauncherProfile::get_showcase_items() const { return showcase_items; }
// Timestamps
String LauncherProfile::get_created_at() const { return created_at; }
String LauncherProfile::get_updated_at() const { return updated_at; }
// Serialization
Dictionary LauncherProfile::to_dictionary() const {
Dictionary dict;
dict["id"] = id;
dict["user_id"] = user_id;
dict["display_name"] = display_name;
dict["bio"] = bio;
dict["avatar_url"] = avatar_url;
dict["banner_url"] = banner_url;
dict["status"] = status;
dict["custom_status"] = custom_status;
dict["pronouns"] = pronouns;
dict["location"] = location;
dict["website"] = website;
dict["discord_username"] = discord_username;
dict["twitter_handle"] = twitter_handle;
dict["github_username"] = github_username;
dict["twitch_username"] = twitch_username;
dict["youtube_channel"] = youtube_channel;
dict["profile_visibility"] = profile_visibility;
dict["show_online_status"] = show_online_status;
dict["show_activity_status"] = show_activity_status;
dict["show_game_activity"] = show_game_activity;
dict["allow_friend_requests"] = allow_friend_requests;
dict["total_playtime"] = total_playtime;
dict["games_owned"] = games_owned;
dict["achievements_unlocked"] = achievements_unlocked;
dict["level"] = level;
dict["xp"] = xp;
dict["theme_accent"] = theme_accent;
dict["featured_badge"] = featured_badge;
dict["showcase_items"] = showcase_items;
return dict;
}
void LauncherProfile::from_dictionary(const Dictionary &p_dict) {
id = p_dict.get("id", "");
user_id = p_dict.get("user_id", "");
display_name = p_dict.get("display_name", "");
bio = p_dict.get("bio", "");
avatar_url = p_dict.get("avatar_url", "");
banner_url = p_dict.get("banner_url", "");
status = p_dict.get("status", "offline");
custom_status = p_dict.get("custom_status", "");
pronouns = p_dict.get("pronouns", "");
location = p_dict.get("location", "");
website = p_dict.get("website", "");
discord_username = p_dict.get("discord_username", "");
twitter_handle = p_dict.get("twitter_handle", "");
github_username = p_dict.get("github_username", "");
twitch_username = p_dict.get("twitch_username", "");
youtube_channel = p_dict.get("youtube_channel", "");
profile_visibility = p_dict.get("profile_visibility", "public");
show_online_status = p_dict.get("show_online_status", true);
show_activity_status = p_dict.get("show_activity_status", true);
show_game_activity = p_dict.get("show_game_activity", true);
allow_friend_requests = p_dict.get("allow_friend_requests", true);
total_playtime = p_dict.get("total_playtime", 0);
games_owned = p_dict.get("games_owned", 0);
achievements_unlocked = p_dict.get("achievements_unlocked", 0);
level = p_dict.get("level", 1);
xp = p_dict.get("xp", 0);
theme_accent = p_dict.get("theme_accent", "");
featured_badge = p_dict.get("featured_badge", "");
showcase_items = p_dict.get("showcase_items", Dictionary());
created_at = p_dict.get("created_at", "");
updated_at = p_dict.get("updated_at", "");
}
void LauncherProfile::clear() {
id = "";
user_id = "";
display_name = "";
bio = "";
avatar_url = "";
banner_url = "";
status = "offline";
custom_status = "";
pronouns = "";
location = "";
website = "";
discord_username = "";
twitter_handle = "";
github_username = "";
twitch_username = "";
youtube_channel = "";
profile_visibility = "public";
show_online_status = true;
show_activity_status = true;
show_game_activity = true;
allow_friend_requests = true;
total_playtime = 0;
games_owned = 0;
achievements_unlocked = 0;
level = 1;
xp = 0;
theme_accent = "";
featured_badge = "";
showcase_items.clear();
}

View file

@ -0,0 +1,179 @@
/**************************************************************************/
/* launcher_profile.h */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifndef LAUNCHER_PROFILE_H
#define LAUNCHER_PROFILE_H
#include "core/object/ref_counted.h"
class LauncherProfile : public RefCounted {
GDCLASS(LauncherProfile, RefCounted);
private:
String id;
String user_id;
String display_name;
String bio;
String avatar_url;
String banner_url;
String status; // online, away, busy, invisible, offline
String custom_status;
String pronouns;
String location;
String website;
// Social links
String discord_username;
String twitter_handle;
String github_username;
String twitch_username;
String youtube_channel;
// Privacy
String profile_visibility; // public, friends, private
bool show_online_status = true;
bool show_activity_status = true;
bool show_game_activity = true;
bool allow_friend_requests = true;
// Stats
int64_t total_playtime = 0;
int games_owned = 0;
int achievements_unlocked = 0;
// Level/XP
int level = 1;
int xp = 0;
int xp_to_next_level = 100;
// Customization
String theme_accent;
String featured_badge;
Dictionary showcase_items;
String created_at;
String updated_at;
protected:
static void _bind_methods();
public:
// Identity
void set_id(const String &p_id);
String get_id() const;
void set_user_id(const String &p_id);
String get_user_id() const;
void set_display_name(const String &p_name);
String get_display_name() const;
void set_bio(const String &p_bio);
String get_bio() const;
void set_avatar_url(const String &p_url);
String get_avatar_url() const;
void set_banner_url(const String &p_url);
String get_banner_url() const;
// Status
void set_status(const String &p_status);
String get_status() const;
void set_custom_status(const String &p_status);
String get_custom_status() const;
// Personal info
void set_pronouns(const String &p_pronouns);
String get_pronouns() const;
void set_location(const String &p_location);
String get_location() const;
void set_website(const String &p_website);
String get_website() const;
// Social links
void set_discord_username(const String &p_username);
String get_discord_username() const;
void set_twitter_handle(const String &p_handle);
String get_twitter_handle() const;
void set_github_username(const String &p_username);
String get_github_username() const;
void set_twitch_username(const String &p_username);
String get_twitch_username() const;
void set_youtube_channel(const String &p_channel);
String get_youtube_channel() const;
// Privacy settings
void set_profile_visibility(const String &p_visibility);
String get_profile_visibility() const;
void set_show_online_status(bool p_show);
bool get_show_online_status() const;
void set_show_activity_status(bool p_show);
bool get_show_activity_status() const;
void set_show_game_activity(bool p_show);
bool get_show_game_activity() const;
void set_allow_friend_requests(bool p_allow);
bool get_allow_friend_requests() const;
// Stats
void set_total_playtime(int64_t p_seconds);
int64_t get_total_playtime() const;
String get_total_playtime_formatted() const;
void set_games_owned(int p_count);
int get_games_owned() const;
void set_achievements_unlocked(int p_count);
int get_achievements_unlocked() const;
// Level
void set_level(int p_level);
int get_level() const;
void set_xp(int p_xp);
int get_xp() const;
void add_xp(int p_xp);
float get_level_progress() const;
// Customization
void set_theme_accent(const String &p_color);
String get_theme_accent() const;
void set_featured_badge(const String &p_badge);
String get_featured_badge() const;
void set_showcase_items(const Dictionary &p_items);
Dictionary get_showcase_items() const;
// Timestamps
String get_created_at() const;
String get_updated_at() const;
// Serialization
Dictionary to_dictionary() const;
void from_dictionary(const Dictionary &p_dict);
void clear();
LauncherProfile();
~LauncherProfile();
};
#endif // LAUNCHER_PROFILE_H

View file

@ -0,0 +1,684 @@
/**************************************************************************/
/* launcher_store.cpp */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#include "launcher_store.h"
#include "../aethex_launcher.h"
#include "core/io/json.h"
#include "core/os/os.h"
// ============================================================================
// StoreItem
// ============================================================================
void StoreItem::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_id"), &StoreItem::get_id);
ClassDB::bind_method(D_METHOD("get_title"), &StoreItem::get_title);
ClassDB::bind_method(D_METHOD("get_description"), &StoreItem::get_description);
ClassDB::bind_method(D_METHOD("get_short_description"), &StoreItem::get_short_description);
ClassDB::bind_method(D_METHOD("get_developer"), &StoreItem::get_developer);
ClassDB::bind_method(D_METHOD("get_publisher"), &StoreItem::get_publisher);
ClassDB::bind_method(D_METHOD("get_cover_image"), &StoreItem::get_cover_image);
ClassDB::bind_method(D_METHOD("get_category"), &StoreItem::get_category);
ClassDB::bind_method(D_METHOD("get_tags"), &StoreItem::get_tags);
ClassDB::bind_method(D_METHOD("has_tag", "tag"), &StoreItem::has_tag);
ClassDB::bind_method(D_METHOD("get_price"), &StoreItem::get_price);
ClassDB::bind_method(D_METHOD("get_sale_price"), &StoreItem::get_sale_price);
ClassDB::bind_method(D_METHOD("get_sale_percentage"), &StoreItem::get_sale_percentage);
ClassDB::bind_method(D_METHOD("get_current_price"), &StoreItem::get_current_price);
ClassDB::bind_method(D_METHOD("get_price_formatted"), &StoreItem::get_price_formatted);
ClassDB::bind_method(D_METHOD("is_on_sale"), &StoreItem::is_on_sale);
ClassDB::bind_method(D_METHOD("get_is_free"), &StoreItem::get_is_free);
ClassDB::bind_method(D_METHOD("get_rating"), &StoreItem::get_rating);
ClassDB::bind_method(D_METHOD("get_review_count"), &StoreItem::get_review_count);
ClassDB::bind_method(D_METHOD("get_review_percentage"), &StoreItem::get_review_percentage);
ClassDB::bind_method(D_METHOD("get_review_summary"), &StoreItem::get_review_summary);
ClassDB::bind_method(D_METHOD("get_download_url"), &StoreItem::get_download_url);
ClassDB::bind_method(D_METHOD("get_download_size_formatted"), &StoreItem::get_download_size_formatted);
ClassDB::bind_method(D_METHOD("get_version"), &StoreItem::get_version);
ClassDB::bind_method(D_METHOD("supports_platform", "platform"), &StoreItem::supports_platform);
ClassDB::bind_method(D_METHOD("get_supports_cloud_saves"), &StoreItem::get_supports_cloud_saves);
ClassDB::bind_method(D_METHOD("get_supports_achievements"), &StoreItem::get_supports_achievements);
ClassDB::bind_method(D_METHOD("get_supports_controller"), &StoreItem::get_supports_controller);
ClassDB::bind_method(D_METHOD("get_supports_multiplayer"), &StoreItem::get_supports_multiplayer);
ClassDB::bind_method(D_METHOD("to_dictionary"), &StoreItem::to_dictionary);
ClassDB::bind_method(D_METHOD("from_dictionary", "dict"), &StoreItem::from_dictionary);
}
StoreItem::StoreItem() {}
StoreItem::~StoreItem() {}
void StoreItem::set_id(const String &p_id) { id = p_id; }
String StoreItem::get_id() const { return id; }
void StoreItem::set_title(const String &p_title) { title = p_title; }
String StoreItem::get_title() const { return title; }
void StoreItem::set_description(const String &p_description) { description = p_description; }
String StoreItem::get_description() const { return description; }
void StoreItem::set_short_description(const String &p_description) { short_description = p_description; }
String StoreItem::get_short_description() const { return short_description; }
void StoreItem::set_developer(const String &p_developer) { developer = p_developer; }
String StoreItem::get_developer() const { return developer; }
void StoreItem::set_publisher(const String &p_publisher) { publisher = p_publisher; }
String StoreItem::get_publisher() const { return publisher; }
void StoreItem::set_cover_image(const String &p_url) { cover_image = p_url; }
String StoreItem::get_cover_image() const { return cover_image; }
void StoreItem::set_background_image(const String &p_url) { background_image = p_url; }
String StoreItem::get_background_image() const { return background_image; }
void StoreItem::set_screenshots(const PackedStringArray &p_urls) { screenshots = p_urls; }
PackedStringArray StoreItem::get_screenshots() const { return screenshots; }
void StoreItem::set_category(const String &p_category) { category = p_category; }
String StoreItem::get_category() const { return category; }
void StoreItem::set_tags(const PackedStringArray &p_tags) { tags = p_tags; }
PackedStringArray StoreItem::get_tags() const { return tags; }
bool StoreItem::has_tag(const String &p_tag) const {
for (int i = 0; i < tags.size(); i++) {
if (tags[i].to_lower() == p_tag.to_lower()) return true;
}
return false;
}
void StoreItem::set_genres(const PackedStringArray &p_genres) { genres = p_genres; }
PackedStringArray StoreItem::get_genres() const { return genres; }
void StoreItem::set_price(float p_price) { price = p_price; }
float StoreItem::get_price() const { return price; }
void StoreItem::set_sale_price(float p_price) { sale_price = p_price; }
float StoreItem::get_sale_price() const { return sale_price; }
void StoreItem::set_sale_percentage(int p_percentage) { sale_percentage = CLAMP(p_percentage, 0, 100); }
int StoreItem::get_sale_percentage() const { return sale_percentage; }
float StoreItem::get_discount_percent() const { return (float)sale_percentage; }
float StoreItem::get_current_price() const {
return is_on_sale() ? sale_price : price;
}
String StoreItem::get_price_formatted() const {
if (is_free) return "Free";
float current = get_current_price();
return "$" + String::num(current, 2);
}
bool StoreItem::is_on_sale() const {
return sale_percentage > 0 && sale_price < price;
}
void StoreItem::set_is_free(bool p_free) { is_free = p_free; }
bool StoreItem::get_is_free() const { return is_free; }
void StoreItem::set_release_date(const String &p_date) { release_date = p_date; }
String StoreItem::get_release_date() const { return release_date; }
void StoreItem::set_is_early_access(bool p_early) { is_early_access = p_early; }
bool StoreItem::get_is_early_access() const { return is_early_access; }
void StoreItem::set_is_coming_soon(bool p_soon) { is_coming_soon = p_soon; }
bool StoreItem::get_is_coming_soon() const { return is_coming_soon; }
void StoreItem::set_rating(float p_rating) { rating = CLAMP(p_rating, 0.0f, 5.0f); }
float StoreItem::get_rating() const { return rating; }
void StoreItem::set_review_count(int p_count) { review_count = p_count; }
int StoreItem::get_review_count() const { return review_count; }
void StoreItem::set_positive_reviews(int p_count) { positive_reviews = p_count; }
int StoreItem::get_positive_reviews() const { return positive_reviews; }
int StoreItem::get_review_percentage() const {
if (review_count == 0) return 0;
return (positive_reviews * 100) / review_count;
}
String StoreItem::get_review_summary() const {
int pct = get_review_percentage();
if (review_count == 0) return "No Reviews";
if (pct >= 95) return "Overwhelmingly Positive";
if (pct >= 80) return "Very Positive";
if (pct >= 70) return "Mostly Positive";
if (pct >= 40) return "Mixed";
if (pct >= 20) return "Mostly Negative";
return "Overwhelmingly Negative";
}
void StoreItem::set_download_url(const String &p_url) { download_url = p_url; }
String StoreItem::get_download_url() const { return download_url; }
void StoreItem::set_download_size(int64_t p_size) { download_size = p_size; }
int64_t StoreItem::get_download_size() const { return download_size; }
String StoreItem::get_download_size_formatted() const {
if (download_size < 1024) return String::num_int64(download_size) + " B";
if (download_size < 1024 * 1024) return String::num(download_size / 1024.0, 1) + " KB";
if (download_size < 1024 * 1024 * 1024) return String::num(download_size / (1024.0 * 1024.0), 1) + " MB";
return String::num(download_size / (1024.0 * 1024.0 * 1024.0), 2) + " GB";
}
void StoreItem::set_version(const String &p_version) { version = p_version; }
String StoreItem::get_version() const { return version; }
void StoreItem::set_platforms(const PackedStringArray &p_platforms) { platforms = p_platforms; }
PackedStringArray StoreItem::get_platforms() const { return platforms; }
bool StoreItem::supports_platform(const String &p_platform) const {
for (int i = 0; i < platforms.size(); i++) {
if (platforms[i].to_lower() == p_platform.to_lower()) return true;
}
return false;
}
void StoreItem::set_supports_cloud_saves(bool p_supports) { supports_cloud_saves = p_supports; }
bool StoreItem::get_supports_cloud_saves() const { return supports_cloud_saves; }
void StoreItem::set_supports_achievements(bool p_supports) { supports_achievements = p_supports; }
bool StoreItem::get_supports_achievements() const { return supports_achievements; }
void StoreItem::set_supports_controller(bool p_supports) { supports_controller = p_supports; }
bool StoreItem::get_supports_controller() const { return supports_controller; }
void StoreItem::set_supports_multiplayer(bool p_supports) { supports_multiplayer = p_supports; }
bool StoreItem::get_supports_multiplayer() const { return supports_multiplayer; }
Dictionary StoreItem::to_dictionary() const {
Dictionary dict;
dict["id"] = id;
dict["title"] = title;
dict["description"] = description;
dict["short_description"] = short_description;
dict["developer"] = developer;
dict["publisher"] = publisher;
dict["cover_image"] = cover_image;
dict["background_image"] = background_image;
dict["category"] = category;
dict["price"] = price;
dict["sale_price"] = sale_price;
dict["sale_percentage"] = sale_percentage;
dict["is_free"] = is_free;
dict["rating"] = rating;
dict["review_count"] = review_count;
dict["positive_reviews"] = positive_reviews;
dict["download_url"] = download_url;
dict["download_size"] = download_size;
dict["version"] = version;
return dict;
}
void StoreItem::from_dictionary(const Dictionary &p_dict) {
id = p_dict.get("id", "");
title = p_dict.get("title", "");
description = p_dict.get("description", "");
short_description = p_dict.get("short_description", "");
developer = p_dict.get("developer", "");
publisher = p_dict.get("publisher", "");
cover_image = p_dict.get("cover_image", "");
background_image = p_dict.get("background_image", "");
category = p_dict.get("category", "Game");
price = p_dict.get("price", 0.0f);
sale_price = p_dict.get("sale_price", 0.0f);
sale_percentage = p_dict.get("sale_percentage", 0);
is_free = p_dict.get("is_free", false);
rating = p_dict.get("rating", 0.0f);
review_count = p_dict.get("review_count", 0);
positive_reviews = p_dict.get("positive_reviews", 0);
download_url = p_dict.get("download_url", "");
download_size = p_dict.get("download_size", 0);
version = p_dict.get("version", "");
}
// ============================================================================
// LauncherStore
// ============================================================================
void LauncherStore::_bind_methods() {
ClassDB::bind_method(D_METHOD("fetch_featured"), &LauncherStore::fetch_featured);
ClassDB::bind_method(D_METHOD("fetch_category", "category", "page"), &LauncherStore::fetch_category, DEFVAL(0));
ClassDB::bind_method(D_METHOD("fetch_new_releases", "page"), &LauncherStore::fetch_new_releases, DEFVAL(0));
ClassDB::bind_method(D_METHOD("fetch_top_sellers", "page"), &LauncherStore::fetch_top_sellers, DEFVAL(0));
ClassDB::bind_method(D_METHOD("fetch_on_sale", "page"), &LauncherStore::fetch_on_sale, DEFVAL(0));
ClassDB::bind_method(D_METHOD("search", "query", "page"), &LauncherStore::search, DEFVAL(0));
ClassDB::bind_method(D_METHOD("get_items"), &LauncherStore::get_items);
ClassDB::bind_method(D_METHOD("get_featured"), &LauncherStore::get_featured);
ClassDB::bind_method(D_METHOD("get_item", "id"), &LauncherStore::get_item);
ClassDB::bind_method(D_METHOD("get_all_items"), &LauncherStore::get_all_items);
ClassDB::bind_method(D_METHOD("get_sale_items"), &LauncherStore::get_sale_items);
ClassDB::bind_method(D_METHOD("get_new_releases"), &LauncherStore::get_new_releases);
ClassDB::bind_method(D_METHOD("get_popular_items"), &LauncherStore::get_popular_items);
ClassDB::bind_method(D_METHOD("get_items_by_category", "category"), &LauncherStore::get_items_by_category);
ClassDB::bind_method(D_METHOD("add_to_wishlist", "id"), &LauncherStore::add_to_wishlist);
ClassDB::bind_method(D_METHOD("remove_from_wishlist", "id"), &LauncherStore::remove_from_wishlist);
ClassDB::bind_method(D_METHOD("toggle_wishlist", "id"), &LauncherStore::toggle_wishlist);
ClassDB::bind_method(D_METHOD("is_wishlisted", "id"), &LauncherStore::is_wishlisted);
ClassDB::bind_method(D_METHOD("get_wishlist"), &LauncherStore::get_wishlist);
ClassDB::bind_method(D_METHOD("add_to_cart", "id"), &LauncherStore::add_to_cart);
ClassDB::bind_method(D_METHOD("remove_from_cart", "id"), &LauncherStore::remove_from_cart);
ClassDB::bind_method(D_METHOD("get_cart_items"), &LauncherStore::get_cart_items);
ClassDB::bind_method(D_METHOD("clear_cart"), &LauncherStore::clear_cart);
ClassDB::bind_method(D_METHOD("get_cart_total"), &LauncherStore::get_cart_total);
ClassDB::bind_method(D_METHOD("checkout"), &LauncherStore::checkout);
ClassDB::bind_method(D_METHOD("get_checkout_url", "id"), &LauncherStore::get_checkout_url);
ADD_SIGNAL(MethodInfo("items_loaded", PropertyInfo(Variant::ARRAY, "items")));
ADD_SIGNAL(MethodInfo("featured_loaded", PropertyInfo(Variant::ARRAY, "items")));
ADD_SIGNAL(MethodInfo("item_details_loaded", PropertyInfo(Variant::OBJECT, "item")));
ADD_SIGNAL(MethodInfo("wishlist_updated"));
ADD_SIGNAL(MethodInfo("cart_updated"));
ADD_SIGNAL(MethodInfo("purchase_completed", PropertyInfo(Variant::STRING, "item_id")));
ADD_SIGNAL(MethodInfo("purchase_failed", PropertyInfo(Variant::STRING, "error")));
}
LauncherStore::LauncherStore() {}
LauncherStore::~LauncherStore() {}
void LauncherStore::set_launcher(AethexLauncher *p_launcher) {
launcher = p_launcher;
}
void LauncherStore::_parse_store_response(const Dictionary &p_response) {
items.clear();
if (p_response.has("items")) {
Array items_array = p_response["items"];
for (int i = 0; i < items_array.size(); i++) {
Ref<StoreItem> item;
item.instantiate();
item->from_dictionary(items_array[i]);
items.push_back(item);
}
}
has_more = p_response.get("has_more", false);
current_page = p_response.get("page", 0);
}
void LauncherStore::fetch_featured() {
// Populate demo store data
_populate_demo_items();
TypedArray<StoreItem> result;
for (int i = 0; i < featured.size(); i++) {
result.push_back(featured[i]);
}
emit_signal("featured_loaded", result);
}
void LauncherStore::fetch_category(const String &p_category, int p_page) {
current_category = p_category;
current_page = p_page;
// TODO: Make API call
emit_signal("items_loaded", Array());
}
void LauncherStore::fetch_new_releases(int p_page) {
fetch_category("new", p_page);
}
void LauncherStore::fetch_top_sellers(int p_page) {
fetch_category("top", p_page);
}
void LauncherStore::fetch_on_sale(int p_page) {
fetch_category("sale", p_page);
}
void LauncherStore::search(const String &p_query, int p_page) {
search_query = p_query;
current_page = p_page;
// TODO: Make API call
emit_signal("items_loaded", Array());
}
void LauncherStore::fetch_item_details(const String &p_id) {
// TODO: Make API call
}
void LauncherStore::fetch_next_page() {
if (has_more) {
if (!search_query.is_empty()) {
search(search_query, current_page + 1);
} else if (!current_category.is_empty()) {
fetch_category(current_category, current_page + 1);
}
}
}
bool LauncherStore::get_has_more() const { return has_more; }
int LauncherStore::get_current_page() const { return current_page; }
TypedArray<StoreItem> LauncherStore::get_items() const {
TypedArray<StoreItem> result;
for (int i = 0; i < items.size(); i++) {
result.push_back(items[i]);
}
return result;
}
TypedArray<StoreItem> LauncherStore::get_featured() const {
TypedArray<StoreItem> result;
for (int i = 0; i < featured.size(); i++) {
result.push_back(featured[i]);
}
return result;
}
Ref<StoreItem> LauncherStore::get_item(const String &p_id) const {
for (int i = 0; i < items.size(); i++) {
if (items[i].is_valid() && items[i]->get_id() == p_id) {
return items[i];
}
}
return Ref<StoreItem>();
}
void LauncherStore::add_to_wishlist(const String &p_id) {
Ref<StoreItem> item = get_item(p_id);
if (item.is_valid() && !is_wishlisted(p_id)) {
wishlist.push_back(item);
emit_signal("wishlist_updated");
}
}
void LauncherStore::remove_from_wishlist(const String &p_id) {
for (int i = 0; i < wishlist.size(); i++) {
if (wishlist[i].is_valid() && wishlist[i]->get_id() == p_id) {
wishlist.remove_at(i);
emit_signal("wishlist_updated");
return;
}
}
}
bool LauncherStore::is_wishlisted(const String &p_id) const {
for (int i = 0; i < wishlist.size(); i++) {
if (wishlist[i].is_valid() && wishlist[i]->get_id() == p_id) {
return true;
}
}
return false;
}
TypedArray<StoreItem> LauncherStore::get_wishlist() const {
TypedArray<StoreItem> result;
for (int i = 0; i < wishlist.size(); i++) {
result.push_back(wishlist[i]);
}
return result;
}
void LauncherStore::fetch_wishlist() {
// TODO: Fetch from server
}
Error LauncherStore::purchase(const String &p_id) {
String url = get_checkout_url(p_id);
if (url.is_empty()) {
return ERR_INVALID_DATA;
}
OS::get_singleton()->shell_open(url);
return OK;
}
String LauncherStore::get_checkout_url(const String &p_id) const {
if (!launcher) return "";
return launcher->get_api_base_url() + "/checkout/" + p_id;
}
// Additional getters for store_panel
TypedArray<StoreItem> LauncherStore::get_featured_items() const {
return get_featured();
}
TypedArray<StoreItem> LauncherStore::get_sale_items() const {
TypedArray<StoreItem> result;
for (int i = 0; i < sale_items.size(); i++) {
result.push_back(sale_items[i]);
}
// If empty, return items that are on sale from all items
if (result.is_empty()) {
for (int i = 0; i < items.size(); i++) {
if (items[i].is_valid() && items[i]->is_on_sale()) {
result.push_back(items[i]);
}
}
}
return result;
}
TypedArray<StoreItem> LauncherStore::get_new_releases() const {
TypedArray<StoreItem> result;
for (int i = 0; i < new_releases.size(); i++) {
result.push_back(new_releases[i]);
}
return result;
}
TypedArray<StoreItem> LauncherStore::get_popular_items() const {
TypedArray<StoreItem> result;
for (int i = 0; i < popular_items.size(); i++) {
result.push_back(popular_items[i]);
}
return result;
}
TypedArray<StoreItem> LauncherStore::get_all_items() const {
return get_items();
}
TypedArray<StoreItem> LauncherStore::get_items_by_category(const String &p_category) const {
TypedArray<StoreItem> result;
for (int i = 0; i < items.size(); i++) {
if (items[i].is_valid() && items[i]->get_category().to_lower() == p_category.to_lower()) {
result.push_back(items[i]);
}
}
return result;
}
void LauncherStore::toggle_wishlist(const String &p_id) {
if (is_wishlisted(p_id)) {
remove_from_wishlist(p_id);
} else {
add_to_wishlist(p_id);
}
}
// Cart functionality
void LauncherStore::add_to_cart(const String &p_id) {
Ref<StoreItem> item = get_item(p_id);
if (item.is_valid()) {
// Check if not already in cart
for (int i = 0; i < cart.size(); i++) {
if (cart[i].is_valid() && cart[i]->get_id() == p_id) {
return; // Already in cart
}
}
cart.push_back(item);
emit_signal("cart_updated");
}
}
void LauncherStore::remove_from_cart(const String &p_id) {
for (int i = 0; i < cart.size(); i++) {
if (cart[i].is_valid() && cart[i]->get_id() == p_id) {
cart.remove_at(i);
emit_signal("cart_updated");
return;
}
}
}
TypedArray<StoreItem> LauncherStore::get_cart_items() const {
TypedArray<StoreItem> result;
for (int i = 0; i < cart.size(); i++) {
result.push_back(cart[i]);
}
return result;
}
void LauncherStore::clear_cart() {
cart.clear();
emit_signal("cart_updated");
}
float LauncherStore::get_cart_total() const {
float total = 0;
for (int i = 0; i < cart.size(); i++) {
if (cart[i].is_valid()) {
total += cart[i]->get_current_price();
}
}
return total;
}
void LauncherStore::checkout() {
// Process checkout - would integrate with payment system
// For now, clear cart
clear_cart();
}
void LauncherStore::_populate_demo_items() {
// Only populate once
if (!items.is_empty()) return;
// Demo Item 1: AeThex Adventure (Featured, Free)
{
Ref<StoreItem> item;
item.instantiate();
item->set_id("aethex-adventure");
item->set_title("AeThex Adventure");
item->set_description("Embark on an epic journey through the AeThex multiverse. Explore procedurally generated worlds, battle fierce creatures, and uncover ancient secrets.");
item->set_short_description("Epic multiverse adventure RPG");
item->set_developer("AeThex Studios");
item->set_publisher("AeThex Labs");
item->set_category("Game");
item->set_is_free(true);
item->set_rating(4.8);
item->set_review_count(1250);
item->set_positive_reviews(1180);
items.push_back(item);
featured.push_back(item);
}
// Demo Item 2: Neon Racer (Featured, Paid)
{
Ref<StoreItem> item;
item.instantiate();
item->set_id("neon-racer");
item->set_title("Neon Racer");
item->set_description("High-octane racing through cyberpunk cityscapes. Customize your ride, outrun the competition, and dominate the neon-lit streets.");
item->set_short_description("Cyberpunk racing action");
item->set_developer("Velocity Games");
item->set_publisher("AeThex Labs");
item->set_category("Game");
item->set_price(19.99);
item->set_rating(4.5);
item->set_review_count(820);
item->set_positive_reviews(750);
items.push_back(item);
featured.push_back(item);
popular_items.push_back(item);
}
// Demo Item 3: Stellar Colonies (On Sale)
{
Ref<StoreItem> item;
item.instantiate();
item->set_id("stellar-colonies");
item->set_title("Stellar Colonies");
item->set_description("Build and manage interstellar colonies across the galaxy. Balance resources, expand your empire, and forge alliances with alien civilizations.");
item->set_short_description("Epic space strategy");
item->set_developer("Cosmic Forge");
item->set_publisher("AeThex Labs");
item->set_category("Game");
item->set_price(29.99);
item->set_sale_price(19.99);
item->set_sale_percentage(33);
item->set_rating(4.7);
item->set_review_count(650);
item->set_positive_reviews(600);
items.push_back(item);
sale_items.push_back(item);
popular_items.push_back(item);
}
// Demo Item 4: Pixel Dungeon Quest (New Release)
{
Ref<StoreItem> item;
item.instantiate();
item->set_id("pixel-dungeon");
item->set_title("Pixel Dungeon Quest");
item->set_description("A charming roguelike adventure with procedurally generated dungeons. Classic gameplay meets modern design.");
item->set_short_description("Retro roguelike adventure");
item->set_developer("Retro Pixel Studios");
item->set_publisher("AeThex Labs");
item->set_category("Game");
item->set_price(9.99);
item->set_rating(4.3);
item->set_review_count(320);
item->set_positive_reviews(280);
items.push_back(item);
new_releases.push_back(item);
}
// Demo Item 5: Quantum Dash (Featured, New)
{
Ref<StoreItem> item;
item.instantiate();
item->set_id("quantum-dash");
item->set_title("Quantum Dash");
item->set_description("A mind-bending puzzle platformer where you manipulate quantum physics to solve increasingly complex challenges.");
item->set_short_description("Physics puzzle platformer");
item->set_developer("Paradox Interactive");
item->set_publisher("AeThex Labs");
item->set_category("Game");
item->set_price(14.99);
item->set_rating(4.6);
item->set_review_count(180);
item->set_positive_reviews(165);
items.push_back(item);
featured.push_back(item);
new_releases.push_back(item);
}
// Demo Item 6: Asset Pack
{
Ref<StoreItem> item;
item.instantiate();
item->set_id("fantasy-asset-pack");
item->set_title("Fantasy RPG Asset Pack");
item->set_description("Complete asset pack for building fantasy RPG games. Includes characters, environments, UI, and effects.");
item->set_short_description("Game dev asset collection");
item->set_developer("Asset Masters");
item->set_publisher("AeThex Labs");
item->set_category("Asset");
item->set_price(24.99);
item->set_sale_price(12.49);
item->set_sale_percentage(50);
item->set_rating(4.9);
item->set_review_count(95);
item->set_positive_reviews(93);
items.push_back(item);
sale_items.push_back(item);
}
}

View file

@ -0,0 +1,264 @@
/**************************************************************************/
/* launcher_store.h */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifndef LAUNCHER_STORE_H
#define LAUNCHER_STORE_H
#include "core/object/ref_counted.h"
#include "core/variant/typed_array.h"
class AethexLauncher;
class StoreItem : public RefCounted {
GDCLASS(StoreItem, RefCounted);
private:
String id;
String title;
String description;
String short_description;
String developer;
String publisher;
String cover_image;
String background_image;
PackedStringArray screenshots;
String trailer_url;
String category; // Game, Tool, Asset, DLC
PackedStringArray tags;
PackedStringArray genres;
float price = 0.0f;
float sale_price = 0.0f;
int sale_percentage = 0;
String currency = "USD";
bool is_free = false;
String release_date;
String early_access_date;
bool is_early_access = false;
bool is_coming_soon = false;
float rating = 0.0f;
int review_count = 0;
int positive_reviews = 0;
String download_url;
int64_t download_size = 0;
String version;
PackedStringArray platforms; // windows, macos, linux
String min_os;
String min_cpu;
String min_ram;
String min_gpu;
String min_storage;
bool supports_cloud_saves = false;
bool supports_achievements = false;
bool supports_controller = false;
bool supports_multiplayer = false;
Dictionary custom_data;
protected:
static void _bind_methods();
public:
void set_id(const String &p_id);
String get_id() const;
void set_title(const String &p_title);
String get_title() const;
void set_description(const String &p_description);
String get_description() const;
void set_short_description(const String &p_description);
String get_short_description() const;
void set_developer(const String &p_developer);
String get_developer() const;
void set_publisher(const String &p_publisher);
String get_publisher() const;
void set_cover_image(const String &p_url);
String get_cover_image() const;
void set_background_image(const String &p_url);
String get_background_image() const;
void set_screenshots(const PackedStringArray &p_urls);
PackedStringArray get_screenshots() const;
void set_category(const String &p_category);
String get_category() const;
void set_tags(const PackedStringArray &p_tags);
PackedStringArray get_tags() const;
bool has_tag(const String &p_tag) const;
void set_genres(const PackedStringArray &p_genres);
PackedStringArray get_genres() const;
// Pricing
void set_price(float p_price);
float get_price() const;
void set_sale_price(float p_price);
float get_sale_price() const;
void set_sale_percentage(int p_percentage);
int get_sale_percentage() const;
float get_discount_percent() const; // Alias for get_sale_percentage
float get_current_price() const;
String get_price_formatted() const;
bool is_on_sale() const;
void set_is_free(bool p_free);
bool get_is_free() const;
// Release
void set_release_date(const String &p_date);
String get_release_date() const;
void set_is_early_access(bool p_early);
bool get_is_early_access() const;
void set_is_coming_soon(bool p_soon);
bool get_is_coming_soon() const;
// Reviews
void set_rating(float p_rating);
float get_rating() const;
void set_review_count(int p_count);
int get_review_count() const;
void set_positive_reviews(int p_count);
int get_positive_reviews() const;
int get_review_percentage() const;
String get_review_summary() const;
// Download info
void set_download_url(const String &p_url);
String get_download_url() const;
void set_download_size(int64_t p_size);
int64_t get_download_size() const;
String get_download_size_formatted() const;
void set_version(const String &p_version);
String get_version() const;
// Platforms
void set_platforms(const PackedStringArray &p_platforms);
PackedStringArray get_platforms() const;
bool supports_platform(const String &p_platform) const;
// Features
void set_supports_cloud_saves(bool p_supports);
bool get_supports_cloud_saves() const;
void set_supports_achievements(bool p_supports);
bool get_supports_achievements() const;
void set_supports_controller(bool p_supports);
bool get_supports_controller() const;
void set_supports_multiplayer(bool p_supports);
bool get_supports_multiplayer() const;
// Serialization
Dictionary to_dictionary() const;
void from_dictionary(const Dictionary &p_dict);
StoreItem();
~StoreItem();
};
class LauncherStore : public RefCounted {
GDCLASS(LauncherStore, RefCounted);
private:
AethexLauncher *launcher = nullptr;
Vector<Ref<StoreItem>> items;
Vector<Ref<StoreItem>> featured;
Vector<Ref<StoreItem>> wishlist;
Vector<Ref<StoreItem>> cart;
Vector<Ref<StoreItem>> sale_items;
Vector<Ref<StoreItem>> new_releases;
Vector<Ref<StoreItem>> popular_items;
String current_category;
String search_query;
int current_page = 0;
int items_per_page = 20;
bool has_more = false;
void _parse_store_response(const Dictionary &p_response);
void _populate_demo_items();
protected:
static void _bind_methods();
public:
void set_launcher(AethexLauncher *p_launcher);
// Fetching
void fetch_featured();
void fetch_category(const String &p_category, int p_page = 0);
void fetch_new_releases(int p_page = 0);
void fetch_top_sellers(int p_page = 0);
void fetch_on_sale(int p_page = 0);
void search(const String &p_query, int p_page = 0);
void fetch_item_details(const String &p_id);
// Pagination
void fetch_next_page();
bool get_has_more() const;
int get_current_page() const;
// Results / Getters for store_panel
TypedArray<StoreItem> get_items() const;
TypedArray<StoreItem> get_featured() const;
TypedArray<StoreItem> get_featured_items() const;
TypedArray<StoreItem> get_sale_items() const;
TypedArray<StoreItem> get_new_releases() const;
TypedArray<StoreItem> get_popular_items() const;
TypedArray<StoreItem> get_all_items() const;
TypedArray<StoreItem> get_items_by_category(const String &p_category) const;
Ref<StoreItem> get_item(const String &p_id) const;
// Wishlist
void add_to_wishlist(const String &p_id);
void remove_from_wishlist(const String &p_id);
void toggle_wishlist(const String &p_id);
bool is_wishlisted(const String &p_id) const;
TypedArray<StoreItem> get_wishlist() const;
void fetch_wishlist();
// Cart
void add_to_cart(const String &p_id);
void remove_from_cart(const String &p_id);
TypedArray<StoreItem> get_cart_items() const;
void clear_cart();
float get_cart_total() const;
// Purchase
Error purchase(const String &p_id);
void checkout();
String get_checkout_url(const String &p_id) const;
LauncherStore();
~LauncherStore();
};
#endif // LAUNCHER_STORE_H

View file

@ -0,0 +1,73 @@
/**************************************************************************/
/* launcher_editor_plugin.cpp */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifdef TOOLS_ENABLED
#include "launcher_editor_plugin.h"
#include "../aethex_launcher.h"
#include "editor/editor_node.h"
#include "editor/editor_main_screen.h"
#include "editor/editor_interface.h"
#include "editor/themes/editor_scale.h"
void LauncherEditorPlugin::_bind_methods() {
}
void LauncherEditorPlugin::make_visible(bool p_visible) {
if (launcher_panel) {
if (p_visible) {
launcher_panel->show();
} else {
launcher_panel->hide();
}
}
}
LauncherEditorPlugin::LauncherEditorPlugin() {
// Safety check - EditorNode must exist
EditorNode *editor = EditorNode::get_singleton();
if (!editor) {
ERR_PRINT("LauncherEditorPlugin: EditorNode not available");
return;
}
EditorMainScreen *main_screen = editor->get_editor_main_screen();
if (!main_screen) {
ERR_PRINT("LauncherEditorPlugin: EditorMainScreen not available");
return;
}
Control *main_control = main_screen->get_control();
if (!main_control) {
ERR_PRINT("LauncherEditorPlugin: Main screen control not available");
return;
}
launcher_panel = memnew(LauncherPanel);
launcher_panel->set_v_size_flags(Control::SIZE_EXPAND_FILL);
main_control->add_child(launcher_panel);
launcher_panel->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
launcher_panel->hide();
// Set launcher after panel is in tree
AethexLauncher *launcher = AethexLauncher::get_singleton();
if (launcher) {
launcher_panel->set_launcher(launcher);
}
// Make AeThex the default main screen (deferred to avoid timing issues)
EditorInterface *ei = EditorInterface::get_singleton();
if (ei) {
callable_mp(ei, &EditorInterface::set_main_screen_editor).call_deferred("AeThex");
}
}
LauncherEditorPlugin::~LauncherEditorPlugin() {
}
#endif // TOOLS_ENABLED

View file

@ -0,0 +1,38 @@
/**************************************************************************/
/* launcher_editor_plugin.h */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifndef LAUNCHER_EDITOR_PLUGIN_H
#define LAUNCHER_EDITOR_PLUGIN_H
#ifdef TOOLS_ENABLED
#include "editor/plugins/editor_plugin.h"
#include "../ui/launcher_panel.h"
class LauncherEditorPlugin : public EditorPlugin {
GDCLASS(LauncherEditorPlugin, EditorPlugin);
private:
LauncherPanel *launcher_panel = nullptr;
protected:
static void _bind_methods();
public:
virtual String get_plugin_name() const override { return "AeThex"; }
bool has_main_screen() const override { return true; }
virtual void edit(Object *p_object) override {}
virtual bool handles(Object *p_object) const override { return false; }
virtual void make_visible(bool p_visible) override;
LauncherEditorPlugin();
~LauncherEditorPlugin();
};
#endif // TOOLS_ENABLED
#endif // LAUNCHER_EDITOR_PLUGIN_H

View file

@ -0,0 +1,73 @@
/**************************************************************************/
/* register_types.cpp */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#include "register_types.h"
#include "aethex_launcher.h"
#include "data/game_library.h"
#include "data/download_manager.h"
#include "data/launcher_store.h"
#include "data/friend_system.h"
#include "data/launcher_profile.h"
#ifdef TOOLS_ENABLED
#include "editor/launcher_editor_plugin.h"
#include "editor/plugins/editor_plugin.h"
#include "ui/launcher_panel.h"
#include "ui/library_panel.h"
#include "ui/store_panel.h"
#include "ui/downloads_panel.h"
#include "ui/friends_panel.h"
#include "ui/profile_panel.h"
#include "ui/auth_panel.h"
#endif
static AethexLauncher *launcher_singleton = nullptr;
void initialize_aethex_launcher_module(ModuleInitializationLevel p_level) {
if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) {
GDREGISTER_CLASS(GameEntry);
GDREGISTER_CLASS(GameLibrary);
GDREGISTER_CLASS(DownloadTask);
GDREGISTER_CLASS(DownloadManager);
GDREGISTER_CLASS(StoreItem);
GDREGISTER_CLASS(LauncherStore);
GDREGISTER_CLASS(Friend);
GDREGISTER_CLASS(FriendSystem);
GDREGISTER_CLASS(LauncherProfile);
GDREGISTER_CLASS(AethexLauncher);
launcher_singleton = memnew(AethexLauncher);
Engine::get_singleton()->add_singleton(Engine::Singleton("AethexLauncher", AethexLauncher::get_singleton()));
}
#ifdef TOOLS_ENABLED
if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) {
// Main panels only - subclasses don't need to be registered
GDREGISTER_CLASS(LauncherPanel);
GDREGISTER_CLASS(LibraryPanel);
GDREGISTER_CLASS(StorePanel);
GDREGISTER_CLASS(DownloadsPanel);
GDREGISTER_CLASS(FriendsPanel);
GDREGISTER_CLASS(ProfilePanel);
GDREGISTER_CLASS(AuthPanel);
GDREGISTER_CLASS(LauncherEditorPlugin);
EditorPlugins::add_by_type<LauncherEditorPlugin>();
}
#endif
}
void uninitialize_aethex_launcher_module(ModuleInitializationLevel p_level) {
if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) {
Engine::get_singleton()->remove_singleton("AethexLauncher");
if (launcher_singleton) {
memdelete(launcher_singleton);
launcher_singleton = nullptr;
}
}
}

View file

@ -0,0 +1,16 @@
/**************************************************************************/
/* register_types.h */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifndef AETHEX_LAUNCHER_REGISTER_TYPES_H
#define AETHEX_LAUNCHER_REGISTER_TYPES_H
#include "modules/register_module_types.h"
void initialize_aethex_launcher_module(ModuleInitializationLevel p_level);
void uninitialize_aethex_launcher_module(ModuleInitializationLevel p_level);
#endif // AETHEX_LAUNCHER_REGISTER_TYPES_H

View file

@ -0,0 +1,420 @@
/**************************************************************************/
/* auth_panel.cpp */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifdef TOOLS_ENABLED
#include "auth_panel.h"
#include "../aethex_launcher.h"
#include "editor/themes/editor_scale.h"
void AuthPanel::_bind_methods() {
ADD_SIGNAL(MethodInfo("authenticated"));
ADD_SIGNAL(MethodInfo("cancelled"));
}
AuthPanel::AuthPanel() {
}
AuthPanel::~AuthPanel() {
}
void AuthPanel::_notification(int p_what) {
if (p_what == NOTIFICATION_ENTER_TREE && !setup_done) {
_setup_ui();
}
}
void AuthPanel::_setup_ui() {
if (setup_done) return;
setup_done = true;
// Semi-transparent background
ColorRect *bg = memnew(ColorRect);
bg->set_color(Color(0, 0, 0, 0.8));
bg->set_anchors_and_offsets_preset(PRESET_FULL_RECT);
add_child(bg);
// Auth card container
auth_card = memnew(PanelContainer);
auth_card->set_custom_minimum_size(Size2(400 * EDSCALE, 500 * EDSCALE));
auth_card->set_anchors_and_offsets_preset(PRESET_CENTER);
add_child(auth_card);
VBoxContainer *card_content = memnew(VBoxContainer);
auth_card->add_child(card_content);
// Logo/Title
Label *title = memnew(Label);
title->set_text("AeThex");
title->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
title->add_theme_font_size_override("font_size", 32 * EDSCALE);
card_content->add_child(title);
Label *subtitle = memnew(Label);
subtitle->set_text("Sign in to continue");
subtitle->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
card_content->add_child(subtitle);
card_content->add_child(memnew(HSeparator));
// Error message
error_label = memnew(Label);
error_label->set_visible(false);
error_label->add_theme_color_override("font_color", Color(0.9, 0.3, 0.3));
error_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
card_content->add_child(error_label);
// Tab bar
tab_bar = memnew(HBoxContainer);
card_content->add_child(tab_bar);
login_tab = memnew(Button);
login_tab->set_text("Sign In");
login_tab->set_toggle_mode(true);
login_tab->set_pressed(true);
login_tab->set_h_size_flags(SIZE_EXPAND_FILL);
login_tab->connect("pressed", callable_mp(this, &AuthPanel::_show_login));
tab_bar->add_child(login_tab);
signup_tab = memnew(Button);
signup_tab->set_text("Sign Up");
signup_tab->set_toggle_mode(true);
signup_tab->set_h_size_flags(SIZE_EXPAND_FILL);
signup_tab->connect("pressed", callable_mp(this, &AuthPanel::_show_signup));
tab_bar->add_child(signup_tab);
// Login form
login_form = memnew(VBoxContainer);
card_content->add_child(login_form);
login_email = memnew(LineEdit);
login_email->set_placeholder("Email");
login_form->add_child(login_email);
login_password = memnew(LineEdit);
login_password->set_placeholder("Password");
login_password->set_secret(true);
login_password->connect("text_submitted", callable_mp(this, &AuthPanel::_on_login_submitted));
login_form->add_child(login_password);
remember_me = memnew(CheckButton);
remember_me->set_text("Remember me");
login_form->add_child(remember_me);
login_button = memnew(Button);
login_button->set_text("Sign In");
login_button->connect("pressed", callable_mp(this, &AuthPanel::_on_login_pressed));
login_form->add_child(login_button);
forgot_password = memnew(LinkButton);
forgot_password->set_text("Forgot password?");
forgot_password->connect("pressed", callable_mp(this, &AuthPanel::_show_forgot_password));
login_form->add_child(forgot_password);
// Signup form (hidden by default)
signup_form = memnew(VBoxContainer);
signup_form->set_visible(false);
card_content->add_child(signup_form);
signup_email = memnew(LineEdit);
signup_email->set_placeholder("Email");
signup_form->add_child(signup_email);
signup_username = memnew(LineEdit);
signup_username->set_placeholder("Username");
signup_form->add_child(signup_username);
signup_password = memnew(LineEdit);
signup_password->set_placeholder("Password");
signup_password->set_secret(true);
signup_form->add_child(signup_password);
signup_confirm = memnew(LineEdit);
signup_confirm->set_placeholder("Confirm Password");
signup_confirm->set_secret(true);
signup_form->add_child(signup_confirm);
terms_checkbox = memnew(CheckButton);
terms_checkbox->set_text("I agree to the Terms of Service");
signup_form->add_child(terms_checkbox);
signup_button = memnew(Button);
signup_button->set_text("Create Account");
signup_button->connect("pressed", callable_mp(this, &AuthPanel::_on_signup_pressed));
signup_form->add_child(signup_button);
// OAuth divider
HBoxContainer *divider = memnew(HBoxContainer);
card_content->add_child(divider);
HSeparator *sep1 = memnew(HSeparator);
sep1->set_h_size_flags(SIZE_EXPAND_FILL);
divider->add_child(sep1);
Label *or_label = memnew(Label);
or_label->set_text(" or ");
divider->add_child(or_label);
HSeparator *sep2 = memnew(HSeparator);
sep2->set_h_size_flags(SIZE_EXPAND_FILL);
divider->add_child(sep2);
// OAuth buttons
oauth_buttons = memnew(VBoxContainer);
card_content->add_child(oauth_buttons);
google_button = memnew(Button);
google_button->set_text("Continue with Google");
google_button->connect("pressed", callable_mp(this, &AuthPanel::_oauth_google));
oauth_buttons->add_child(google_button);
discord_button = memnew(Button);
discord_button->set_text("Continue with Discord");
discord_button->connect("pressed", callable_mp(this, &AuthPanel::_oauth_discord));
oauth_buttons->add_child(discord_button);
github_button = memnew(Button);
github_button->set_text("Continue with GitHub");
github_button->connect("pressed", callable_mp(this, &AuthPanel::_oauth_github));
oauth_buttons->add_child(github_button);
// Skip/Guest option
Control *spacer = memnew(Control);
spacer->set_custom_minimum_size(Size2(0, 10 * EDSCALE));
card_content->add_child(spacer);
skip_button = memnew(LinkButton);
skip_button->set_text("Continue as Guest");
skip_button->connect("pressed", callable_mp(this, &AuthPanel::_skip_auth));
card_content->add_child(skip_button);
// Profile setup overlay (shown after signup/oauth)
profile_setup = memnew(VBoxContainer);
profile_setup->set_visible(false);
add_child(profile_setup);
Label *setup_title = memnew(Label);
setup_title->set_text("Set Up Your Profile");
setup_title->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
setup_title->add_theme_font_size_override("font_size", 20 * EDSCALE);
profile_setup->add_child(setup_title);
setup_display_name = memnew(LineEdit);
setup_display_name->set_placeholder("Display Name");
profile_setup->add_child(setup_display_name);
setup_avatar_button = memnew(Button);
setup_avatar_button->set_text("Choose Avatar");
profile_setup->add_child(setup_avatar_button);
setup_bio = memnew(TextEdit);
setup_bio->set_placeholder("Tell us about yourself (optional)");
setup_bio->set_custom_minimum_size(Size2(0, 80 * EDSCALE));
profile_setup->add_child(setup_bio);
setup_complete_button = memnew(Button);
setup_complete_button->set_text("Complete Setup");
setup_complete_button->connect("pressed", callable_mp(this, &AuthPanel::_complete_profile_setup));
profile_setup->add_child(setup_complete_button);
setup_skip_button = memnew(LinkButton);
setup_skip_button->set_text("Skip for now");
setup_skip_button->connect("pressed", callable_mp(this, &AuthPanel::_skip_profile_setup));
profile_setup->add_child(setup_skip_button);
// Loading overlay
loading_overlay = memnew(VBoxContainer);
loading_overlay->set_visible(false);
loading_overlay->set_anchors_and_offsets_preset(PRESET_CENTER);
add_child(loading_overlay);
loading_spinner = memnew(TextureRect);
loading_overlay->add_child(loading_spinner);
loading_label = memnew(Label);
loading_label->set_text("Please wait...");
loading_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
loading_overlay->add_child(loading_label);
}
void AuthPanel::set_launcher(AethexLauncher *p_launcher) {
launcher = p_launcher;
if (launcher) {
launcher->connect("authenticated", callable_mp(this, &AuthPanel::_on_authenticated));
launcher->connect("auth_error", callable_mp(this, &AuthPanel::_on_auth_error));
}
}
void AuthPanel::show_auth() {
set_visible(true);
auth_card->set_visible(true);
profile_setup->set_visible(false);
loading_overlay->set_visible(false);
_show_login();
}
void AuthPanel::_show_login() {
is_signup_mode = false;
login_tab->set_pressed(true);
signup_tab->set_pressed(false);
login_form->set_visible(true);
signup_form->set_visible(false);
error_label->set_visible(false);
}
void AuthPanel::_show_signup() {
is_signup_mode = true;
login_tab->set_pressed(false);
signup_tab->set_pressed(true);
login_form->set_visible(false);
signup_form->set_visible(true);
error_label->set_visible(false);
}
void AuthPanel::_show_forgot_password() {
// Would show forgot password flow
}
void AuthPanel::_on_login_pressed() {
String email = login_email->get_text().strip_edges();
String password = login_password->get_text();
if (email.is_empty() || password.is_empty()) {
_show_error("Please enter email and password");
return;
}
_show_loading("Signing in...");
if (launcher) {
launcher->sign_in_with_email(email, password);
}
}
void AuthPanel::_on_login_submitted(const String &p_text) {
_on_login_pressed();
}
void AuthPanel::_on_signup_pressed() {
String email = signup_email->get_text().strip_edges();
String username = signup_username->get_text().strip_edges();
String password = signup_password->get_text();
String confirm = signup_confirm->get_text();
if (email.is_empty() || username.is_empty() || password.is_empty()) {
_show_error("Please fill in all fields");
return;
}
if (password != confirm) {
_show_error("Passwords do not match");
return;
}
if (password.length() < 8) {
_show_error("Password must be at least 8 characters");
return;
}
if (!terms_checkbox->is_pressed()) {
_show_error("Please accept the Terms of Service");
return;
}
_show_loading("Creating account...");
if (launcher) {
launcher->sign_up_with_email(email, password, username);
}
}
void AuthPanel::_oauth_google() {
_show_loading("Connecting to Google...");
if (launcher) {
launcher->sign_in_with_oauth("google");
}
}
void AuthPanel::_oauth_discord() {
_show_loading("Connecting to Discord...");
if (launcher) {
launcher->sign_in_with_oauth("discord");
}
}
void AuthPanel::_oauth_github() {
_show_loading("Connecting to GitHub...");
if (launcher) {
launcher->sign_in_with_oauth("github");
}
}
void AuthPanel::_skip_auth() {
emit_signal("cancelled");
set_visible(false);
}
void AuthPanel::_on_authenticated(const Dictionary &p_user) {
loading_overlay->set_visible(false);
// Check if this is a new user that needs profile setup
bool is_new = p_user.get("is_new", false);
if (is_new) {
_show_profile_setup();
} else {
emit_signal("authenticated");
set_visible(false);
}
}
void AuthPanel::_on_auth_error(const String &p_error) {
loading_overlay->set_visible(false);
auth_card->set_visible(true);
_show_error(p_error);
}
void AuthPanel::_show_error(const String &p_message) {
error_label->set_text(p_message);
error_label->set_visible(true);
}
void AuthPanel::_show_loading(const String &p_message) {
loading_label->set_text(p_message);
loading_overlay->set_visible(true);
auth_card->set_visible(false);
}
void AuthPanel::_show_profile_setup() {
auth_card->set_visible(false);
profile_setup->set_visible(true);
profile_setup->set_anchors_and_offsets_preset(PRESET_CENTER);
}
void AuthPanel::_complete_profile_setup() {
String display_name = setup_display_name->get_text().strip_edges();
if (display_name.is_empty()) {
return;
}
if (launcher) {
auto profile = launcher->get_current_profile();
if (profile.is_valid()) {
profile->set_display_name(display_name);
profile->set_bio(setup_bio->get_text().strip_edges());
// TODO: Save profile to server via launcher
}
}
emit_signal("authenticated");
set_visible(false);
}
void AuthPanel::_skip_profile_setup() {
emit_signal("authenticated");
set_visible(false);
}
#endif // TOOLS_ENABLED

View file

@ -0,0 +1,129 @@
/**************************************************************************/
/* auth_panel.h */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifndef AUTH_PANEL_H
#define AUTH_PANEL_H
#ifdef TOOLS_ENABLED
#include "scene/gui/box_container.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/button.h"
#include "scene/gui/label.h"
#include "scene/gui/texture_rect.h"
#include "scene/gui/check_button.h"
#include "scene/gui/link_button.h"
#include "scene/gui/text_edit.h"
#include "scene/gui/separator.h"
#include "scene/gui/color_rect.h"
#include "scene/gui/control.h"
class AethexLauncher;
class AuthPanel : public Control {
GDCLASS(AuthPanel, Control);
private:
bool setup_done = false;
AethexLauncher *launcher = nullptr;
// Main card
PanelContainer *auth_card = nullptr;
// Login/signup tabs
HBoxContainer *tab_bar = nullptr;
Button *login_tab = nullptr;
Button *signup_tab = nullptr;
// Login form
VBoxContainer *login_form = nullptr;
LineEdit *login_email = nullptr;
LineEdit *login_password = nullptr;
CheckButton *remember_me = nullptr;
Button *login_button = nullptr;
LinkButton *forgot_password = nullptr;
// Signup form
VBoxContainer *signup_form = nullptr;
LineEdit *signup_email = nullptr;
LineEdit *signup_username = nullptr;
LineEdit *signup_password = nullptr;
LineEdit *signup_confirm = nullptr;
CheckButton *terms_checkbox = nullptr;
Button *signup_button = nullptr;
// OAuth buttons
VBoxContainer *oauth_buttons = nullptr;
Button *google_button = nullptr;
Button *discord_button = nullptr;
Button *github_button = nullptr;
// Skip button
LinkButton *skip_button = nullptr;
// Error display
Label *error_label = nullptr;
// Profile setup (after signup)
VBoxContainer *profile_setup = nullptr;
LineEdit *setup_display_name = nullptr;
Button *setup_avatar_button = nullptr;
TextEdit *setup_bio = nullptr;
Button *setup_complete_button = nullptr;
LinkButton *setup_skip_button = nullptr;
// Loading overlay
VBoxContainer *loading_overlay = nullptr;
TextureRect *loading_spinner = nullptr;
Label *loading_label = nullptr;
// State
bool is_signup_mode = false;
bool showing_auth = false;
void _setup_ui();
void _show_login();
void _show_signup();
void _show_forgot_password();
void _on_login_pressed();
void _on_login_submitted(const String &p_text);
void _on_signup_pressed();
void _oauth_google();
void _oauth_discord();
void _oauth_github();
void _skip_auth();
void _show_error(const String &p_message);
void _show_loading(const String &p_message);
void _show_profile_setup();
void _complete_profile_setup();
void _skip_profile_setup();
void _on_authenticated(const Dictionary &p_user);
void _on_auth_error(const String &p_error);
protected:
static void _bind_methods();
void _notification(int p_what);
public:
AuthPanel();
~AuthPanel();
void set_launcher(AethexLauncher *p_launcher);
void show_auth();
};
#endif // TOOLS_ENABLED
#endif // AUTH_PANEL_H

View file

@ -0,0 +1,442 @@
/**************************************************************************/
/* downloads_panel.cpp */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifdef TOOLS_ENABLED
#include "downloads_panel.h"
#include "../aethex_launcher.h"
#include "../data/download_manager.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/separator.h"
// DownloadItemRow implementation
void DownloadItemRow::_bind_methods() {
ADD_SIGNAL(MethodInfo("pause_toggled", PropertyInfo(Variant::STRING, "task_id")));
ADD_SIGNAL(MethodInfo("cancel_requested", PropertyInfo(Variant::STRING, "task_id")));
}
DownloadItemRow::DownloadItemRow() {
}
DownloadItemRow::~DownloadItemRow() {
}
void DownloadItemRow::_notification(int p_what) {
if (p_what == NOTIFICATION_ENTER_TREE && !setup_done) {
_setup_ui();
}
}
void DownloadItemRow::_setup_ui() {
if (setup_done) return;
setup_done = true;
set_h_size_flags(SIZE_EXPAND_FILL);
// Icon
icon = memnew(TextureRect);
icon->set_custom_minimum_size(Size2(48 * EDSCALE, 48 * EDSCALE));
add_child(icon);
// Info column
VBoxContainer *info = memnew(VBoxContainer);
info->set_h_size_flags(SIZE_EXPAND_FILL);
add_child(info);
// Top row: title + status
HBoxContainer *top_row = memnew(HBoxContainer);
info->add_child(top_row);
title_label = memnew(Label);
title_label->set_h_size_flags(SIZE_EXPAND_FILL);
top_row->add_child(title_label);
status_label = memnew(Label);
top_row->add_child(status_label);
// Progress bar
progress_bar = memnew(ProgressBar);
progress_bar->set_max(100);
info->add_child(progress_bar);
// Bottom row: size + speed + ETA
HBoxContainer *bottom_row = memnew(HBoxContainer);
info->add_child(bottom_row);
size_label = memnew(Label);
size_label->add_theme_font_size_override("font_size", 10 * EDSCALE);
bottom_row->add_child(size_label);
Control *spacer = memnew(Control);
spacer->set_h_size_flags(SIZE_EXPAND_FILL);
bottom_row->add_child(spacer);
speed_label = memnew(Label);
speed_label->add_theme_font_size_override("font_size", 10 * EDSCALE);
bottom_row->add_child(speed_label);
eta_label = memnew(Label);
eta_label->add_theme_font_size_override("font_size", 10 * EDSCALE);
bottom_row->add_child(eta_label);
// Buttons
pause_button = memnew(Button);
pause_button->set_text("Pause");
pause_button->set_toggle_mode(true);
pause_button->connect("toggled", callable_mp(this, &DownloadItemRow::_on_pause_toggled));
add_child(pause_button);
cancel_button = memnew(Button);
cancel_button->set_text("X");
cancel_button->connect("pressed", callable_mp(this, &DownloadItemRow::_on_cancel_pressed));
add_child(cancel_button);
}
void DownloadItemRow::set_task(const Ref<DownloadTask> &p_task) {
task = p_task;
_update_display();
}
void DownloadItemRow::_update_display() {
if (task.is_null()) return;
title_label->set_text(task->get_filename());
// Status
String status;
switch (task->get_status()) {
case DownloadTask::STATUS_PENDING: status = "Pending"; break;
case DownloadTask::STATUS_DOWNLOADING: status = "Downloading"; break;
case DownloadTask::STATUS_PAUSED: status = "Paused"; break;
case DownloadTask::STATUS_COMPLETED: status = "Completed"; break;
case DownloadTask::STATUS_FAILED: status = "Failed"; break;
case DownloadTask::STATUS_INSTALLING: status = "Installing"; break;
}
status_label->set_text(status);
// Progress
progress_bar->set_value(task->get_progress() * 100);
// Size
int64_t downloaded = task->get_downloaded_bytes();
int64_t total = task->get_total_bytes();
size_label->set_text(vformat("%s / %s", _format_size(downloaded), _format_size(total)));
// Speed
float speed = task->get_speed();
speed_label->set_text(vformat("%s/s", _format_size((int64_t)speed)));
// ETA
int eta_secs = task->get_eta_seconds();
if (eta_secs > 0) {
int mins = eta_secs / 60;
int secs = eta_secs % 60;
eta_label->set_text(vformat("%02d:%02d remaining", mins, secs));
} else {
eta_label->set_text("");
}
// Button states
bool is_active = task->get_status() == DownloadTask::STATUS_DOWNLOADING ||
task->get_status() == DownloadTask::STATUS_PAUSED;
pause_button->set_visible(is_active);
cancel_button->set_visible(is_active || task->get_status() == DownloadTask::STATUS_PENDING);
pause_button->set_pressed(task->get_status() == DownloadTask::STATUS_PAUSED);
pause_button->set_text(task->get_status() == DownloadTask::STATUS_PAUSED ? "Resume" : "Pause");
}
String DownloadItemRow::_format_size(int64_t p_bytes) {
if (p_bytes < 1024) {
return vformat("%d B", p_bytes);
} else if (p_bytes < 1024 * 1024) {
return vformat("%.1f KB", p_bytes / 1024.0);
} else if (p_bytes < 1024 * 1024 * 1024) {
return vformat("%.1f MB", p_bytes / (1024.0 * 1024.0));
} else {
return vformat("%.2f GB", p_bytes / (1024.0 * 1024.0 * 1024.0));
}
}
void DownloadItemRow::_on_pause_toggled(bool p_pressed) {
if (task.is_valid()) {
emit_signal("pause_toggled", task->get_id());
}
}
void DownloadItemRow::_on_cancel_pressed() {
if (task.is_valid()) {
emit_signal("cancel_requested", task->get_id());
}
}
// DownloadsPanel implementation
void DownloadsPanel::_bind_methods() {
ClassDB::bind_method(D_METHOD("refresh"), &DownloadsPanel::refresh);
}
DownloadsPanel::DownloadsPanel() {
}
DownloadsPanel::~DownloadsPanel() {
}
void DownloadsPanel::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_TREE: {
if (!setup_done) {
_setup_ui();
}
} break;
case NOTIFICATION_PROCESS: {
// Update downloads every frame when visible
if (is_visible_in_tree()) {
_update_all_downloads();
}
} break;
}
}
void DownloadsPanel::_setup_ui() {
if (setup_done) return;
setup_done = true;
// Toolbar
HBoxContainer *toolbar = memnew(HBoxContainer);
add_child(toolbar);
pause_all_button = memnew(Button);
pause_all_button->set_text("Pause All");
pause_all_button->connect("pressed", callable_mp(this, &DownloadsPanel::_pause_all));
toolbar->add_child(pause_all_button);
resume_all_button = memnew(Button);
resume_all_button->set_text("Resume All");
resume_all_button->connect("pressed", callable_mp(this, &DownloadsPanel::_resume_all));
toolbar->add_child(resume_all_button);
Control *toolbar_spacer = memnew(Control);
toolbar_spacer->set_h_size_flags(SIZE_EXPAND_FILL);
toolbar->add_child(toolbar_spacer);
clear_completed_button = memnew(Button);
clear_completed_button->set_text("Clear Completed");
clear_completed_button->connect("pressed", callable_mp(this, &DownloadsPanel::_clear_completed));
toolbar->add_child(clear_completed_button);
add_child(memnew(HSeparator));
// Main scroll
ScrollContainer *scroll = memnew(ScrollContainer);
scroll->set_v_size_flags(SIZE_EXPAND_FILL);
add_child(scroll);
VBoxContainer *content = memnew(VBoxContainer);
content->set_h_size_flags(SIZE_EXPAND_FILL);
scroll->add_child(content);
// Active downloads section
Label *active_label = memnew(Label);
active_label->set_text("Active Downloads");
active_label->add_theme_font_size_override("font_size", 16 * EDSCALE);
content->add_child(active_label);
active_downloads = memnew(VBoxContainer);
content->add_child(active_downloads);
// Pending section
Label *pending_label = memnew(Label);
pending_label->set_text("Pending");
pending_label->add_theme_font_size_override("font_size", 16 * EDSCALE);
content->add_child(pending_label);
pending_downloads = memnew(VBoxContainer);
content->add_child(pending_downloads);
// Completed section
completed_header = memnew(HBoxContainer);
content->add_child(completed_header);
Label *completed_label = memnew(Label);
completed_label->set_text("Completed");
completed_label->add_theme_font_size_override("font_size", 16 * EDSCALE);
completed_header->add_child(completed_label);
completed_downloads = memnew(VBoxContainer);
content->add_child(completed_downloads);
// Empty state
empty_state = memnew(Label);
empty_state->set_text("No downloads.\nVisit the Store to find games to download.");
empty_state->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
empty_state->set_v_size_flags(SIZE_EXPAND_FILL);
content->add_child(empty_state);
set_process(true);
}
void DownloadsPanel::set_launcher(AethexLauncher *p_launcher) {
launcher = p_launcher;
if (launcher) {
auto dm = launcher->get_download_manager();
if (dm.is_valid()) {
dm->connect("download_started", callable_mp(this, &DownloadsPanel::_on_download_started));
dm->connect("download_progress", callable_mp(this, &DownloadsPanel::_on_download_progress));
dm->connect("download_completed", callable_mp(this, &DownloadsPanel::_on_download_completed));
dm->connect("download_failed", callable_mp(this, &DownloadsPanel::_on_download_failed));
}
}
}
void DownloadsPanel::refresh() {
if (!active_downloads) return; // Not setup yet
_load_downloads();
}
void DownloadsPanel::_load_downloads() {
if (!active_downloads || !pending_downloads || !completed_downloads) return;
// Clear all sections
_clear_container(active_downloads);
_clear_container(pending_downloads);
_clear_container(completed_downloads);
download_rows.clear();
if (!launcher) {
empty_state->set_visible(true);
return;
}
auto dm = launcher->get_download_manager();
if (dm.is_null()) {
empty_state->set_visible(true);
return;
}
TypedArray<DownloadTask> active = dm->get_active_downloads();
TypedArray<DownloadTask> pending = dm->get_pending_downloads();
TypedArray<DownloadTask> completed = dm->get_completed_downloads();
bool has_any = active.size() > 0 || pending.size() > 0 || completed.size() > 0;
empty_state->set_visible(!has_any);
// Add active downloads
for (int i = 0; i < active.size(); i++) {
Ref<DownloadTask> task = active[i];
DownloadItemRow *row = memnew(DownloadItemRow);
row->set_task(task);
row->connect("pause_toggled", callable_mp(this, &DownloadsPanel::_toggle_pause));
row->connect("cancel_requested", callable_mp(this, &DownloadsPanel::_cancel_download));
active_downloads->add_child(row);
download_rows[task->get_id()] = row;
}
// Add pending downloads
for (int i = 0; i < pending.size(); i++) {
Ref<DownloadTask> task = pending[i];
DownloadItemRow *row = memnew(DownloadItemRow);
row->set_task(task);
row->connect("cancel_requested", callable_mp(this, &DownloadsPanel::_cancel_download));
pending_downloads->add_child(row);
download_rows[task->get_id()] = row;
}
// Add completed downloads
for (int i = 0; i < completed.size(); i++) {
Ref<DownloadTask> task = completed[i];
DownloadItemRow *row = memnew(DownloadItemRow);
row->set_task(task);
completed_downloads->add_child(row);
download_rows[task->get_id()] = row;
}
}
void DownloadsPanel::_clear_container(VBoxContainer *p_container) {
if (!p_container) return;
for (int i = p_container->get_child_count() - 1; i >= 0; i--) {
Node *child = p_container->get_child(i);
p_container->remove_child(child);
child->queue_free();
}
}
void DownloadsPanel::_update_all_downloads() {
for (KeyValue<String, DownloadItemRow *> &kv : download_rows) {
if (kv.value) {
kv.value->_update_display();
}
}
}
void DownloadsPanel::_toggle_pause(const String &p_task_id) {
if (!launcher) return;
auto dm = launcher->get_download_manager();
if (dm.is_valid()) {
Ref<DownloadTask> task = dm->get_download(p_task_id);
if (task.is_valid()) {
if (task->get_status() == DownloadTask::STATUS_PAUSED) {
dm->resume_download(p_task_id);
} else {
dm->pause_download(p_task_id);
}
}
}
}
void DownloadsPanel::_cancel_download(const String &p_task_id) {
if (!launcher) return;
auto dm = launcher->get_download_manager();
if (dm.is_valid()) {
dm->cancel_download(p_task_id);
refresh();
}
}
void DownloadsPanel::_pause_all() {
if (!launcher) return;
auto dm = launcher->get_download_manager();
if (dm.is_valid()) {
dm->pause_all();
}
}
void DownloadsPanel::_resume_all() {
if (!launcher) return;
auto dm = launcher->get_download_manager();
if (dm.is_valid()) {
dm->resume_all();
}
}
void DownloadsPanel::_clear_completed() {
if (!launcher) return;
auto dm = launcher->get_download_manager();
if (dm.is_valid()) {
dm->clear_completed();
refresh();
}
}
void DownloadsPanel::_on_download_started(const String &p_task_id) {
refresh();
}
void DownloadsPanel::_on_download_progress(const String &p_task_id, float p_progress) {
// Updates handled by _update_all_downloads
}
void DownloadsPanel::_on_download_completed(const String &p_task_id) {
refresh();
}
void DownloadsPanel::_on_download_failed(const String &p_task_id, const String &p_error) {
refresh();
}
#endif // TOOLS_ENABLED

View file

@ -0,0 +1,111 @@
/**************************************************************************/
/* downloads_panel.h */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifndef DOWNLOADS_PANEL_H
#define DOWNLOADS_PANEL_H
#ifdef TOOLS_ENABLED
#include "scene/gui/box_container.h"
#include "scene/gui/scroll_container.h"
#include "scene/gui/progress_bar.h"
#include "scene/gui/button.h"
#include "scene/gui/label.h"
#include "scene/gui/texture_rect.h"
class AethexLauncher;
class DownloadTask;
class DownloadItemRow : public HBoxContainer {
GDCLASS(DownloadItemRow, HBoxContainer);
private:
bool setup_done = false;
Ref<DownloadTask> task;
TextureRect *icon = nullptr;
Label *title_label = nullptr;
Label *status_label = nullptr;
Label *size_label = nullptr;
Label *speed_label = nullptr;
Label *eta_label = nullptr;
ProgressBar *progress_bar = nullptr;
Button *pause_button = nullptr;
Button *cancel_button = nullptr;
void _setup_ui();
void _on_pause_toggled(bool p_pressed);
void _on_cancel_pressed();
String _format_size(int64_t p_bytes);
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void set_task(const Ref<DownloadTask> &p_task);
void _update_display();
DownloadItemRow();
~DownloadItemRow();
};
class DownloadsPanel : public VBoxContainer {
GDCLASS(DownloadsPanel, VBoxContainer);
private:
AethexLauncher *launcher = nullptr;
bool setup_done = false;
// Header/Toolbar
Button *pause_all_button = nullptr;
Button *resume_all_button = nullptr;
Button *clear_completed_button = nullptr;
// Sections
VBoxContainer *active_downloads = nullptr;
VBoxContainer *pending_downloads = nullptr;
VBoxContainer *completed_downloads = nullptr;
HBoxContainer *completed_header = nullptr;
// Empty state
Label *empty_state = nullptr;
// Row tracking
HashMap<String, DownloadItemRow *> download_rows;
void _setup_ui();
void _load_downloads();
void _clear_container(VBoxContainer *p_container);
void _update_all_downloads();
void _toggle_pause(const String &p_task_id);
void _cancel_download(const String &p_task_id);
void _pause_all();
void _resume_all();
void _clear_completed();
void _on_download_started(const String &p_task_id);
void _on_download_progress(const String &p_task_id, float p_progress);
void _on_download_completed(const String &p_task_id);
void _on_download_failed(const String &p_task_id, const String &p_error);
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void set_launcher(AethexLauncher *p_launcher);
void refresh();
DownloadsPanel();
~DownloadsPanel();
};
#endif // TOOLS_ENABLED
#endif // DOWNLOADS_PANEL_H

View file

@ -0,0 +1,426 @@
/**************************************************************************/
/* friends_panel.cpp */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifdef TOOLS_ENABLED
#include "friends_panel.h"
#include "../aethex_launcher.h"
#include "../data/friend_system.h"
#include "editor/themes/editor_scale.h"
// ============================================================================
// FriendRow
// ============================================================================
void FriendRow::_bind_methods() {
ADD_SIGNAL(MethodInfo("message_clicked", PropertyInfo(Variant::STRING, "friend_id")));
ADD_SIGNAL(MethodInfo("profile_clicked", PropertyInfo(Variant::STRING, "friend_id")));
ADD_SIGNAL(MethodInfo("remove_clicked", PropertyInfo(Variant::STRING, "friend_id")));
}
FriendRow::FriendRow() {
}
FriendRow::~FriendRow() {
}
void FriendRow::_notification(int p_what) {
if (p_what == NOTIFICATION_ENTER_TREE && !setup_done) {
_setup_ui();
}
}
void FriendRow::_setup_ui() {
if (setup_done) return;
setup_done = true;
set_h_size_flags(SIZE_EXPAND_FILL);
avatar = memnew(TextureRect);
avatar->set_custom_minimum_size(Size2(40 * EDSCALE, 40 * EDSCALE));
add_child(avatar);
status_indicator = memnew(ColorRect);
status_indicator->set_custom_minimum_size(Size2(10 * EDSCALE, 10 * EDSCALE));
add_child(status_indicator);
VBoxContainer *info = memnew(VBoxContainer);
info->set_h_size_flags(SIZE_EXPAND_FILL);
add_child(info);
HBoxContainer *name_row = memnew(HBoxContainer);
info->add_child(name_row);
name_label = memnew(Label);
name_row->add_child(name_label);
nickname_label = memnew(Label);
nickname_label->add_theme_font_size_override("font_size", 10 * EDSCALE);
name_row->add_child(nickname_label);
status_label = memnew(Label);
status_label->add_theme_font_size_override("font_size", 10 * EDSCALE);
info->add_child(status_label);
game_label = memnew(Label);
game_label->add_theme_font_size_override("font_size", 10 * EDSCALE);
info->add_child(game_label);
message_button = memnew(Button);
message_button->set_text("Message");
message_button->connect("pressed", callable_mp(this, &FriendRow::_on_message_pressed));
add_child(message_button);
menu_button = memnew(MenuButton);
menu_button->set_text("...");
PopupMenu *menu = menu_button->get_popup();
menu->add_item("View Profile", 0);
menu->add_item("Set Nickname", 1);
menu->add_separator();
menu->add_item("Remove Friend", 2);
menu->add_item("Block", 3);
menu->connect("id_pressed", callable_mp(this, &FriendRow::_on_menu_selected));
add_child(menu_button);
}
void FriendRow::set_friend_data(const Ref<Friend> &p_friend) {
friend_data = p_friend;
_update_display();
}
void FriendRow::_update_display() {
if (friend_data.is_null()) return;
name_label->set_text(friend_data->get_display_name());
String nickname = friend_data->get_nickname();
if (!nickname.is_empty()) {
nickname_label->set_text("(" + nickname + ")");
nickname_label->set_visible(true);
} else {
nickname_label->set_visible(false);
}
Color status_color = Color(0.5, 0.5, 0.5);
String status_text = "Offline";
switch (friend_data->get_status()) {
case Friend::STATUS_ONLINE:
status_color = Color(0.3, 0.8, 0.3);
status_text = "Online";
break;
case Friend::STATUS_AWAY:
status_color = Color(0.9, 0.7, 0.2);
status_text = "Away";
break;
case Friend::STATUS_BUSY:
status_color = Color(0.8, 0.3, 0.3);
status_text = "Busy";
break;
default:
break;
}
status_indicator->set_color(status_color);
status_label->set_text(status_text);
String current_game = friend_data->get_current_game();
if (!current_game.is_empty()) {
game_label->set_text("Playing: " + current_game);
game_label->set_visible(true);
} else {
game_label->set_visible(false);
}
}
void FriendRow::_on_message_pressed() {
if (friend_data.is_valid()) {
emit_signal("message_clicked", friend_data->get_id());
}
}
void FriendRow::_on_menu_selected(int p_id) {
if (friend_data.is_null()) return;
switch (p_id) {
case 0:
emit_signal("profile_clicked", friend_data->get_id());
break;
case 2:
emit_signal("remove_clicked", friend_data->get_id());
break;
}
}
// ============================================================================
// FriendsPanel
// ============================================================================
void FriendsPanel::_bind_methods() {
ClassDB::bind_method(D_METHOD("refresh"), &FriendsPanel::refresh);
}
FriendsPanel::FriendsPanel() {
}
FriendsPanel::~FriendsPanel() {
}
void FriendsPanel::_notification(int p_what) {
if (p_what == NOTIFICATION_ENTER_TREE && !setup_done) {
_setup_ui();
}
}
void FriendsPanel::_setup_ui() {
if (setup_done) return;
setup_done = true;
_setup_header();
_setup_sections();
_setup_add_dialog();
}
void FriendsPanel::_setup_header() {
header = memnew(HBoxContainer);
add_child(header);
tab_bar = memnew(TabBar);
tab_bar->add_tab("All Friends");
tab_bar->add_tab("Online");
tab_bar->add_tab("Pending");
tab_bar->connect("tab_changed", callable_mp(this, &FriendsPanel::_on_tab_changed));
header->add_child(tab_bar);
search_bar = memnew(LineEdit);
search_bar->set_placeholder("Search friends...");
search_bar->set_h_size_flags(SIZE_EXPAND_FILL);
search_bar->connect("text_changed", callable_mp(this, &FriendsPanel::_on_search_changed));
header->add_child(search_bar);
add_friend_button = memnew(Button);
add_friend_button->set_text("Add Friend");
add_friend_button->connect("pressed", callable_mp(this, &FriendsPanel::_on_add_friend));
header->add_child(add_friend_button);
// Stats
stats = memnew(HBoxContainer);
add_child(stats);
online_count_label = memnew(Label);
online_count_label->set_text("0 Online");
stats->add_child(online_count_label);
total_count_label = memnew(Label);
total_count_label->set_text("0 Friends");
stats->add_child(total_count_label);
pending_count_label = memnew(Label);
pending_count_label->set_text("0 Pending");
stats->add_child(pending_count_label);
}
void FriendsPanel::_setup_sections() {
scroll = memnew(ScrollContainer);
scroll->set_v_size_flags(SIZE_EXPAND_FILL);
add_child(scroll);
content = memnew(VBoxContainer);
content->set_h_size_flags(SIZE_EXPAND_FILL);
scroll->add_child(content);
// Online section
online_section = memnew(VBoxContainer);
content->add_child(online_section);
Label *online_header = memnew(Label);
online_header->set_text("Online");
online_section->add_child(online_header);
online_list = memnew(VBoxContainer);
online_section->add_child(online_list);
// Offline section
offline_section = memnew(VBoxContainer);
content->add_child(offline_section);
Label *offline_header = memnew(Label);
offline_header->set_text("Offline");
offline_section->add_child(offline_header);
offline_list = memnew(VBoxContainer);
offline_section->add_child(offline_list);
// Pending section
pending_section = memnew(VBoxContainer);
content->add_child(pending_section);
Label *pending_header = memnew(Label);
pending_header->set_text("Pending Requests");
pending_section->add_child(pending_header);
pending_list = memnew(VBoxContainer);
pending_section->add_child(pending_list);
// Blocked section
blocked_section = memnew(VBoxContainer);
blocked_section->set_visible(false);
content->add_child(blocked_section);
blocked_list = memnew(VBoxContainer);
blocked_section->add_child(blocked_list);
}
void FriendsPanel::_setup_add_dialog() {
add_dialog = memnew(VBoxContainer);
add_dialog->set_visible(false);
add_child(add_dialog);
add_username_input = memnew(LineEdit);
add_username_input->set_placeholder("Enter username");
add_dialog->add_child(add_username_input);
add_send_button = memnew(Button);
add_send_button->set_text("Send Request");
add_send_button->connect("pressed", callable_mp(this, &FriendsPanel::_on_send_request));
add_dialog->add_child(add_send_button);
}
void FriendsPanel::set_launcher(AethexLauncher *p_launcher) {
launcher = p_launcher;
}
void FriendsPanel::refresh() {
if (!online_list || !offline_list) return; // Not setup yet
_refresh_friends();
}
void FriendsPanel::_refresh_friends() {
if (!online_list || !offline_list) return;
// Clear existing rows
for (int i = 0; i < friend_rows.size(); i++) {
if (friend_rows[i]) {
friend_rows[i]->queue_free();
}
}
friend_rows.clear();
if (!launcher) return;
auto fs = launcher->get_friend_system();
if (fs.is_null()) return;
TypedArray<Friend> friends = fs->get_friends();
int online_count = 0;
for (int i = 0; i < friends.size(); i++) {
Ref<Friend> f = friends[i];
if (f.is_null()) continue;
FriendRow *row = memnew(FriendRow);
row->set_friend_data(f);
if (f->get_status() == Friend::STATUS_ONLINE ||
f->get_status() == Friend::STATUS_AWAY ||
f->get_status() == Friend::STATUS_BUSY) {
online_list->add_child(row);
online_count++;
} else {
offline_list->add_child(row);
}
friend_rows.push_back(row);
}
_update_stats();
}
void FriendsPanel::_update_stats() {
if (!launcher) return;
auto fs = launcher->get_friend_system();
if (fs.is_null()) return;
int online = 0;
int total = 0;
int pending = 0;
TypedArray<Friend> friends = fs->get_friends();
total = friends.size();
for (int i = 0; i < friends.size(); i++) {
Ref<Friend> f = friends[i];
if (f.is_valid() && f->get_status() != Friend::STATUS_OFFLINE) {
online++;
}
}
online_count_label->set_text(vformat("%d Online", online));
total_count_label->set_text(vformat("%d Friends", total));
pending_count_label->set_text(vformat("%d Pending", pending));
}
void FriendsPanel::_apply_filter(const String &p_query) {
_refresh_friends();
}
void FriendsPanel::_on_tab_changed(int p_idx) {
_refresh_friends();
}
void FriendsPanel::_on_search_changed(const String &p_text) {
_apply_filter(p_text);
}
void FriendsPanel::_on_add_friend() {
add_dialog->set_visible(!add_dialog->is_visible());
}
void FriendsPanel::_on_send_request() {
if (!launcher) return;
String username = add_username_input->get_text().strip_edges();
if (username.is_empty()) return;
auto fs = launcher->get_friend_system();
if (fs.is_valid()) {
fs->send_friend_request(username);
}
add_username_input->set_text("");
add_dialog->set_visible(false);
}
void FriendsPanel::_on_accept_request(const String &p_request_id) {
// TODO: Implement
}
void FriendsPanel::_on_decline_request(const String &p_request_id) {
// TODO: Implement
}
void FriendsPanel::_on_remove_friend(const String &p_friend_id) {
if (!launcher) return;
auto fs = launcher->get_friend_system();
if (fs.is_valid()) {
fs->remove_friend(p_friend_id);
refresh();
}
}
void FriendsPanel::_on_block_user(const String &p_user_id) {
// TODO: Implement
}
void FriendsPanel::_on_friends_loaded() {
_refresh_friends();
}
void FriendsPanel::_on_friend_status_changed(const String &p_id, const String &p_status) {
_refresh_friends();
}
#endif // TOOLS_ENABLED

View file

@ -0,0 +1,136 @@
/**************************************************************************/
/* friends_panel.h */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifndef FRIENDS_PANEL_H
#define FRIENDS_PANEL_H
#ifdef TOOLS_ENABLED
#include "scene/gui/box_container.h"
#include "scene/gui/scroll_container.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/button.h"
#include "scene/gui/label.h"
#include "scene/gui/texture_rect.h"
#include "scene/gui/tab_bar.h"
#include "scene/gui/color_rect.h"
#include "scene/gui/menu_button.h"
class AethexLauncher;
class Friend;
class FriendRow : public HBoxContainer {
GDCLASS(FriendRow, HBoxContainer);
private:
bool setup_done = false;
Ref<Friend> friend_data;
TextureRect *avatar = nullptr;
ColorRect *status_indicator = nullptr;
Label *name_label = nullptr;
Label *nickname_label = nullptr;
Label *status_label = nullptr;
Label *game_label = nullptr;
Button *message_button = nullptr;
MenuButton *menu_button = nullptr;
void _setup_ui();
void _update_display();
void _on_message_pressed();
void _on_menu_selected(int p_id);
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void set_friend_data(const Ref<Friend> &p_friend);
FriendRow();
~FriendRow();
};
class FriendsPanel : public VBoxContainer {
GDCLASS(FriendsPanel, VBoxContainer);
private:
AethexLauncher *launcher = nullptr;
bool setup_done = false;
// Header
HBoxContainer *header = nullptr;
TabBar *tab_bar = nullptr;
LineEdit *search_bar = nullptr;
Button *add_friend_button = nullptr;
// Stats
HBoxContainer *stats = nullptr;
Label *online_count_label = nullptr;
Label *total_count_label = nullptr;
Label *pending_count_label = nullptr;
// Content
ScrollContainer *scroll = nullptr;
VBoxContainer *content = nullptr;
// Sections
VBoxContainer *online_section = nullptr;
VBoxContainer *online_list = nullptr;
VBoxContainer *offline_section = nullptr;
VBoxContainer *offline_list = nullptr;
VBoxContainer *pending_section = nullptr;
VBoxContainer *pending_list = nullptr;
VBoxContainer *blocked_section = nullptr;
VBoxContainer *blocked_list = nullptr;
// Add friend dialog
VBoxContainer *add_dialog = nullptr;
LineEdit *add_username_input = nullptr;
Button *add_send_button = nullptr;
Vector<FriendRow *> friend_rows;
void _setup_ui();
void _setup_header();
void _setup_sections();
void _setup_add_dialog();
void _refresh_friends();
void _update_stats();
void _apply_filter(const String &p_query);
void _on_tab_changed(int p_idx);
void _on_search_changed(const String &p_text);
void _on_add_friend();
void _on_send_request();
void _on_accept_request(const String &p_request_id);
void _on_decline_request(const String &p_request_id);
void _on_remove_friend(const String &p_friend_id);
void _on_block_user(const String &p_user_id);
void _on_friends_loaded();
void _on_friend_status_changed(const String &p_id, const String &p_status);
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void set_launcher(AethexLauncher *p_launcher);
void refresh();
FriendsPanel();
~FriendsPanel();
};
#endif // TOOLS_ENABLED
#endif // FRIENDS_PANEL_H

View file

@ -0,0 +1,590 @@
/**************************************************************************/
/* launcher_panel.cpp */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifdef TOOLS_ENABLED
#include "launcher_panel.h"
#include "library_panel.h"
#include "store_panel.h"
#include "downloads_panel.h"
#include "friends_panel.h"
#include "profile_panel.h"
#include "auth_panel.h"
#include "../aethex_launcher.h"
#include "editor/editor_string_names.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/panel_container.h"
#include "scene/resources/style_box_flat.h"
void LauncherPanel::_bind_methods() {
ClassDB::bind_method(D_METHOD("navigate_to", "section"), &LauncherPanel::navigate_to);
ClassDB::bind_method(D_METHOD("show_game_details", "game_id"), &LauncherPanel::show_game_details);
ADD_SIGNAL(MethodInfo("section_changed", PropertyInfo(Variant::STRING, "section")));
}
LauncherPanel::LauncherPanel() {
set_v_size_flags(SIZE_EXPAND_FILL);
set_h_size_flags(SIZE_EXPAND_FILL);
}
LauncherPanel::~LauncherPanel() {
}
void LauncherPanel::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_TREE: {
// Don't setup UI in ENTER_TREE - wait for set_launcher
} break;
case NOTIFICATION_THEME_CHANGED: {
// Update theme
} break;
}
}
void LauncherPanel::_setup_ui() {
// Main split: sidebar | content
main_split = memnew(HSplitContainer);
main_split->set_v_size_flags(SIZE_EXPAND_FILL);
main_split->set_h_size_flags(SIZE_EXPAND_FILL);
add_child(main_split);
_setup_sidebar();
// Content area - right side
content_area = memnew(VBoxContainer);
content_area->set_h_size_flags(SIZE_EXPAND_FILL);
content_area->set_v_size_flags(SIZE_EXPAND_FILL);
main_split->add_child(content_area);
_setup_header();
_setup_home();
_setup_panels(); // Create the sub-panels
// Show home by default
_show_home();
}
void LauncherPanel::_setup_sidebar() {
// Sidebar with dark background
PanelContainer *sidebar_bg = memnew(PanelContainer);
sidebar_bg->set_custom_minimum_size(Size2(220 * EDSCALE, 0));
// Add dark background style
Ref<StyleBoxFlat> sidebar_style;
sidebar_style.instantiate();
sidebar_style->set_bg_color(Color(0.12, 0.12, 0.14, 1.0)); // Dark gray
sidebar_style->set_content_margin_all(8 * EDSCALE);
sidebar_bg->add_theme_style_override("panel", sidebar_style);
main_split->add_child(sidebar_bg);
sidebar = memnew(VBoxContainer);
sidebar->add_theme_constant_override("separation", 4 * EDSCALE);
sidebar_bg->add_child(sidebar);
// Logo with gradient accent
HBoxContainer *logo_row = memnew(HBoxContainer);
sidebar->add_child(logo_row);
Label *logo = memnew(Label);
logo->set_text("AeThex");
logo->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_LEFT);
logo->add_theme_font_size_override("font_size", 28 * EDSCALE);
logo->add_theme_color_override("font_color", Color(0.4, 0.6, 1.0)); // Blue accent
logo_row->add_child(logo);
Control *logo_spacer = memnew(Control);
logo_spacer->set_h_size_flags(SIZE_EXPAND_FILL);
logo_row->add_child(logo_spacer);
sidebar->add_child(memnew(Control)); // Spacing
// Navigation buttons instead of tree
nav_tree = memnew(Tree);
nav_tree->set_hide_root(true);
nav_tree->set_hide_folding(true);
nav_tree->set_v_size_flags(SIZE_EXPAND_FILL);
nav_tree->set_select_mode(Tree::SELECT_SINGLE);
nav_tree->connect("item_selected", callable_mp(this, &LauncherPanel::_on_nav_selected));
// Style the tree to look like buttons
Ref<StyleBoxFlat> tree_bg;
tree_bg.instantiate();
tree_bg->set_bg_color(Color(0, 0, 0, 0)); // Transparent
nav_tree->add_theme_style_override("panel", tree_bg);
nav_tree->add_theme_style_override("focus", tree_bg);
// Selected item style
Ref<StyleBoxFlat> tree_selected;
tree_selected.instantiate();
tree_selected->set_bg_color(Color(0.3, 0.5, 0.9, 0.3)); // Blue highlight
tree_selected->set_corner_radius_all(4 * EDSCALE);
nav_tree->add_theme_style_override("selected", tree_selected);
nav_tree->add_theme_style_override("selected_focus", tree_selected);
// Hover style
Ref<StyleBoxFlat> tree_hover;
tree_hover.instantiate();
tree_hover->set_bg_color(Color(1, 1, 1, 0.05));
tree_hover->set_corner_radius_all(4 * EDSCALE);
nav_tree->add_theme_style_override("hover", tree_hover);
nav_tree->add_theme_color_override("font_color", Color(0.85, 0.85, 0.9));
nav_tree->add_theme_color_override("font_selected_color", Color(1, 1, 1));
sidebar->add_child(nav_tree);
TreeItem *root = nav_tree->create_item();
nav_home = nav_tree->create_item(root);
nav_home->set_text(0, " 🏠 Home");
nav_home->set_metadata(0, "home");
nav_library = nav_tree->create_item(root);
nav_library->set_text(0, " 📚 Library");
nav_library->set_metadata(0, "library");
nav_store = nav_tree->create_item(root);
nav_store->set_text(0, " 🛒 Store");
nav_store->set_metadata(0, "store");
nav_downloads = nav_tree->create_item(root);
nav_downloads->set_text(0, " ⬇️ Downloads");
nav_downloads->set_metadata(0, "downloads");
nav_friends = nav_tree->create_item(root);
nav_friends->set_text(0, " 👥 Friends");
nav_friends->set_metadata(0, "friends");
nav_profile = nav_tree->create_item(root);
nav_profile->set_text(0, " 👤 Profile");
nav_profile->set_metadata(0, "profile");
nav_settings = nav_tree->create_item(root);
nav_settings->set_text(0, " ⚙️ Settings");
nav_settings->set_metadata(0, "settings");
nav_home->select(0);
}
void LauncherPanel::_setup_header() {
// Header with subtle background
PanelContainer *header_bg = memnew(PanelContainer);
Ref<StyleBoxFlat> header_style;
header_style.instantiate();
header_style->set_bg_color(Color(0.14, 0.14, 0.16, 1.0));
header_style->set_content_margin(SIDE_LEFT, 16 * EDSCALE);
header_style->set_content_margin(SIDE_RIGHT, 16 * EDSCALE);
header_style->set_content_margin(SIDE_TOP, 12 * EDSCALE);
header_style->set_content_margin(SIDE_BOTTOM, 12 * EDSCALE);
header_bg->add_theme_style_override("panel", header_style);
content_area->add_child(header_bg);
header = memnew(HBoxContainer);
header->add_theme_constant_override("separation", 12 * EDSCALE);
header_bg->add_child(header);
header_title = memnew(Label);
header_title->set_text("Home");
header_title->add_theme_font_size_override("font_size", 22 * EDSCALE);
header_title->add_theme_color_override("font_color", Color(0.95, 0.95, 0.98));
header->add_child(header_title);
Control *header_spacer = memnew(Control);
header_spacer->set_h_size_flags(SIZE_EXPAND_FILL);
header->add_child(header_spacer);
// Styled search bar
search_bar = memnew(LineEdit);
search_bar->set_placeholder("🔍 Search...");
search_bar->set_custom_minimum_size(Size2(250 * EDSCALE, 0));
Ref<StyleBoxFlat> search_style;
search_style.instantiate();
search_style->set_bg_color(Color(0.1, 0.1, 0.12, 1.0));
search_style->set_corner_radius_all(6 * EDSCALE);
search_style->set_content_margin(SIDE_LEFT, 12 * EDSCALE);
search_style->set_content_margin(SIDE_RIGHT, 12 * EDSCALE);
search_bar->add_theme_style_override("normal", search_style);
search_bar->connect("text_changed", callable_mp(this, &LauncherPanel::_on_search_changed));
header->add_child(search_bar);
// Styled user button
user_button = memnew(Button);
user_button->set_text("Sign In");
Ref<StyleBoxFlat> user_btn_style;
user_btn_style.instantiate();
user_btn_style->set_bg_color(Color(0.3, 0.5, 0.9, 1.0));
user_btn_style->set_corner_radius_all(6 * EDSCALE);
user_button->add_theme_style_override("normal", user_btn_style);
Ref<StyleBoxFlat> user_btn_hover;
user_btn_hover.instantiate();
user_btn_hover->set_bg_color(Color(0.35, 0.55, 0.95, 1.0));
user_btn_hover->set_corner_radius_all(6 * EDSCALE);
user_button->add_theme_style_override("hover", user_btn_hover);
user_button->connect("pressed", callable_mp(this, &LauncherPanel::_on_user_button_pressed));
header->add_child(user_button);
}
void LauncherPanel::_setup_home() {
// Main scroll container for home
ScrollContainer *home_scroll = memnew(ScrollContainer);
home_scroll->set_v_size_flags(SIZE_EXPAND_FILL);
home_scroll->set_h_size_flags(SIZE_EXPAND_FILL);
home_scroll->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
content_area->add_child(home_scroll);
home_panel = memnew(VBoxContainer);
home_panel->set_h_size_flags(SIZE_EXPAND_FILL);
home_panel->add_theme_constant_override("separation", 24 * EDSCALE);
home_scroll->add_child(home_panel);
// Hero section with gradient background
PanelContainer *hero_section = memnew(PanelContainer);
Ref<StyleBoxFlat> hero_style;
hero_style.instantiate();
hero_style->set_bg_color(Color(0.15, 0.2, 0.35, 1.0));
hero_style->set_corner_radius_all(12 * EDSCALE);
hero_style->set_content_margin_all(32 * EDSCALE);
hero_section->add_theme_style_override("panel", hero_style);
home_panel->add_child(hero_section);
VBoxContainer *hero_content = memnew(VBoxContainer);
hero_content->add_theme_constant_override("separation", 12 * EDSCALE);
hero_section->add_child(hero_content);
// Welcome message
welcome_label = memnew(RichTextLabel);
welcome_label->set_use_bbcode(true);
welcome_label->set_fit_content(true);
welcome_label->set_selection_enabled(false);
welcome_label->parse_bbcode("[font_size=36][b]Welcome to AeThex[/b][/font_size]\n[color=#aabbcc]Your all-in-one game development and gaming platform[/color]");
hero_content->add_child(welcome_label);
// Quick actions in hero
quick_actions = memnew(HBoxContainer);
quick_actions->add_theme_constant_override("separation", 12 * EDSCALE);
hero_content->add_child(quick_actions);
// Styled action buttons
auto create_action_button = [this](const String &text, const String &section, const Color &color) -> Button* {
Button *btn = memnew(Button);
btn->set_text(text);
btn->set_custom_minimum_size(Size2(140 * EDSCALE, 44 * EDSCALE));
Ref<StyleBoxFlat> style;
style.instantiate();
style->set_bg_color(color);
style->set_corner_radius_all(8 * EDSCALE);
btn->add_theme_style_override("normal", style);
Ref<StyleBoxFlat> hover;
hover.instantiate();
hover->set_bg_color(color * 1.15);
hover->set_corner_radius_all(8 * EDSCALE);
btn->add_theme_style_override("hover", hover);
btn->connect("pressed", callable_mp(this, &LauncherPanel::navigate_to).bind(section));
return btn;
};
quick_actions->add_child(create_action_button("📚 My Library", "library", Color(0.3, 0.5, 0.9)));
quick_actions->add_child(create_action_button("🛒 Store", "store", Color(0.4, 0.7, 0.4)));
quick_actions->add_child(create_action_button("⬇️ Downloads", "downloads", Color(0.6, 0.4, 0.8)));
quick_actions->add_child(create_action_button("👥 Friends", "friends", Color(0.8, 0.5, 0.3)));
// Recent games section
VBoxContainer *recent_section = memnew(VBoxContainer);
recent_section->add_theme_constant_override("separation", 12 * EDSCALE);
home_panel->add_child(recent_section);
HBoxContainer *recent_header = memnew(HBoxContainer);
recent_section->add_child(recent_header);
Label *rg_label = memnew(Label);
rg_label->set_text("🎮 Continue Playing");
rg_label->add_theme_font_size_override("font_size", 20 * EDSCALE);
rg_label->add_theme_color_override("font_color", Color(0.9, 0.9, 0.95));
recent_header->add_child(rg_label);
Control *rg_spacer = memnew(Control);
rg_spacer->set_h_size_flags(SIZE_EXPAND_FILL);
recent_header->add_child(rg_spacer);
Button *see_all = memnew(Button);
see_all->set_text("See All →");
see_all->set_flat(true);
see_all->add_theme_color_override("font_color", Color(0.5, 0.7, 1.0));
see_all->connect("pressed", callable_mp(this, &LauncherPanel::navigate_to).bind("library"));
recent_header->add_child(see_all);
recent_games = memnew(HBoxContainer);
recent_games->set_custom_minimum_size(Size2(0, 200 * EDSCALE));
recent_games->add_theme_constant_override("separation", 16 * EDSCALE);
recent_section->add_child(recent_games);
// Placeholder for no games
Label *no_games = memnew(Label);
no_games->set_text("No recent games. Add games from your Library!");
no_games->set_modulate(Color(1, 1, 1, 0.5));
recent_games->add_child(no_games);
home_panel->add_child(memnew(HSeparator));
// Friend activity
Label *fa_label = memnew(Label);
fa_label->set_text("Friend Activity");
fa_label->add_theme_font_size_override("font_size", 18 * EDSCALE);
home_panel->add_child(fa_label);
friend_activity = memnew(VBoxContainer);
home_panel->add_child(friend_activity);
// Placeholder for no activity
Label *no_activity = memnew(Label);
no_activity->set_text("No friend activity yet. Add some friends!");
no_activity->set_modulate(Color(1, 1, 1, 0.5));
friend_activity->add_child(no_activity);
}
void LauncherPanel::_setup_panels() {
// Create actual panel implementations with full functionality
// Library panel - shows user's game collection
library_panel = memnew(LibraryPanel);
library_panel->set_visible(false);
library_panel->set_v_size_flags(SIZE_EXPAND_FILL);
library_panel->set_h_size_flags(SIZE_EXPAND_FILL);
library_panel->set_launcher(launcher);
content_area->add_child(library_panel);
// Store panel - browse and purchase games
store_panel = memnew(StorePanel);
store_panel->set_visible(false);
store_panel->set_v_size_flags(SIZE_EXPAND_FILL);
store_panel->set_h_size_flags(SIZE_EXPAND_FILL);
store_panel->set_launcher(launcher);
content_area->add_child(store_panel);
// Downloads panel - active and completed downloads
downloads_panel = memnew(DownloadsPanel);
downloads_panel->set_visible(false);
downloads_panel->set_v_size_flags(SIZE_EXPAND_FILL);
downloads_panel->set_h_size_flags(SIZE_EXPAND_FILL);
downloads_panel->set_launcher(launcher);
content_area->add_child(downloads_panel);
// Friends panel - social features
friends_panel = memnew(FriendsPanel);
friends_panel->set_visible(false);
friends_panel->set_v_size_flags(SIZE_EXPAND_FILL);
friends_panel->set_h_size_flags(SIZE_EXPAND_FILL);
friends_panel->set_launcher(launcher);
content_area->add_child(friends_panel);
// Profile panel - user profile and settings
profile_panel = memnew(ProfilePanel);
profile_panel->set_visible(false);
profile_panel->set_v_size_flags(SIZE_EXPAND_FILL);
profile_panel->set_h_size_flags(SIZE_EXPAND_FILL);
profile_panel->set_launcher(launcher);
content_area->add_child(profile_panel);
}
void LauncherPanel::_on_nav_selected() {
TreeItem *selected = nav_tree->get_selected();
if (!selected) return;
String section = selected->get_metadata(0);
navigate_to(section);
}
void LauncherPanel::_on_search_changed(const String &p_text) {
// Pass search to the active panel if it supports searching
if (current_panel == library_panel && library_panel) {
library_panel->search(p_text);
} else if (current_panel == store_panel && store_panel) {
store_panel->search(p_text);
}
}
void LauncherPanel::_on_user_button_pressed() {
if (launcher && launcher->is_authenticated()) {
navigate_to("profile");
} else {
_show_auth();
}
}
void LauncherPanel::_show_panel(Control *p_panel, const String &p_title) {
// Hide all panels (with null checks)
if (home_panel) home_panel->set_visible(false);
if (library_panel) library_panel->set_visible(false);
if (store_panel) store_panel->set_visible(false);
if (downloads_panel) downloads_panel->set_visible(false);
if (friends_panel) friends_panel->set_visible(false);
if (profile_panel) profile_panel->set_visible(false);
// Show requested panel
if (p_panel) {
p_panel->set_visible(true);
current_panel = p_panel;
}
// Update header title safely
if (header_title) {
header_title->set_text(p_title);
}
}
void LauncherPanel::_show_home() {
_show_panel(nullptr, "Home");
if (home_panel) {
home_panel->set_visible(true);
current_panel = home_panel;
_refresh_home();
}
}
void LauncherPanel::_show_auth() {
showing_auth = true;
if (auth_panel) {
auth_panel->set_visible(true);
auth_panel->show_auth();
}
}
void LauncherPanel::_hide_auth() {
showing_auth = false;
if (auth_panel) {
auth_panel->set_visible(false);
}
}
void LauncherPanel::_on_authenticated(const Dictionary &p_user) {
_hide_auth();
_refresh_user_button();
_refresh_home();
}
void LauncherPanel::_on_signed_out() {
_refresh_user_button();
_show_auth();
}
void LauncherPanel::_refresh_home() {
if (!recent_games) return;
// Clear and rebuild recent games
while (recent_games->get_child_count() > 0) {
recent_games->get_child(0)->queue_free();
recent_games->remove_child(recent_games->get_child(0));
}
if (launcher) {
// Add recently played games
auto lib = launcher->get_game_library();
if (lib.is_valid()) {
auto recent = lib->get_recently_played(5);
for (int i = 0; i < recent.size(); i++) {
// Would create GameCard widgets here
}
}
}
if (recent_games->get_child_count() == 0) {
Label *empty = memnew(Label);
empty->set_text("No recent games. Check out the store!");
recent_games->add_child(empty);
}
}
void LauncherPanel::_refresh_user_button() {
if (!user_button) return;
if (launcher && launcher->is_authenticated()) {
user_button->set_text(launcher->get_username());
} else {
user_button->set_text("Sign In");
}
}
void LauncherPanel::set_launcher(AethexLauncher *p_launcher) {
launcher = p_launcher;
// Setup UI now that we have the launcher
if (launcher && !main_split) {
_setup_ui();
}
if (launcher) {
// Initialize the launcher if not already done
launcher->initialize();
launcher->connect("authenticated", callable_mp(this, &LauncherPanel::_on_authenticated));
launcher->connect("signed_out", callable_mp(this, &LauncherPanel::_on_signed_out));
// Update launcher reference for panels that may have been created before launcher was set
if (library_panel) {
library_panel->set_launcher(launcher);
library_panel->refresh();
}
if (store_panel) {
store_panel->set_launcher(launcher);
store_panel->refresh();
}
if (downloads_panel) {
downloads_panel->set_launcher(launcher);
downloads_panel->refresh();
}
if (friends_panel) {
friends_panel->set_launcher(launcher);
friends_panel->refresh();
}
if (profile_panel) {
profile_panel->set_launcher(launcher);
profile_panel->refresh();
}
if (auth_panel) auth_panel->set_launcher(launcher);
}
}
// Getter implementations moved to header as inline functions
void LauncherPanel::navigate_to(const String &p_section) {
if (p_section == "home") {
_show_home();
} else if (p_section == "library" && library_panel) {
_show_panel(library_panel, "Library");
} else if (p_section == "store" && store_panel) {
_show_panel(store_panel, "Store");
} else if (p_section == "downloads" && downloads_panel) {
_show_panel(downloads_panel, "Downloads");
} else if (p_section == "friends" && friends_panel) {
_show_panel(friends_panel, "Friends");
} else if (p_section == "profile" && profile_panel) {
_show_panel(profile_panel, "Profile");
} else if (p_section == "settings") {
// TODO: Settings panel
_show_panel(nullptr, "Settings");
}
emit_signal("section_changed", p_section);
}
void LauncherPanel::show_game_details(const String &p_game_id) {
// TODO: Implement when full library panel is ready
navigate_to("library");
}
#endif // TOOLS_ENABLED

View file

@ -0,0 +1,121 @@
/**************************************************************************/
/* launcher_panel.h */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifndef LAUNCHER_PANEL_H
#define LAUNCHER_PANEL_H
#ifdef TOOLS_ENABLED
#include "editor/gui/editor_bottom_panel.h"
#include "scene/gui/box_container.h"
#include "scene/gui/tab_container.h"
#include "scene/gui/split_container.h"
#include "scene/gui/scroll_container.h"
#include "scene/gui/tree.h"
#include "scene/gui/rich_text_label.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/button.h"
class LibraryPanel;
class StorePanel;
class DownloadsPanel;
class FriendsPanel;
class ProfilePanel;
class AuthPanel;
class AethexLauncher;
class LauncherPanel : public VBoxContainer {
GDCLASS(LauncherPanel, VBoxContainer);
private:
AethexLauncher *launcher = nullptr;
// Main layout
HSplitContainer *main_split = nullptr;
// Sidebar
VBoxContainer *sidebar = nullptr;
Tree *nav_tree = nullptr;
TreeItem *nav_home = nullptr;
TreeItem *nav_library = nullptr;
TreeItem *nav_store = nullptr;
TreeItem *nav_downloads = nullptr;
TreeItem *nav_friends = nullptr;
TreeItem *nav_profile = nullptr;
TreeItem *nav_settings = nullptr;
// Content area
VBoxContainer *content_area = nullptr;
HBoxContainer *header = nullptr;
Label *header_title = nullptr;
LineEdit *search_bar = nullptr;
Button *user_button = nullptr;
// Panels (stacked) - using actual panel types
Control *current_panel = nullptr;
LibraryPanel *library_panel = nullptr;
StorePanel *store_panel = nullptr;
DownloadsPanel *downloads_panel = nullptr;
FriendsPanel *friends_panel = nullptr;
ProfilePanel *profile_panel = nullptr;
// Auth overlay
AuthPanel *auth_panel = nullptr;
bool showing_auth = false;
// Home content
VBoxContainer *home_panel = nullptr;
RichTextLabel *welcome_label = nullptr;
HBoxContainer *quick_actions = nullptr;
HBoxContainer *recent_games = nullptr;
VBoxContainer *friend_activity = nullptr;
void _setup_ui();
void _setup_sidebar();
void _setup_header();
void _setup_home();
void _setup_panels();
void _on_nav_selected();
void _on_search_changed(const String &p_text);
void _on_user_button_pressed();
void _show_panel(Control *p_panel, const String &p_title);
void _show_home();
void _show_auth();
void _hide_auth();
void _on_authenticated(const Dictionary &p_user);
void _on_signed_out();
void _refresh_home();
void _refresh_user_button();
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void set_launcher(AethexLauncher *p_launcher);
// Panel getters - return actual panel types
LibraryPanel *get_library_panel() const { return library_panel; }
StorePanel *get_store_panel() const { return store_panel; }
DownloadsPanel *get_downloads_panel() const { return downloads_panel; }
FriendsPanel *get_friends_panel() const { return friends_panel; }
ProfilePanel *get_profile_panel() const { return profile_panel; }
void navigate_to(const String &p_section);
void show_game_details(const String &p_game_id);
LauncherPanel();
~LauncherPanel();
};
#endif // TOOLS_ENABLED
#endif // LAUNCHER_PANEL_H

View file

@ -0,0 +1,566 @@
/**************************************************************************/
/* library_panel.cpp */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifdef TOOLS_ENABLED
#include "library_panel.h"
#include "../aethex_launcher.h"
#include "../data/game_library.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/panel_container.h"
#include "scene/resources/style_box_flat.h"
// ============================================================================
// GameCard
// ============================================================================
void GameCard::_bind_methods() {
ADD_SIGNAL(MethodInfo("play_requested", PropertyInfo(Variant::STRING, "game_id")));
ADD_SIGNAL(MethodInfo("details_requested", PropertyInfo(Variant::STRING, "game_id")));
}
GameCard::GameCard() {
set_custom_minimum_size(Size2(200 * EDSCALE, 280 * EDSCALE));
}
GameCard::~GameCard() {
}
void GameCard::_notification(int p_what) {
if (p_what == NOTIFICATION_ENTER_TREE && !setup_done) {
_setup_ui();
}
}
void GameCard::_setup_ui() {
if (setup_done) return;
setup_done = true;
// Card background with rounded corners
Ref<StyleBoxFlat> card_style;
card_style.instantiate();
card_style->set_bg_color(Color(0.15, 0.15, 0.18, 1.0));
card_style->set_corner_radius_all(8 * EDSCALE);
card_style->set_content_margin_all(8 * EDSCALE);
add_theme_style_override("panel", card_style);
// Add some spacing between elements
add_theme_constant_override("separation", 6 * EDSCALE);
// Cover image with rounded corners mask
PanelContainer *cover_container = memnew(PanelContainer);
Ref<StyleBoxFlat> cover_style;
cover_style.instantiate();
cover_style->set_bg_color(Color(0.1, 0.1, 0.12, 1.0));
cover_style->set_corner_radius_all(6 * EDSCALE);
cover_container->add_theme_style_override("panel", cover_style);
add_child(cover_container);
cover_image = memnew(TextureRect);
cover_image->set_custom_minimum_size(Size2(180 * EDSCALE, 240 * EDSCALE));
cover_image->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
cover_image->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_COVERED);
cover_container->add_child(cover_image);
title_label = memnew(Label);
title_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
title_label->add_theme_font_size_override("font_size", 14 * EDSCALE);
title_label->add_theme_color_override("font_color", Color(0.95, 0.95, 0.98));
title_label->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
add_child(title_label);
status_label = memnew(Label);
status_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
status_label->add_theme_font_size_override("font_size", 11 * EDSCALE);
status_label->add_theme_color_override("font_color", Color(0.6, 0.6, 0.65));
add_child(status_label);
// Styled play button
play_button = memnew(Button);
play_button->set_text("Play");
play_button->set_h_size_flags(SIZE_EXPAND_FILL);
// Green gradient for play button
Ref<StyleBoxFlat> btn_style;
btn_style.instantiate();
btn_style->set_bg_color(Color(0.2, 0.7, 0.3, 1.0));
btn_style->set_corner_radius_all(4 * EDSCALE);
play_button->add_theme_style_override("normal", btn_style);
Ref<StyleBoxFlat> btn_hover;
btn_hover.instantiate();
btn_hover->set_bg_color(Color(0.25, 0.8, 0.35, 1.0));
btn_hover->set_corner_radius_all(4 * EDSCALE);
play_button->add_theme_style_override("hover", btn_hover);
play_button->connect("pressed", callable_mp(this, &GameCard::_on_play_pressed));
add_child(play_button);
}
void GameCard::set_game(const Ref<GameEntry> &p_game) {
game = p_game;
_update_display();
}
Ref<GameEntry> GameCard::get_game() const {
return game;
}
void GameCard::_update_display() {
if (game.is_null()) return;
title_label->set_text(game->get_title());
String status_text = "Ready to play";
GameEntry::Status st = game->get_status();
switch (st) {
case GameEntry::STATUS_NOT_INSTALLED:
status_text = "Not installed";
play_button->set_text("Install");
break;
case GameEntry::STATUS_DOWNLOADING:
status_text = "Downloading...";
play_button->set_text("Pause");
break;
case GameEntry::STATUS_INSTALLING:
status_text = "Installing...";
play_button->set_disabled(true);
break;
case GameEntry::STATUS_INSTALLED:
status_text = vformat("%.1f hrs played", game->get_playtime_minutes() / 60.0);
play_button->set_text("Play");
break;
case GameEntry::STATUS_UPDATE_AVAILABLE:
status_text = "Update available";
play_button->set_text("Update");
break;
case GameEntry::STATUS_RUNNING:
status_text = "Running";
play_button->set_text("Stop");
break;
}
status_label->set_text(status_text);
}
void GameCard::_on_play_pressed() {
if (game.is_valid()) {
emit_signal("play_requested", game->get_id());
}
}
void GameCard::_on_card_clicked() {
if (game.is_valid()) {
emit_signal("details_requested", game->get_id());
}
}
// ============================================================================
// GameDetailsPanel
// ============================================================================
void GameDetailsPanel::_bind_methods() {
ADD_SIGNAL(MethodInfo("closed"));
}
GameDetailsPanel::GameDetailsPanel() {
}
GameDetailsPanel::~GameDetailsPanel() {
}
void GameDetailsPanel::_notification(int p_what) {
if (p_what == NOTIFICATION_ENTER_TREE && !setup_done) {
_setup_ui();
}
}
void GameDetailsPanel::_setup_ui() {
if (setup_done) return;
setup_done = true;
HBoxContainer *header = memnew(HBoxContainer);
add_child(header);
cover_image = memnew(TextureRect);
cover_image->set_custom_minimum_size(Size2(200 * EDSCALE, 200 * EDSCALE));
header->add_child(cover_image);
VBoxContainer *info_box = memnew(VBoxContainer);
info_box->set_h_size_flags(SIZE_EXPAND_FILL);
header->add_child(info_box);
title_label = memnew(Label);
title_label->add_theme_font_size_override("font_size", 20 * EDSCALE);
info_box->add_child(title_label);
developer_label = memnew(Label);
info_box->add_child(developer_label);
playtime_label = memnew(Label);
info_box->add_child(playtime_label);
achievements_label = memnew(Label);
info_box->add_child(achievements_label);
HBoxContainer *actions = memnew(HBoxContainer);
info_box->add_child(actions);
play_button = memnew(Button);
play_button->set_text("Play");
play_button->connect("pressed", callable_mp(this, &GameDetailsPanel::_on_play_pressed));
actions->add_child(play_button);
settings_button = memnew(Button);
settings_button->set_text("Settings");
settings_button->connect("pressed", callable_mp(this, &GameDetailsPanel::_show_settings));
actions->add_child(settings_button);
tabs = memnew(TabContainer);
tabs->set_v_size_flags(SIZE_EXPAND_FILL);
add_child(tabs);
overview_panel = memnew(VBoxContainer);
overview_panel->set_name("Overview");
tabs->add_child(overview_panel);
description_label = memnew(RichTextLabel);
description_label->set_use_bbcode(true);
description_label->set_v_size_flags(SIZE_EXPAND_FILL);
overview_panel->add_child(description_label);
achievements_panel = memnew(VBoxContainer);
achievements_panel->set_name("Achievements");
tabs->add_child(achievements_panel);
dlc_panel = memnew(VBoxContainer);
dlc_panel->set_name("DLC");
tabs->add_child(dlc_panel);
news_panel = memnew(VBoxContainer);
news_panel->set_name("News");
tabs->add_child(news_panel);
// Settings overlay
settings_overlay = memnew(PanelContainer);
settings_overlay->set_visible(false);
add_child(settings_overlay);
VBoxContainer *settings_box = memnew(VBoxContainer);
settings_overlay->add_child(settings_box);
Label *settings_title = memnew(Label);
settings_title->set_text("Game Settings");
settings_box->add_child(settings_title);
install_location = memnew(HBoxContainer);
settings_box->add_child(install_location);
launch_options = memnew(LineEdit);
launch_options->set_placeholder("Launch Options");
settings_box->add_child(launch_options);
uninstall_button = memnew(Button);
uninstall_button->set_text("Uninstall");
uninstall_button->connect("pressed", callable_mp(this, &GameDetailsPanel::_on_uninstall));
settings_box->add_child(uninstall_button);
}
void GameDetailsPanel::set_game(const Ref<GameEntry> &p_game) {
game = p_game;
_update_display();
}
void GameDetailsPanel::_update_display() {
if (game.is_null()) return;
title_label->set_text(game->get_title());
developer_label->set_text(game->get_developer());
playtime_label->set_text(vformat("%.1f hours played", game->get_playtime_minutes() / 60.0));
achievements_label->set_text(vformat("%d achievements", game->get_achievements_unlocked()));
description_label->set_text(game->get_description());
}
void GameDetailsPanel::_on_play_pressed() {
if (launcher && game.is_valid()) {
launcher->launch_game(game->get_id());
}
}
void GameDetailsPanel::_show_settings() {
settings_overlay->set_visible(true);
}
void GameDetailsPanel::_close_settings() {
settings_overlay->set_visible(false);
}
void GameDetailsPanel::_save_settings() {
_close_settings();
}
void GameDetailsPanel::_on_uninstall() {
// TODO: Implement uninstall
}
// ============================================================================
// LibraryPanel
// ============================================================================
void LibraryPanel::_bind_methods() {
ClassDB::bind_method(D_METHOD("refresh"), &LibraryPanel::refresh);
ClassDB::bind_method(D_METHOD("search", "query"), &LibraryPanel::search);
ClassDB::bind_method(D_METHOD("show_game", "game_id"), &LibraryPanel::show_game);
ADD_SIGNAL(MethodInfo("game_launched", PropertyInfo(Variant::STRING, "game_id")));
}
LibraryPanel::LibraryPanel() {
}
LibraryPanel::~LibraryPanel() {
}
void LibraryPanel::_notification(int p_what) {
if (p_what == NOTIFICATION_ENTER_TREE && !setup_done) {
_setup_ui();
}
}
void LibraryPanel::_setup_ui() {
if (setup_done) return;
setup_done = true;
// Toolbar
toolbar = memnew(HBoxContainer);
add_child(toolbar);
search_bar = memnew(LineEdit);
search_bar->set_placeholder("Search games...");
search_bar->set_h_size_flags(SIZE_EXPAND_FILL);
search_bar->connect("text_changed", callable_mp(this, &LibraryPanel::_on_search_changed));
toolbar->add_child(search_bar);
filter_option = memnew(OptionButton);
filter_option->add_item("All Games", 0);
filter_option->add_item("Installed", 1);
filter_option->add_item("Not Installed", 2);
filter_option->connect("item_selected", callable_mp(this, &LibraryPanel::_on_filter_changed));
toolbar->add_child(filter_option);
add_game_button = memnew(Button);
add_game_button->set_text("Add Game");
add_game_button->connect("pressed", callable_mp(this, &LibraryPanel::_show_add_game_dialog));
toolbar->add_child(add_game_button);
add_child(memnew(HSeparator));
// Main content
HSplitContainer *main_split = memnew(HSplitContainer);
main_split->set_v_size_flags(SIZE_EXPAND_FILL);
add_child(main_split);
VBoxContainer *games_container = memnew(VBoxContainer);
games_container->set_h_size_flags(SIZE_EXPAND_FILL);
main_split->add_child(games_container);
scroll_container = memnew(ScrollContainer);
scroll_container->set_v_size_flags(SIZE_EXPAND_FILL);
scroll_container->set_h_size_flags(SIZE_EXPAND_FILL);
games_container->add_child(scroll_container);
game_grid = memnew(GridContainer);
game_grid->set_columns(4);
scroll_container->add_child(game_grid);
game_list = memnew(Tree);
game_list->set_visible(false);
game_list->set_v_size_flags(SIZE_EXPAND_FILL);
game_list->set_columns(4);
game_list->set_column_titles_visible(true);
game_list->set_column_title(0, "Name");
game_list->set_column_title(1, "Status");
game_list->set_column_title(2, "Playtime");
game_list->set_column_title(3, "Last Played");
game_list->connect("item_selected", callable_mp(this, &LibraryPanel::_on_game_selected_list));
game_list->connect("item_activated", callable_mp(this, &LibraryPanel::_on_game_activated_list));
games_container->add_child(game_list);
details_panel = memnew(GameDetailsPanel);
details_panel->set_custom_minimum_size(Size2(400 * EDSCALE, 0));
details_panel->set_visible(false);
main_split->add_child(details_panel);
// Add game dialog
add_game_dialog = memnew(AcceptDialog);
add_game_dialog->set_title("Add Game");
add_game_dialog->get_ok_button()->set_text("Add");
add_game_dialog->connect("confirmed", callable_mp(this, &LibraryPanel::_on_add_game_confirmed));
add_child(add_game_dialog);
VBoxContainer *dialog_content = memnew(VBoxContainer);
add_game_dialog->add_child(dialog_content);
dialog_game_name = memnew(LineEdit);
dialog_game_name->set_placeholder("Game Name");
dialog_content->add_child(dialog_game_name);
HBoxContainer *path_row = memnew(HBoxContainer);
dialog_content->add_child(path_row);
dialog_game_path = memnew(LineEdit);
dialog_game_path->set_h_size_flags(SIZE_EXPAND_FILL);
dialog_game_path->set_placeholder("Path to executable");
path_row->add_child(dialog_game_path);
Button *browse = memnew(Button);
browse->set_text("Browse");
browse->connect("pressed", callable_mp(this, &LibraryPanel::_browse_for_game));
path_row->add_child(browse);
file_dialog = memnew(FileDialog);
file_dialog->set_file_mode(FileDialog::FILE_MODE_OPEN_FILE);
file_dialog->set_access(FileDialog::ACCESS_FILESYSTEM);
file_dialog->add_filter("*.exe", "Executable");
file_dialog->connect("file_selected", callable_mp(this, &LibraryPanel::_on_file_selected));
add_child(file_dialog);
}
void LibraryPanel::set_launcher(AethexLauncher *p_launcher) {
launcher = p_launcher;
if (details_panel) {
details_panel->set_launcher(launcher);
}
}
void LibraryPanel::refresh() {
if (!game_grid) {
// Not setup yet, will refresh on ENTER_TREE
return;
}
_refresh_library();
}
void LibraryPanel::search(const String &p_query) {
if (search_bar) {
search_bar->set_text(p_query);
}
_refresh_library();
}
void LibraryPanel::show_game(const String &p_game_id) {
if (!launcher) return;
auto lib = launcher->get_game_library();
if (lib.is_null()) return;
Ref<GameEntry> game = lib->get_game(p_game_id);
if (game.is_valid() && details_panel) {
details_panel->set_game(game);
details_panel->show();
}
}
void LibraryPanel::_refresh_library() {
if (!launcher || !game_grid) return;
// Clear existing cards - proper removal
for (int i = game_grid->get_child_count() - 1; i >= 0; i--) {
Node *child = game_grid->get_child(i);
game_grid->remove_child(child);
child->queue_free();
}
game_cards.clear();
auto lib = launcher->get_game_library();
if (lib.is_null()) return;
TypedArray<GameEntry> games = lib->get_all_games();
for (int i = 0; i < games.size(); i++) {
Ref<GameEntry> game = games[i];
if (game.is_null()) continue;
GameCard *card = memnew(GameCard);
card->set_game(game);
card->connect("play_requested", callable_mp(this, &LibraryPanel::_on_game_card_play));
card->connect("details_requested", callable_mp(this, &LibraryPanel::_on_game_card_details));
game_grid->add_child(card);
game_cards.push_back(card);
}
}
void LibraryPanel::_on_search_changed(const String &p_text) {
_refresh_library();
}
void LibraryPanel::_on_filter_changed(int p_idx) {
_refresh_library();
}
void LibraryPanel::_show_add_game_dialog() {
dialog_game_name->set_text("");
dialog_game_path->set_text("");
add_game_dialog->popup_centered();
}
void LibraryPanel::_on_add_game_confirmed() {
if (!launcher) return;
String name = dialog_game_name->get_text().strip_edges();
String path = dialog_game_path->get_text().strip_edges();
if (name.is_empty() || path.is_empty()) return;
auto lib = launcher->get_game_library();
if (lib.is_valid()) {
lib->add_external_game(name, path);
refresh();
}
}
void LibraryPanel::_browse_for_game() {
file_dialog->popup_centered_ratio(0.7);
}
void LibraryPanel::_on_file_selected(const String &p_path) {
dialog_game_path->set_text(p_path);
}
void LibraryPanel::_on_game_selected_list() {
TreeItem *selected = game_list->get_selected();
if (selected && details_panel) {
selected_game_id = selected->get_metadata(0);
// TODO: Show details
}
}
void LibraryPanel::_on_game_activated_list() {
TreeItem *selected = game_list->get_selected();
if (selected && launcher) {
launcher->launch_game(selected->get_metadata(0));
}
}
void LibraryPanel::_on_game_card_play(const String &p_game_id) {
if (launcher) {
launcher->launch_game(p_game_id);
}
}
void LibraryPanel::_on_game_card_details(const String &p_game_id) {
selected_game_id = p_game_id;
if (details_panel && launcher) {
auto lib = launcher->get_game_library();
if (lib.is_valid()) {
details_panel->set_game(lib->get_game(p_game_id));
details_panel->set_visible(true);
}
}
}
#endif // TOOLS_ENABLED

View file

@ -0,0 +1,176 @@
/**************************************************************************/
/* library_panel.h */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifndef LIBRARY_PANEL_H
#define LIBRARY_PANEL_H
#ifdef TOOLS_ENABLED
#include "scene/gui/box_container.h"
#include "scene/gui/split_container.h"
#include "scene/gui/grid_container.h"
#include "scene/gui/scroll_container.h"
#include "scene/gui/tree.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/option_button.h"
#include "scene/gui/button.h"
#include "scene/gui/label.h"
#include "scene/gui/texture_rect.h"
#include "scene/gui/progress_bar.h"
#include "scene/gui/tab_container.h"
#include "scene/gui/rich_text_label.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/dialogs.h"
#include "scene/gui/file_dialog.h"
#include "scene/gui/separator.h"
class AethexLauncher;
class GameEntry;
class GameCard : public VBoxContainer {
GDCLASS(GameCard, VBoxContainer);
private:
bool setup_done = false;
Ref<GameEntry> game;
TextureRect *cover_image = nullptr;
Label *title_label = nullptr;
Label *status_label = nullptr;
Button *play_button = nullptr;
void _setup_ui();
void _update_display();
void _on_play_pressed();
void _on_card_clicked();
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void set_game(const Ref<GameEntry> &p_game);
Ref<GameEntry> get_game() const;
GameCard();
~GameCard();
};
class GameDetailsPanel : public VBoxContainer {
GDCLASS(GameDetailsPanel, VBoxContainer);
private:
bool setup_done = false;
AethexLauncher *launcher = nullptr;
Ref<GameEntry> game;
// Header
TextureRect *cover_image = nullptr;
Label *title_label = nullptr;
Label *developer_label = nullptr;
Label *playtime_label = nullptr;
Label *achievements_label = nullptr;
// Actions
Button *play_button = nullptr;
Button *settings_button = nullptr;
// Tabs
TabContainer *tabs = nullptr;
VBoxContainer *overview_panel = nullptr;
RichTextLabel *description_label = nullptr;
VBoxContainer *achievements_panel = nullptr;
VBoxContainer *dlc_panel = nullptr;
VBoxContainer *news_panel = nullptr;
// Settings overlay
PanelContainer *settings_overlay = nullptr;
HBoxContainer *install_location = nullptr;
LineEdit *launch_options = nullptr;
Button *uninstall_button = nullptr;
void _setup_ui();
void _update_display();
void _on_play_pressed();
void _show_settings();
void _close_settings();
void _save_settings();
void _on_uninstall();
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void set_launcher(AethexLauncher *p_launcher) { launcher = p_launcher; }
void set_game(const Ref<GameEntry> &p_game);
GameDetailsPanel();
~GameDetailsPanel();
};
class LibraryPanel : public VBoxContainer {
GDCLASS(LibraryPanel, VBoxContainer);
private:
AethexLauncher *launcher = nullptr;
bool setup_done = false;
// Toolbar
HBoxContainer *toolbar = nullptr;
LineEdit *search_bar = nullptr;
OptionButton *filter_option = nullptr;
Button *add_game_button = nullptr;
// Views
ScrollContainer *scroll_container = nullptr;
GridContainer *game_grid = nullptr;
Tree *game_list = nullptr;
// Details panel
GameDetailsPanel *details_panel = nullptr;
String selected_game_id;
// Add game dialog
AcceptDialog *add_game_dialog = nullptr;
LineEdit *dialog_game_name = nullptr;
LineEdit *dialog_game_path = nullptr;
FileDialog *file_dialog = nullptr;
// Cards
Vector<GameCard *> game_cards;
void _setup_ui();
void _refresh_library();
void _on_search_changed(const String &p_text);
void _on_filter_changed(int p_idx);
void _show_add_game_dialog();
void _on_add_game_confirmed();
void _browse_for_game();
void _on_file_selected(const String &p_path);
void _on_game_selected_list();
void _on_game_activated_list();
void _on_game_card_play(const String &p_game_id);
void _on_game_card_details(const String &p_game_id);
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void set_launcher(AethexLauncher *p_launcher);
void refresh();
void search(const String &p_query);
void show_game(const String &p_game_id);
LibraryPanel();
~LibraryPanel();
};
#endif // TOOLS_ENABLED
#endif // LIBRARY_PANEL_H

View file

@ -0,0 +1,391 @@
/**************************************************************************/
/* profile_panel.cpp */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifdef TOOLS_ENABLED
#include "profile_panel.h"
#include "../aethex_launcher.h"
#include "../data/launcher_profile.h"
#include "editor/themes/editor_scale.h"
void ProfilePanel::_bind_methods() {
ClassDB::bind_method(D_METHOD("refresh"), &ProfilePanel::refresh);
}
ProfilePanel::ProfilePanel() {
}
ProfilePanel::~ProfilePanel() {
}
void ProfilePanel::_notification(int p_what) {
if (p_what == NOTIFICATION_ENTER_TREE && !setup_done) {
_setup_ui();
}
}
void ProfilePanel::_setup_ui() {
if (setup_done) return;
setup_done = true;
// Main scroll
ScrollContainer *scroll = memnew(ScrollContainer);
scroll->set_v_size_flags(SIZE_EXPAND_FILL);
add_child(scroll);
VBoxContainer *content = memnew(VBoxContainer);
content->set_h_size_flags(SIZE_EXPAND_FILL);
scroll->add_child(content);
// Header section
VBoxContainer *header_section = memnew(VBoxContainer);
content->add_child(header_section);
// Banner
banner_image = memnew(TextureRect);
banner_image->set_custom_minimum_size(Size2(0, 200 * EDSCALE));
banner_image->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
banner_image->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_COVERED);
header_section->add_child(banner_image);
// Avatar and basic info
HBoxContainer *profile_row = memnew(HBoxContainer);
header_section->add_child(profile_row);
avatar_image = memnew(TextureRect);
avatar_image->set_custom_minimum_size(Size2(100 * EDSCALE, 100 * EDSCALE));
profile_row->add_child(avatar_image);
VBoxContainer *name_box = memnew(VBoxContainer);
name_box->set_h_size_flags(SIZE_EXPAND_FILL);
profile_row->add_child(name_box);
display_name_label = memnew(Label);
display_name_label->add_theme_font_size_override("font_size", 24 * EDSCALE);
name_box->add_child(display_name_label);
username_label = memnew(Label);
name_box->add_child(username_label);
level_label = memnew(Label);
name_box->add_child(level_label);
// Edit button
edit_button = memnew(Button);
edit_button->set_text("Edit Profile");
edit_button->connect("pressed", callable_mp(this, &ProfilePanel::_show_edit_dialog));
profile_row->add_child(edit_button);
settings_button = memnew(Button);
settings_button->set_text("Settings");
settings_button->connect("pressed", callable_mp(this, &ProfilePanel::_show_settings_dialog));
profile_row->add_child(settings_button);
sign_out_button = memnew(Button);
sign_out_button->set_text("Sign Out");
sign_out_button->connect("pressed", callable_mp(this, &ProfilePanel::_sign_out));
profile_row->add_child(sign_out_button);
content->add_child(memnew(HSeparator));
// Stats bar
stats_bar = memnew(HBoxContainer);
content->add_child(stats_bar);
games_count = memnew(VBoxContainer);
stats_bar->add_child(games_count);
Label *gc_num = memnew(Label);
gc_num->set_name("num");
gc_num->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
gc_num->add_theme_font_size_override("font_size", 20 * EDSCALE);
games_count->add_child(gc_num);
Label *gc_lbl = memnew(Label);
gc_lbl->set_text("Games");
gc_lbl->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
games_count->add_child(gc_lbl);
friends_count = memnew(VBoxContainer);
stats_bar->add_child(friends_count);
Label *fc_num = memnew(Label);
fc_num->set_name("num");
fc_num->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
fc_num->add_theme_font_size_override("font_size", 20 * EDSCALE);
friends_count->add_child(fc_num);
Label *fc_lbl = memnew(Label);
fc_lbl->set_text("Friends");
fc_lbl->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
friends_count->add_child(fc_lbl);
achievements_count = memnew(VBoxContainer);
stats_bar->add_child(achievements_count);
Label *ac_num = memnew(Label);
ac_num->set_name("num");
ac_num->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
ac_num->add_theme_font_size_override("font_size", 20 * EDSCALE);
achievements_count->add_child(ac_num);
Label *ac_lbl = memnew(Label);
ac_lbl->set_text("Achievements");
ac_lbl->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
achievements_count->add_child(ac_lbl);
playtime_total = memnew(VBoxContainer);
stats_bar->add_child(playtime_total);
Label *pt_num = memnew(Label);
pt_num->set_name("num");
pt_num->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
pt_num->add_theme_font_size_override("font_size", 20 * EDSCALE);
playtime_total->add_child(pt_num);
Label *pt_lbl = memnew(Label);
pt_lbl->set_text("Hours Played");
pt_lbl->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
playtime_total->add_child(pt_lbl);
content->add_child(memnew(HSeparator));
// Bio
Label *bio_header = memnew(Label);
bio_header->set_text("About");
bio_header->add_theme_font_size_override("font_size", 16 * EDSCALE);
content->add_child(bio_header);
bio_label = memnew(RichTextLabel);
bio_label->set_use_bbcode(true);
bio_label->set_fit_content(true);
content->add_child(bio_label);
// Recent activity
Label *activity_header = memnew(Label);
activity_header->set_text("Recent Activity");
activity_header->add_theme_font_size_override("font_size", 16 * EDSCALE);
content->add_child(activity_header);
recent_activity = memnew(VBoxContainer);
content->add_child(recent_activity);
// Edit dialog
edit_dialog = memnew(AcceptDialog);
edit_dialog->set_title("Edit Profile");
edit_dialog->get_ok_button()->set_text("Save");
edit_dialog->connect("confirmed", callable_mp(this, &ProfilePanel::_on_edit_confirmed));
add_child(edit_dialog);
VBoxContainer *edit_content = memnew(VBoxContainer);
edit_dialog->add_child(edit_content);
edit_display_name = memnew(LineEdit);
edit_display_name->set_placeholder("Display Name");
edit_content->add_child(edit_display_name);
edit_bio = memnew(TextEdit);
edit_bio->set_placeholder("Tell us about yourself...");
edit_bio->set_custom_minimum_size(Size2(0, 100 * EDSCALE));
edit_content->add_child(edit_bio);
Label *social_label = memnew(Label);
social_label->set_text("Social Links");
edit_content->add_child(social_label);
edit_website = memnew(LineEdit);
edit_website->set_placeholder("Website");
edit_content->add_child(edit_website);
edit_twitter = memnew(LineEdit);
edit_twitter->set_placeholder("Twitter/X");
edit_content->add_child(edit_twitter);
edit_github = memnew(LineEdit);
edit_github->set_placeholder("GitHub");
edit_content->add_child(edit_github);
edit_discord = memnew(LineEdit);
edit_discord->set_placeholder("Discord");
edit_content->add_child(edit_discord);
// Settings dialog
settings_dialog = memnew(AcceptDialog);
settings_dialog->set_title("Settings");
settings_dialog->get_ok_button()->set_text("Save");
settings_dialog->connect("confirmed", callable_mp(this, &ProfilePanel::_on_settings_confirmed));
add_child(settings_dialog);
VBoxContainer *settings_content = memnew(VBoxContainer);
settings_dialog->add_child(settings_content);
Label *privacy_label = memnew(Label);
privacy_label->set_text("Privacy");
settings_content->add_child(privacy_label);
privacy_profile = memnew(OptionButton);
privacy_profile->add_item("Public", 0);
privacy_profile->add_item("Friends Only", 1);
privacy_profile->add_item("Private", 2);
settings_content->add_child(privacy_profile);
privacy_activity = memnew(CheckButton);
privacy_activity->set_text("Show Activity");
settings_content->add_child(privacy_activity);
privacy_games = memnew(CheckButton);
privacy_games->set_text("Show Games");
settings_content->add_child(privacy_games);
privacy_friends = memnew(CheckButton);
privacy_friends->set_text("Show Friends");
settings_content->add_child(privacy_friends);
Label *notif_label = memnew(Label);
notif_label->set_text("Notifications");
settings_content->add_child(notif_label);
notif_friend_requests = memnew(CheckButton);
notif_friend_requests->set_text("Friend Requests");
settings_content->add_child(notif_friend_requests);
notif_messages = memnew(CheckButton);
notif_messages->set_text("Messages");
settings_content->add_child(notif_messages);
notif_game_updates = memnew(CheckButton);
notif_game_updates->set_text("Game Updates");
settings_content->add_child(notif_game_updates);
}
void ProfilePanel::set_launcher(AethexLauncher *p_launcher) {
launcher = p_launcher;
}
void ProfilePanel::refresh() {
if (!launcher || !display_name_label) return; // Not setup yet
Ref<LauncherProfile> profile = launcher->get_current_profile();
if (profile.is_null()) return;
display_name_label->set_text(profile->get_display_name());
username_label->set_text(profile->get_status());
level_label->set_text(vformat("Level %d", profile->get_level()));
bio_label->set_text(profile->get_bio());
// Stats
if (games_count) {
if (Label *lbl = Object::cast_to<Label>(games_count->get_node_or_null(NodePath("num")))) {
lbl->set_text(String::num_int64(profile->get_games_owned()));
}
}
if (friends_count) {
if (Label *lbl = Object::cast_to<Label>(friends_count->get_node_or_null(NodePath("num")))) {
lbl->set_text("0");
}
}
if (achievements_count) {
if (Label *lbl = Object::cast_to<Label>(achievements_count->get_node_or_null(NodePath("num")))) {
lbl->set_text(String::num_int64(profile->get_achievements_unlocked()));
}
}
if (playtime_total) {
if (Label *lbl = Object::cast_to<Label>(playtime_total->get_node_or_null(NodePath("num")))) {
lbl->set_text(vformat("%.1f", profile->get_total_playtime() / 3600.0));
}
}
// Clear recent activity
if (recent_activity) {
for (int i = recent_activity->get_child_count() - 1; i >= 0; i--) {
Node *child = recent_activity->get_child(i);
recent_activity->remove_child(child);
child->queue_free();
}
}
}
void ProfilePanel::_show_edit_dialog() {
if (!launcher) return;
Ref<LauncherProfile> profile = launcher->get_current_profile();
if (profile.is_null()) return;
edit_display_name->set_text(profile->get_display_name());
edit_bio->set_text(profile->get_bio());
edit_website->set_text(profile->get_website());
edit_twitter->set_text(profile->get_twitter_handle());
edit_github->set_text(profile->get_github_username());
edit_discord->set_text(profile->get_discord_username());
edit_dialog->popup_centered();
}
void ProfilePanel::_show_settings_dialog() {
if (!launcher) return;
Ref<LauncherProfile> profile = launcher->get_current_profile();
if (profile.is_null()) return;
// Load current settings
String vis = profile->get_profile_visibility();
if (vis == "friends") {
privacy_profile->select(1);
} else if (vis == "private") {
privacy_profile->select(2);
} else {
privacy_profile->select(0);
}
privacy_activity->set_pressed(profile->get_show_activity_status());
privacy_games->set_pressed(profile->get_show_game_activity());
privacy_friends->set_pressed(profile->get_allow_friend_requests());
// Default notifications
notif_friend_requests->set_pressed(true);
notif_messages->set_pressed(true);
notif_game_updates->set_pressed(true);
settings_dialog->popup_centered();
}
void ProfilePanel::_on_edit_confirmed() {
if (!launcher) return;
Ref<LauncherProfile> profile = launcher->get_current_profile();
if (profile.is_null()) return;
profile->set_display_name(edit_display_name->get_text().strip_edges());
profile->set_bio(edit_bio->get_text().strip_edges());
profile->set_website(edit_website->get_text().strip_edges());
profile->set_twitter_handle(edit_twitter->get_text().strip_edges());
profile->set_github_username(edit_github->get_text().strip_edges());
profile->set_discord_username(edit_discord->get_text().strip_edges());
refresh();
}
void ProfilePanel::_on_settings_confirmed() {
if (!launcher) return;
Ref<LauncherProfile> profile = launcher->get_current_profile();
if (profile.is_null()) return;
int vis_idx = privacy_profile->get_selected_id();
if (vis_idx == 1) {
profile->set_profile_visibility("friends");
} else if (vis_idx == 2) {
profile->set_profile_visibility("private");
} else {
profile->set_profile_visibility("public");
}
profile->set_show_activity_status(privacy_activity->is_pressed());
profile->set_show_game_activity(privacy_games->is_pressed());
profile->set_allow_friend_requests(privacy_friends->is_pressed());
}
void ProfilePanel::_sign_out() {
if (launcher) {
launcher->sign_out();
}
}
#endif // TOOLS_ENABLED

View file

@ -0,0 +1,99 @@
/**************************************************************************/
/* profile_panel.h */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifndef PROFILE_PANEL_H
#define PROFILE_PANEL_H
#ifdef TOOLS_ENABLED
#include "scene/gui/box_container.h"
#include "scene/gui/scroll_container.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/text_edit.h"
#include "scene/gui/option_button.h"
#include "scene/gui/check_button.h"
#include "scene/gui/button.h"
#include "scene/gui/label.h"
#include "scene/gui/texture_rect.h"
#include "scene/gui/rich_text_label.h"
#include "scene/gui/dialogs.h"
#include "scene/gui/separator.h"
class AethexLauncher;
class LauncherProfile;
class ProfilePanel : public VBoxContainer {
GDCLASS(ProfilePanel, VBoxContainer);
private:
AethexLauncher *launcher = nullptr;
bool setup_done = false;
// Header
TextureRect *banner_image = nullptr;
TextureRect *avatar_image = nullptr;
Label *display_name_label = nullptr;
Label *username_label = nullptr;
Label *level_label = nullptr;
// Buttons
Button *edit_button = nullptr;
Button *settings_button = nullptr;
Button *sign_out_button = nullptr;
// Stats bar
HBoxContainer *stats_bar = nullptr;
VBoxContainer *games_count = nullptr;
VBoxContainer *friends_count = nullptr;
VBoxContainer *achievements_count = nullptr;
VBoxContainer *playtime_total = nullptr;
// Bio and activity
RichTextLabel *bio_label = nullptr;
VBoxContainer *recent_activity = nullptr;
// Edit dialog
AcceptDialog *edit_dialog = nullptr;
LineEdit *edit_display_name = nullptr;
TextEdit *edit_bio = nullptr;
LineEdit *edit_website = nullptr;
LineEdit *edit_twitter = nullptr;
LineEdit *edit_github = nullptr;
LineEdit *edit_discord = nullptr;
// Settings dialog
AcceptDialog *settings_dialog = nullptr;
OptionButton *privacy_profile = nullptr;
CheckButton *privacy_activity = nullptr;
CheckButton *privacy_games = nullptr;
CheckButton *privacy_friends = nullptr;
CheckButton *notif_friend_requests = nullptr;
CheckButton *notif_messages = nullptr;
CheckButton *notif_game_updates = nullptr;
void _setup_ui();
void _show_edit_dialog();
void _show_settings_dialog();
void _on_edit_confirmed();
void _on_settings_confirmed();
void _sign_out();
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void set_launcher(AethexLauncher *p_launcher);
void refresh();
ProfilePanel();
~ProfilePanel();
};
#endif // TOOLS_ENABLED
#endif // PROFILE_PANEL_H

View file

@ -0,0 +1,615 @@
/**************************************************************************/
/* store_panel.cpp */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifdef TOOLS_ENABLED
#include "store_panel.h"
#include "../aethex_launcher.h"
#include "../data/launcher_store.h"
#include "editor/themes/editor_scale.h"
// StoreItemCard implementation
void StoreItemCard::_bind_methods() {
ADD_SIGNAL(MethodInfo("item_clicked", PropertyInfo(Variant::STRING, "item_id")));
ADD_SIGNAL(MethodInfo("add_to_cart", PropertyInfo(Variant::STRING, "item_id")));
ADD_SIGNAL(MethodInfo("wishlist_toggled", PropertyInfo(Variant::STRING, "item_id")));
}
StoreItemCard::StoreItemCard() {
set_custom_minimum_size(Size2(220 * EDSCALE, 320 * EDSCALE));
}
StoreItemCard::~StoreItemCard() {
}
void StoreItemCard::_notification(int p_what) {
if (p_what == NOTIFICATION_ENTER_TREE && !setup_done) {
_setup_ui();
}
}
void StoreItemCard::_setup_ui() {
if (setup_done) return;
setup_done = true;
// Cover image
cover_image = memnew(TextureRect);
cover_image->set_custom_minimum_size(Size2(220 * EDSCALE, 180 * EDSCALE));
cover_image->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
cover_image->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_COVERED);
add_child(cover_image);
// Title
title_label = memnew(Label);
title_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
add_child(title_label);
// Developer
developer_label = memnew(Label);
developer_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
developer_label->add_theme_font_size_override("font_size", 10 * EDSCALE);
add_child(developer_label);
// Price row
HBoxContainer *price_row = memnew(HBoxContainer);
add_child(price_row);
Control *price_spacer1 = memnew(Control);
price_spacer1->set_h_size_flags(SIZE_EXPAND_FILL);
price_row->add_child(price_spacer1);
discount_label = memnew(Label);
discount_label->set_visible(false);
discount_label->add_theme_color_override("font_color", Color(0.2, 0.8, 0.3));
price_row->add_child(discount_label);
original_price = memnew(Label);
original_price->set_visible(false);
price_row->add_child(original_price);
price_label = memnew(Label);
price_row->add_child(price_label);
Control *price_spacer2 = memnew(Control);
price_spacer2->set_h_size_flags(SIZE_EXPAND_FILL);
price_row->add_child(price_spacer2);
// Rating
HBoxContainer *rating_row = memnew(HBoxContainer);
add_child(rating_row);
Control *rating_spacer1 = memnew(Control);
rating_spacer1->set_h_size_flags(SIZE_EXPAND_FILL);
rating_row->add_child(rating_spacer1);
rating_stars = memnew(Label);
rating_row->add_child(rating_stars);
review_count = memnew(Label);
review_count->add_theme_font_size_override("font_size", 10 * EDSCALE);
rating_row->add_child(review_count);
Control *rating_spacer2 = memnew(Control);
rating_spacer2->set_h_size_flags(SIZE_EXPAND_FILL);
rating_row->add_child(rating_spacer2);
// Buttons
HBoxContainer *buttons = memnew(HBoxContainer);
add_child(buttons);
buy_button = memnew(Button);
buy_button->set_text("Add to Cart");
buy_button->set_h_size_flags(SIZE_EXPAND_FILL);
buy_button->connect("pressed", callable_mp(this, &StoreItemCard::_on_buy_pressed));
buttons->add_child(buy_button);
wishlist_button = memnew(Button);
wishlist_button->set_text("");
wishlist_button->set_toggle_mode(true);
wishlist_button->connect("toggled", callable_mp(this, &StoreItemCard::_on_wishlist_toggled));
buttons->add_child(wishlist_button);
}
void StoreItemCard::set_item(const Ref<StoreItem> &p_item) {
item = p_item;
_update_display();
}
Ref<StoreItem> StoreItemCard::get_item() const {
return item;
}
void StoreItemCard::set_wishlisted(bool p_wishlisted) {
wishlist_button->set_pressed(p_wishlisted);
wishlist_button->set_text(p_wishlisted ? "" : "");
}
void StoreItemCard::_update_display() {
if (item.is_null()) return;
title_label->set_text(item->get_title());
developer_label->set_text(item->get_developer());
// Price
float price = item->get_price();
float discount = item->get_discount_percent();
if (discount > 0) {
discount_label->set_text(vformat("-%d%%", (int)discount));
discount_label->set_visible(true);
original_price->set_text(vformat("$%.2f", price));
original_price->set_visible(true);
price_label->set_text(vformat("$%.2f", price * (1.0 - discount / 100.0)));
} else {
discount_label->set_visible(false);
original_price->set_visible(false);
if (price > 0) {
price_label->set_text(vformat("$%.2f", price));
} else {
price_label->set_text("Free");
}
}
// Rating
float rating = item->get_rating();
String stars;
for (int i = 0; i < 5; i++) {
stars += (i < (int)rating) ? "" : "";
}
rating_stars->set_text(stars);
review_count->set_text(vformat("(%d)", item->get_review_count()));
}
void StoreItemCard::_on_buy_pressed() {
if (item.is_valid()) {
emit_signal("add_to_cart", item->get_id());
}
}
void StoreItemCard::_on_wishlist_toggled(bool p_pressed) {
if (item.is_valid()) {
emit_signal("wishlist_toggled", item->get_id());
}
}
// FeaturedBanner implementation
FeaturedBanner::FeaturedBanner() {
set_custom_minimum_size(Size2(0, 300 * EDSCALE));
}
FeaturedBanner::~FeaturedBanner() {
}
void FeaturedBanner::_notification(int p_what) {
if (p_what == NOTIFICATION_ENTER_TREE && !setup_done) {
_setup_ui();
}
}
void FeaturedBanner::_setup_ui() {
if (setup_done) return;
setup_done = true;
// Banner image
banner_image = memnew(TextureRect);
banner_image->set_anchors_and_offsets_preset(PRESET_FULL_RECT);
banner_image->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
banner_image->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_COVERED);
add_child(banner_image);
// Info overlay
VBoxContainer *info = memnew(VBoxContainer);
info->set_anchors_and_offsets_preset(PRESET_BOTTOM_LEFT);
info->set_offset(SIDE_LEFT, 20 * EDSCALE);
info->set_offset(SIDE_BOTTOM, -20 * EDSCALE);
add_child(info);
title_label = memnew(Label);
title_label->add_theme_font_size_override("font_size", 24 * EDSCALE);
info->add_child(title_label);
description_label = memnew(Label);
info->add_child(description_label);
price_label = memnew(Label);
info->add_child(price_label);
// Navigation
HBoxContainer *nav = memnew(HBoxContainer);
nav->set_anchors_and_offsets_preset(PRESET_TOP_RIGHT);
add_child(nav);
prev_button = memnew(Button);
prev_button->set_text("<");
prev_button->connect("pressed", callable_mp(this, &FeaturedBanner::_prev_item));
nav->add_child(prev_button);
indicator = memnew(Label);
nav->add_child(indicator);
next_button = memnew(Button);
next_button->set_text(">");
next_button->connect("pressed", callable_mp(this, &FeaturedBanner::_next_item));
nav->add_child(next_button);
}
void FeaturedBanner::set_items(const TypedArray<StoreItem> &p_items) {
items = p_items;
current_index = 0;
_update_display();
}
void FeaturedBanner::_update_display() {
if (items.is_empty()) return;
Ref<StoreItem> item = items[current_index];
if (item.is_null()) return;
title_label->set_text(item->get_title());
description_label->set_text(item->get_short_description());
float price = item->get_price();
if (price > 0) {
price_label->set_text(vformat("$%.2f", price));
} else {
price_label->set_text("Free");
}
indicator->set_text(vformat("%d / %d", current_index + 1, items.size()));
}
void FeaturedBanner::_prev_item() {
if (items.is_empty()) return;
current_index = (current_index - 1 + items.size()) % items.size();
_update_display();
}
void FeaturedBanner::_next_item() {
if (items.is_empty()) return;
current_index = (current_index + 1) % items.size();
_update_display();
}
// StorePanel implementation
void StorePanel::_bind_methods() {
ClassDB::bind_method(D_METHOD("refresh"), &StorePanel::refresh);
ClassDB::bind_method(D_METHOD("search", "query"), &StorePanel::search);
ADD_SIGNAL(MethodInfo("item_purchased", PropertyInfo(Variant::STRING, "item_id")));
}
StorePanel::StorePanel() {
}
StorePanel::~StorePanel() {
}
void StorePanel::_notification(int p_what) {
if (p_what == NOTIFICATION_ENTER_TREE && !setup_done) {
_setup_ui();
}
}
void StorePanel::_setup_ui() {
if (setup_done) return;
setup_done = true;
// Main scroll
scroll = memnew(ScrollContainer);
scroll->set_v_size_flags(SIZE_EXPAND_FILL);
scroll->set_h_size_flags(SIZE_EXPAND_FILL);
add_child(scroll);
VBoxContainer *content = memnew(VBoxContainer);
content->set_h_size_flags(SIZE_EXPAND_FILL);
scroll->add_child(content);
// Featured banner
featured_banner = memnew(FeaturedBanner);
content->add_child(featured_banner);
// Categories
HBoxContainer *cat_row = memnew(HBoxContainer);
content->add_child(cat_row);
category_list = memnew(HBoxContainer);
cat_row->add_child(category_list);
const char *categories[] = { "All", "Action", "Adventure", "RPG", "Strategy", "Simulation", "Indie", "Free" };
for (int i = 0; i < 8; i++) {
Button *btn = memnew(Button);
btn->set_text(categories[i]);
btn->set_toggle_mode(true);
btn->set_pressed(i == 0);
btn->connect("pressed", callable_mp(this, &StorePanel::_on_category_selected).bind(categories[i]));
category_list->add_child(btn);
if (i == 0) {
btn->set_pressed(true);
}
}
// Sale section
Label *sale_label = memnew(Label);
sale_label->set_text("On Sale");
sale_label->add_theme_font_size_override("font_size", 18 * EDSCALE);
content->add_child(sale_label);
sale_scroll = memnew(ScrollContainer);
sale_scroll->set_vertical_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
sale_scroll->set_custom_minimum_size(Size2(0, 340 * EDSCALE));
content->add_child(sale_scroll);
sale_grid = memnew(HBoxContainer);
sale_scroll->add_child(sale_grid);
// New releases
Label *new_label = memnew(Label);
new_label->set_text("New Releases");
new_label->add_theme_font_size_override("font_size", 18 * EDSCALE);
content->add_child(new_label);
new_releases_scroll = memnew(ScrollContainer);
new_releases_scroll->set_vertical_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
new_releases_scroll->set_custom_minimum_size(Size2(0, 340 * EDSCALE));
content->add_child(new_releases_scroll);
new_releases_grid = memnew(HBoxContainer);
new_releases_scroll->add_child(new_releases_grid);
// Popular
Label *pop_label = memnew(Label);
pop_label->set_text("Popular");
pop_label->add_theme_font_size_override("font_size", 18 * EDSCALE);
content->add_child(pop_label);
popular_scroll = memnew(ScrollContainer);
popular_scroll->set_vertical_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
popular_scroll->set_custom_minimum_size(Size2(0, 340 * EDSCALE));
content->add_child(popular_scroll);
popular_grid = memnew(HBoxContainer);
popular_scroll->add_child(popular_grid);
// All items (for search/category)
all_items_label = memnew(Label);
all_items_label->set_text("All Games");
all_items_label->add_theme_font_size_override("font_size", 18 * EDSCALE);
content->add_child(all_items_label);
items_grid = memnew(GridContainer);
items_grid->set_columns(4);
content->add_child(items_grid);
// Cart panel (slide-out)
cart_panel = memnew(PanelContainer);
cart_panel->set_custom_minimum_size(Size2(300 * EDSCALE, 0));
cart_panel->set_visible(false);
add_child(cart_panel);
VBoxContainer *cart_content = memnew(VBoxContainer);
cart_panel->add_child(cart_content);
Label *cart_title = memnew(Label);
cart_title->set_text("Shopping Cart");
cart_content->add_child(cart_title);
cart_items = memnew(VBoxContainer);
cart_items->set_v_size_flags(SIZE_EXPAND_FILL);
cart_content->add_child(cart_items);
cart_total = memnew(Label);
cart_content->add_child(cart_total);
checkout_button = memnew(Button);
checkout_button->set_text("Checkout");
checkout_button->connect("pressed", callable_mp(this, &StorePanel::_checkout));
cart_content->add_child(checkout_button);
// Item details dialog
item_details = memnew(AcceptDialog);
item_details->set_title("Game Details");
item_details->get_ok_button()->set_text("Add to Cart");
item_details->connect("confirmed", callable_mp(this, &StorePanel::_add_viewing_to_cart));
add_child(item_details);
}
void StorePanel::set_launcher(AethexLauncher *p_launcher) {
launcher = p_launcher;
}
void StorePanel::refresh() {
if (!launcher || !featured_banner) return; // Not setup yet
auto store = launcher->get_store();
if (store.is_null()) return;
// Load featured
featured_banner->set_items(store->get_featured_items());
// Load sale items
if (sale_grid) _populate_grid(sale_grid, store->get_sale_items());
// Load new releases
if (new_releases_grid) _populate_grid(new_releases_grid, store->get_new_releases());
// Load popular
if (popular_grid) _populate_grid(popular_grid, store->get_popular_items());
// Load all items
if (items_grid) _populate_grid(items_grid, store->get_all_items());
}
void StorePanel::search(const String &p_query) {
current_search = p_query;
_filter_items();
}
void StorePanel::_populate_grid(Control *p_grid, const TypedArray<StoreItem> &p_items) {
if (!p_grid) return;
// Clear existing - proper removal
for (int i = p_grid->get_child_count() - 1; i >= 0; i--) {
Node *child = p_grid->get_child(i);
p_grid->remove_child(child);
child->queue_free();
}
for (int i = 0; i < p_items.size(); i++) {
Ref<StoreItem> item = p_items[i];
StoreItemCard *card = memnew(StoreItemCard);
card->set_item(item);
card->connect("item_clicked", callable_mp(this, &StorePanel::_show_item_details));
card->connect("add_to_cart", callable_mp(this, &StorePanel::_add_to_cart));
card->connect("wishlist_toggled", callable_mp(this, &StorePanel::_toggle_wishlist));
p_grid->add_child(card);
}
}
void StorePanel::_filter_items() {
if (!launcher) return;
auto store = launcher->get_store();
if (store.is_null()) return;
TypedArray<StoreItem> items;
if (!current_search.is_empty()) {
// Search is async, so just trigger it and items will be updated via signal
store->search(current_search);
items = store->get_items();
all_items_label->set_text(vformat("Search: %s", current_search));
} else if (!current_category.is_empty() && current_category != "All") {
items = store->get_items_by_category(current_category);
all_items_label->set_text(current_category);
} else {
items = store->get_all_items();
all_items_label->set_text("All Games");
}
_populate_grid(items_grid, items);
}
void StorePanel::_on_category_selected(const String &p_category) {
current_category = p_category;
current_search = "";
_filter_items();
// Update button states
for (int i = 0; i < category_list->get_child_count(); i++) {
Button *btn = Object::cast_to<Button>(category_list->get_child(i));
if (btn) {
btn->set_pressed(btn->get_text() == p_category);
}
}
}
void StorePanel::_show_item_details(const String &p_item_id) {
viewing_item_id = p_item_id;
if (!launcher) return;
auto store = launcher->get_store();
if (store.is_null()) return;
Ref<StoreItem> item = store->get_item(p_item_id);
if (item.is_null()) return;
item_details->set_title(item->get_title());
// Would populate dialog content here
item_details->popup_centered_ratio(0.6);
}
void StorePanel::_add_to_cart(const String &p_item_id) {
if (!launcher) return;
auto store = launcher->get_store();
if (store.is_valid()) {
store->add_to_cart(p_item_id);
_update_cart();
}
}
void StorePanel::_add_viewing_to_cart() {
_add_to_cart(viewing_item_id);
}
void StorePanel::_remove_from_cart(const String &p_item_id) {
if (!launcher) return;
auto store = launcher->get_store();
if (store.is_valid()) {
store->remove_from_cart(p_item_id);
_update_cart();
}
}
void StorePanel::_update_cart() {
// Clear cart display
while (cart_items->get_child_count() > 0) {
cart_items->get_child(0)->queue_free();
cart_items->remove_child(cart_items->get_child(0));
}
if (!launcher) return;
auto store = launcher->get_store();
if (store.is_null()) return;
TypedArray<StoreItem> cart = store->get_cart_items();
float total = 0;
for (int i = 0; i < cart.size(); i++) {
Ref<StoreItem> item = cart[i];
if (item.is_null()) continue;
HBoxContainer *row = memnew(HBoxContainer);
cart_items->add_child(row);
Label *name = memnew(Label);
name->set_text(item->get_title());
name->set_h_size_flags(SIZE_EXPAND_FILL);
row->add_child(name);
float price = item->get_price() * (1.0 - item->get_discount_percent() / 100.0);
Label *price_lbl = memnew(Label);
price_lbl->set_text(vformat("$%.2f", price));
row->add_child(price_lbl);
Button *remove = memnew(Button);
remove->set_text("X");
remove->connect("pressed", callable_mp(this, &StorePanel::_remove_from_cart).bind(item->get_id()));
row->add_child(remove);
total += price;
}
cart_total->set_text(vformat("Total: $%.2f", total));
}
void StorePanel::_toggle_wishlist(const String &p_item_id) {
if (!launcher) return;
auto store = launcher->get_store();
if (store.is_valid()) {
store->toggle_wishlist(p_item_id);
}
}
void StorePanel::_checkout() {
if (!launcher) return;
auto store = launcher->get_store();
if (store.is_valid()) {
store->checkout();
}
}
void StorePanel::_show_cart() {
cart_panel->set_visible(true);
_update_cart();
}
void StorePanel::_hide_cart() {
cart_panel->set_visible(false);
}
#endif // TOOLS_ENABLED

View file

@ -0,0 +1,164 @@
/**************************************************************************/
/* store_panel.h */
/**************************************************************************/
/* AeThex Engine */
/* https://aethex.dev */
/**************************************************************************/
#ifndef STORE_PANEL_H
#define STORE_PANEL_H
#ifdef TOOLS_ENABLED
#include "scene/gui/box_container.h"
#include "scene/gui/scroll_container.h"
#include "scene/gui/grid_container.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/button.h"
#include "scene/gui/label.h"
#include "scene/gui/texture_rect.h"
#include "scene/gui/dialogs.h"
class AethexLauncher;
class StoreItem;
class StoreItemCard : public VBoxContainer {
GDCLASS(StoreItemCard, VBoxContainer);
private:
bool setup_done = false;
Ref<StoreItem> item;
TextureRect *cover_image = nullptr;
Label *title_label = nullptr;
Label *developer_label = nullptr;
Label *discount_label = nullptr;
Label *original_price = nullptr;
Label *price_label = nullptr;
Label *rating_stars = nullptr;
Label *review_count = nullptr;
Button *buy_button = nullptr;
Button *wishlist_button = nullptr;
void _setup_ui();
void _update_display();
void _on_buy_pressed();
void _on_wishlist_toggled(bool p_pressed);
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void set_item(const Ref<StoreItem> &p_item);
Ref<StoreItem> get_item() const;
void set_wishlisted(bool p_wishlisted);
StoreItemCard();
~StoreItemCard();
};
class FeaturedBanner : public Control {
GDCLASS(FeaturedBanner, Control);
private:
bool setup_done = false;
TypedArray<StoreItem> items;
int current_index = 0;
TextureRect *banner_image = nullptr;
Label *title_label = nullptr;
Label *description_label = nullptr;
Label *price_label = nullptr;
Button *prev_button = nullptr;
Button *next_button = nullptr;
Label *indicator = nullptr;
void _setup_ui();
void _update_display();
void _prev_item();
void _next_item();
protected:
static void _bind_methods() {}
void _notification(int p_what);
public:
void set_items(const TypedArray<StoreItem> &p_items);
FeaturedBanner();
~FeaturedBanner();
};
class StorePanel : public VBoxContainer {
GDCLASS(StorePanel, VBoxContainer);
private:
AethexLauncher *launcher = nullptr;
bool setup_done = false;
// Content
ScrollContainer *scroll = nullptr;
// Featured
FeaturedBanner *featured_banner = nullptr;
// Categories
HBoxContainer *category_list = nullptr;
String current_category;
String current_search;
// Item grids
ScrollContainer *sale_scroll = nullptr;
HBoxContainer *sale_grid = nullptr;
ScrollContainer *new_releases_scroll = nullptr;
HBoxContainer *new_releases_grid = nullptr;
ScrollContainer *popular_scroll = nullptr;
HBoxContainer *popular_grid = nullptr;
Label *all_items_label = nullptr;
GridContainer *items_grid = nullptr;
// Cart
PanelContainer *cart_panel = nullptr;
VBoxContainer *cart_items = nullptr;
Label *cart_total = nullptr;
Button *checkout_button = nullptr;
// Item details
AcceptDialog *item_details = nullptr;
String viewing_item_id;
void _setup_ui();
void _populate_grid(Control *p_grid, const TypedArray<StoreItem> &p_items);
void _filter_items();
void _on_category_selected(const String &p_category);
void _show_item_details(const String &p_item_id);
void _add_to_cart(const String &p_item_id);
void _add_viewing_to_cart();
void _remove_from_cart(const String &p_item_id);
void _update_cart();
void _toggle_wishlist(const String &p_item_id);
void _checkout();
void _show_cart();
void _hide_cart();
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void set_launcher(AethexLauncher *p_launcher);
void refresh();
void search(const String &p_query);
StorePanel();
~StorePanel();
};
#endif // TOOLS_ENABLED
#endif // STORE_PANEL_H

View file

@ -151,7 +151,7 @@ public:
}; };
private: private:
String api_base_url = "https://api.aethex.io/forge/v1"; String api_base_url = "https://api.aethex.dev/forge/v1";
String api_key; String api_key;
String user_token; String user_token;
String user_id; String user_id;

View file

@ -16,7 +16,7 @@
#include "core/os/os.h" #include "core/os/os.h"
#include "core/variant/typed_array.h" #include "core/variant/typed_array.h"
const String AeThexTemplateManager::TEMPLATES_API_URL = "https://api.aethex.io/templates/v1"; const String AeThexTemplateManager::TEMPLATES_API_URL = "https://api.aethex.dev/templates/v1";
const String AeThexTemplateManager::TEMPLATES_CACHE_PATH = "user://aethex_templates/"; const String AeThexTemplateManager::TEMPLATES_CACHE_PATH = "user://aethex_templates/";
AeThexTemplateManager *AeThexTemplateManager::singleton = nullptr; AeThexTemplateManager *AeThexTemplateManager::singleton = nullptr;

View file

@ -36,7 +36,7 @@ DiscordRPC *DiscordRPC::get_singleton() {
void DiscordRPC::_bind_methods() { void DiscordRPC::_bind_methods() {
ClassDB::bind_method(D_METHOD("initialize", "application_id"), &DiscordRPC::initialize); ClassDB::bind_method(D_METHOD("initialize", "application_id"), &DiscordRPC::initialize);
ClassDB::bind_method(D_METHOD("shutdown"), &DiscordRPC::shutdown); ClassDB::bind_method(D_METHOD("shutdown"), &DiscordRPC::shutdown);
ClassDB::bind_method(D_METHOD("is_connected"), &DiscordRPC::is_connected); ClassDB::bind_method(D_METHOD("is_rpc_connected"), &DiscordRPC::is_rpc_connected);
ClassDB::bind_method(D_METHOD("get_connection_state"), &DiscordRPC::get_state); ClassDB::bind_method(D_METHOD("get_connection_state"), &DiscordRPC::get_state);
ClassDB::bind_method(D_METHOD("set_details", "details"), &DiscordRPC::set_details); ClassDB::bind_method(D_METHOD("set_details", "details"), &DiscordRPC::set_details);
@ -327,8 +327,7 @@ void DiscordRPC::_thread_func() {
} }
// Check for READY event // Check for READY event
Variant parsed; Variant parsed = JSON::parse_string(response);
JSON::parse(response, parsed);
Dictionary resp = parsed; Dictionary resp = parsed;
if (resp.get("evt", "") != "READY") { if (resp.get("evt", "") != "READY") {
@ -359,7 +358,8 @@ void DiscordRPC::initialize(const String &p_app_id) {
state = CONNECTING; state = CONNECTING;
running = true; running = true;
thread = Thread::create(_thread_wrapper, this); thread = memnew(Thread);
thread->start(_thread_wrapper, this);
} }
void DiscordRPC::shutdown() { void DiscordRPC::shutdown() {
@ -369,7 +369,7 @@ void DiscordRPC::shutdown() {
running = false; running = false;
if (thread) { if (thread) {
Thread::wait_to_finish(thread); thread->wait_to_finish();
memdelete(thread); memdelete(thread);
thread = nullptr; thread = nullptr;
} }
@ -378,7 +378,7 @@ void DiscordRPC::shutdown() {
state = DISCONNECTED; state = DISCONNECTED;
} }
bool DiscordRPC::is_connected() const { bool DiscordRPC::is_rpc_connected() const {
return state == CONNECTED; return state == CONNECTED;
} }

View file

@ -73,7 +73,7 @@ public:
// Initialization // Initialization
void initialize(const String &p_app_id); void initialize(const String &p_app_id);
void shutdown(); void shutdown();
bool is_connected() const; bool is_rpc_connected() const;
ConnectionState get_state() const; ConnectionState get_state() const;
// Presence setters // Presence setters

View file

@ -1,4 +1,4 @@
//********************************************************* //*********************************************************
// //
// Copyright (c) Microsoft Corporation. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT License (MIT). // Licensed under the MIT License (MIT).

View file

@ -1,4 +1,4 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics) // Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe // SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT

View file

@ -1,4 +1,4 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics) // Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe // SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT

View file

@ -1,4 +1,4 @@
/* bcdec.h - v0.97 /* bcdec.h - v0.97
provides functions to decompress blocks of BC compressed images provides functions to decompress blocks of BC compressed images
written by Sergii "iOrange" Kudlai in 2022 written by Sergii "iOrange" Kudlai in 2022
@ -1249,7 +1249,7 @@ BCDECDEF void bcdec_bc7(const void* compressedBlock, void* decompressedBlock, in
/* The index value for interpolating color comes from the secondary index bits for the texel /* The index value for interpolating color comes from the secondary index bits for the texel
if the mode has an index selection bit and its value is one, and from the primary index bits otherwise. if the mode has an index selection bit and its value is one, and from the primary index bits otherwise.
The alpha index comes from the secondary index bits if the block has a secondary index and The alpha index comes from the secondary index bits if the block has a secondary index and
the block either doesnt have an index selection bit or that bit is zero, and from the primary index bits otherwise. */ the block either doesnt have an index selection bit or that bit is zero, and from the primary index bits otherwise. */
if (!indexSelectionBit) { if (!indexSelectionBit) {
r = bcdec__interpolate(endpoints[partitionSet * 2][0], endpoints[partitionSet * 2 + 1][0], weights, index); r = bcdec__interpolate(endpoints[partitionSet * 2][0], endpoints[partitionSet * 2 + 1][0], weights, index);
g = bcdec__interpolate(endpoints[partitionSet * 2][1], endpoints[partitionSet * 2 + 1][1], weights, index); g = bcdec__interpolate(endpoints[partitionSet * 2][1], endpoints[partitionSet * 2 + 1][1], weights, index);
@ -1264,10 +1264,10 @@ BCDECDEF void bcdec_bc7(const void* compressedBlock, void* decompressedBlock, in
} }
switch (rotation) { switch (rotation) {
case 1: { /* 01 Block format is Scalar(R) Vector(AGB) - swap A and R */ case 1: { /* 01 Block format is Scalar(R) Vector(AGB) - swap A and R */
bcdec__swap_values(&a, &r); bcdec__swap_values(&a, &r);
} break; } break;
case 2: { /* 10 Block format is Scalar(G) Vector(RAB) - swap A and G */ case 2: { /* 10 Block format is Scalar(G) Vector(RAB) - swap A and G */
bcdec__swap_values(&a, &g); bcdec__swap_values(&a, &g);
} break; } break;
case 3: { /* 11 - Block format is Scalar(B) Vector(RGA) - swap A and B */ case 3: { /* 11 - Block format is Scalar(B) Vector(RGA) - swap A and B */

View file

@ -1,4 +1,4 @@
/* /*
* Copyright (c) 2021 - 2024 the ThorVG project. All rights reserved. * Copyright (c) 2021 - 2024 the ThorVG project. All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy

View file

@ -0,0 +1,39 @@
# AeThex Development Environment
# https://aethex.dev
# This file contains the local development environment configuration
# Copy this to .env for local development
# Server Configuration
NODE_ENV=development
PORT=3001
# Database - PostgreSQL (run with Docker or local install)
DATABASE_URL="postgresql://aethex:aethex_dev@localhost:5432/aethex_dev"
# JWT Configuration
JWT_SECRET=aethex-dev-secret-change-in-production
JWT_EXPIRES_IN=7d
REFRESH_TOKEN_SECRET=aethex-refresh-secret-change-in-prod
# OAuth Providers (optional for development)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
# Frontend URL
FRONTEND_URL=http://localhost:3001
# C++ Launcher connects to this
API_BASE_URL=http://localhost:3001
# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=1000
# Email (optional)
SMTP_HOST=
SMTP_PORT=
SMTP_USER=
SMTP_PASS=

View file

@ -3,11 +3,11 @@ version: '3.8'
services: services:
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine
container_name: aethex-auth-db container_name: aethex-postgres
environment: environment:
POSTGRES_USER: aethex POSTGRES_USER: aethex
POSTGRES_PASSWORD: dev_password_change_in_prod POSTGRES_PASSWORD: aethex_dev
POSTGRES_DB: aethex_auth POSTGRES_DB: aethex_dev
ports: ports:
- "5432:5432" - "5432:5432"
volumes: volumes:
@ -18,32 +18,8 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
auth-service: # Run auth-service locally with: npm run dev
build: # This docker-compose only runs the database
context: .
dockerfile: Dockerfile
container_name: aethex-auth-service
environment:
NODE_ENV: development
PORT: 3000
DATABASE_URL: postgresql://aethex:dev_password_change_in_prod@postgres:5432/aethex_auth
JWT_SECRET: dev_secret_change_in_prod_make_it_long_and_random
JWT_EXPIRES_IN: 7d
REFRESH_TOKEN_SECRET: dev_refresh_secret_change_in_prod
GOOGLE_CLIENT_ID: your_google_client_id
GOOGLE_CLIENT_SECRET: your_google_client_secret
GITHUB_CLIENT_ID: your_github_client_id
GITHUB_CLIENT_SECRET: your_github_client_secret
FRONTEND_URL: http://localhost:9002
ports:
- "3000:3000"
depends_on:
postgres:
condition: service_healthy
volumes:
- ./src:/app/src
- ./prisma:/app/prisma
command: npm run dev
volumes: volumes:
postgres_data: postgres_data:

View file

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../baseline-browser-mapping/dist/cli.cjs" "$@"
else
exec node "$basedir/../baseline-browser-mapping/dist/cli.cjs" "$@"
fi

View file

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\baseline-browser-mapping\dist\cli.cjs" %*

View file

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args
} else {
& "$basedir/node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args
} else {
& "node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
services/auth-service/node_modules/.bin/browserslist generated vendored Normal file
View file

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../browserslist/cli.js" "$@"
else
exec node "$basedir/../browserslist/cli.js" "$@"
fi

View file

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\browserslist\cli.js" %*

View file

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../browserslist/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../browserslist/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../browserslist/cli.js" $args
} else {
& "node$exe" "$basedir/../browserslist/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
services/auth-service/node_modules/.bin/color-support generated vendored Normal file
View file

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../color-support/bin.js" "$@"
else
exec node "$basedir/../color-support/bin.js" "$@"
fi

View file

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\color-support\bin.js" %*

View file

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../color-support/bin.js" $args
} else {
& "$basedir/node$exe" "$basedir/../color-support/bin.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../color-support/bin.js" $args
} else {
& "node$exe" "$basedir/../color-support/bin.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
services/auth-service/node_modules/.bin/create-jest generated vendored Normal file
View file

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../create-jest/bin/create-jest.js" "$@"
else
exec node "$basedir/../create-jest/bin/create-jest.js" "$@"
fi

View file

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\create-jest\bin\create-jest.js" %*

View file

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../create-jest/bin/create-jest.js" $args
} else {
& "$basedir/node$exe" "$basedir/../create-jest/bin/create-jest.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../create-jest/bin/create-jest.js" $args
} else {
& "node$exe" "$basedir/../create-jest/bin/create-jest.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
services/auth-service/node_modules/.bin/esbuild generated vendored Normal file
View file

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../esbuild/bin/esbuild" "$@"
else
exec node "$basedir/../esbuild/bin/esbuild" "$@"
fi

17
services/auth-service/node_modules/.bin/esbuild.cmd generated vendored Normal file
View file

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\esbuild\bin\esbuild" %*

28
services/auth-service/node_modules/.bin/esbuild.ps1 generated vendored Normal file
View file

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
} else {
& "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../esbuild/bin/esbuild" $args
} else {
& "node$exe" "$basedir/../esbuild/bin/esbuild" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
services/auth-service/node_modules/.bin/esparse generated vendored Normal file
View file

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../esprima/bin/esparse.js" "$@"
else
exec node "$basedir/../esprima/bin/esparse.js" "$@"
fi

17
services/auth-service/node_modules/.bin/esparse.cmd generated vendored Normal file
View file

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\esprima\bin\esparse.js" %*

28
services/auth-service/node_modules/.bin/esparse.ps1 generated vendored Normal file
View file

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../esprima/bin/esparse.js" $args
} else {
& "$basedir/node$exe" "$basedir/../esprima/bin/esparse.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../esprima/bin/esparse.js" $args
} else {
& "node$exe" "$basedir/../esprima/bin/esparse.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
services/auth-service/node_modules/.bin/esvalidate generated vendored Normal file
View file

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../esprima/bin/esvalidate.js" "$@"
else
exec node "$basedir/../esprima/bin/esvalidate.js" "$@"
fi

17
services/auth-service/node_modules/.bin/esvalidate.cmd generated vendored Normal file
View file

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\esprima\bin\esvalidate.js" %*

28
services/auth-service/node_modules/.bin/esvalidate.ps1 generated vendored Normal file
View file

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../esprima/bin/esvalidate.js" $args
} else {
& "$basedir/node$exe" "$basedir/../esprima/bin/esvalidate.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../esprima/bin/esvalidate.js" $args
} else {
& "node$exe" "$basedir/../esprima/bin/esvalidate.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
services/auth-service/node_modules/.bin/handlebars generated vendored Normal file
View file

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../handlebars/bin/handlebars" "$@"
else
exec node "$basedir/../handlebars/bin/handlebars" "$@"
fi

17
services/auth-service/node_modules/.bin/handlebars.cmd generated vendored Normal file
View file

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\handlebars\bin\handlebars" %*

28
services/auth-service/node_modules/.bin/handlebars.ps1 generated vendored Normal file
View file

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../handlebars/bin/handlebars" $args
} else {
& "$basedir/node$exe" "$basedir/../handlebars/bin/handlebars" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../handlebars/bin/handlebars" $args
} else {
& "node$exe" "$basedir/../handlebars/bin/handlebars" $args
}
$ret=$LASTEXITCODE
}
exit $ret

View file

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../import-local/fixtures/cli.js" "$@"
else
exec node "$basedir/../import-local/fixtures/cli.js" "$@"
fi

View file

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\import-local\fixtures\cli.js" %*

View file

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../import-local/fixtures/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../import-local/fixtures/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../import-local/fixtures/cli.js" $args
} else {
& "node$exe" "$basedir/../import-local/fixtures/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
services/auth-service/node_modules/.bin/jest generated vendored Normal file
View file

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../jest/bin/jest.js" "$@"
else
exec node "$basedir/../jest/bin/jest.js" "$@"
fi

17
services/auth-service/node_modules/.bin/jest.cmd generated vendored Normal file
View file

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\jest\bin\jest.js" %*

28
services/auth-service/node_modules/.bin/jest.ps1 generated vendored Normal file
View file

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../jest/bin/jest.js" $args
} else {
& "$basedir/node$exe" "$basedir/../jest/bin/jest.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../jest/bin/jest.js" $args
} else {
& "node$exe" "$basedir/../jest/bin/jest.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
services/auth-service/node_modules/.bin/js-yaml generated vendored Normal file
View file

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../js-yaml/bin/js-yaml.js" "$@"
else
exec node "$basedir/../js-yaml/bin/js-yaml.js" "$@"
fi

17
services/auth-service/node_modules/.bin/js-yaml.cmd generated vendored Normal file
View file

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\js-yaml\bin\js-yaml.js" %*

28
services/auth-service/node_modules/.bin/js-yaml.ps1 generated vendored Normal file
View file

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../js-yaml/bin/js-yaml.js" $args
} else {
& "$basedir/node$exe" "$basedir/../js-yaml/bin/js-yaml.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../js-yaml/bin/js-yaml.js" $args
} else {
& "node$exe" "$basedir/../js-yaml/bin/js-yaml.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
services/auth-service/node_modules/.bin/jsesc generated vendored Normal file
View file

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../jsesc/bin/jsesc" "$@"
else
exec node "$basedir/../jsesc/bin/jsesc" "$@"
fi

17
services/auth-service/node_modules/.bin/jsesc.cmd generated vendored Normal file
View file

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\jsesc\bin\jsesc" %*

28
services/auth-service/node_modules/.bin/jsesc.ps1 generated vendored Normal file
View file

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../jsesc/bin/jsesc" $args
} else {
& "$basedir/node$exe" "$basedir/../jsesc/bin/jsesc" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../jsesc/bin/jsesc" $args
} else {
& "node$exe" "$basedir/../jsesc/bin/jsesc" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
services/auth-service/node_modules/.bin/json5 generated vendored Normal file
View file

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../json5/lib/cli.js" "$@"
else
exec node "$basedir/../json5/lib/cli.js" "$@"
fi

Some files were not shown because too many files have changed in this diff Show more