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
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:
parent
7c95244e3e
commit
190b6b2eab
8179 changed files with 1384369 additions and 94 deletions
|
|
@ -510,7 +510,7 @@ spec:
|
|||
|
||||
### REST API Standards
|
||||
|
||||
**Base URL:** `https://api.aethex.io/v1`
|
||||
**Base URL:** `https://api.aethex.dev/v1`
|
||||
|
||||
**Request Format:**
|
||||
```json
|
||||
|
|
@ -559,7 +559,7 @@ spec:
|
|||
|
||||
### WebSocket Protocol
|
||||
|
||||
**Connection URL:** `wss://ws.aethex.io/v1/multiplayer`
|
||||
**Connection URL:** `wss://ws.aethex.dev/v1/multiplayer`
|
||||
|
||||
**Authentication:**
|
||||
```json
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ website = "https://godotengine.org"
|
|||
```python
|
||||
short_name = "aethex"
|
||||
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
|
||||
|
|
@ -102,7 +102,7 @@ engine/editor/editor_themes.cpp
|
|||
```cpp
|
||||
// Add AeThex default settings
|
||||
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",
|
||||
true, "")
|
||||
EDITOR_SETTING(Variant::STRING, PROPERTY_HINT_NONE, "aethex/theme",
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ Download export templates for your target platform:
|
|||
# Via Studio IDE: Editor → Export Templates → Download
|
||||
|
||||
# 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/
|
||||
```
|
||||
|
||||
|
|
|
|||
375
docs/LAUNCHER_INTEGRATION_PLAN.md
Normal file
375
docs/LAUNCHER_INTEGRATION_PLAN.md
Normal 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
|
||||
|
|
@ -143,7 +143,7 @@ Test if your Godot project works in AeThex without changes.
|
|||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
# 2. Open your Godot project
|
||||
|
|
@ -769,7 +769,7 @@ git checkout -b aethex-migration
|
|||
|
||||
### 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:**
|
||||
- [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
|
||||
|
||||
**Support:**
|
||||
- [Email Support](mailto:support@aethex.io) - Response within 24h
|
||||
- [Pro Support](https://aethex.io/pro) - Priority support for paying customers
|
||||
- [Email Support](mailto:support@aethex.dev) - Response within 24h
|
||||
- [Pro Support](https://aethex.dev/pro) - Priority support for paying customers
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -734,7 +734,7 @@ Create a community server:
|
|||
|
||||
### Tools
|
||||
|
||||
- **Analytics:** [AeThex Analytics](https://studio.aethex.io/analytics)
|
||||
- **Analytics:** [AeThex Analytics](https://studio.aethex.dev/analytics)
|
||||
- **Marketing:** [Presskit()](https://dopresskit.com/)
|
||||
- **Community:** [Discord](https://discord.com)
|
||||
- **Email:** [Mailchimp](https://mailchimp.com)
|
||||
|
|
@ -748,9 +748,9 @@ Create a community server:
|
|||
|
||||
### Support
|
||||
|
||||
- **Email:** [support@aethex.io](mailto:support@aethex.io)
|
||||
- **Email:** [support@aethex.dev](mailto:support@aethex.dev)
|
||||
- **Discord:** [AeThex Community](https://discord.gg/aethex)
|
||||
- **Docs:** [docs.aethex.io](https://docs.aethex.io)
|
||||
- **Docs:** [docs.aethex.dev](https://docs.aethex.dev)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ website = "https://godotengine.org"
|
|||
|
||||
**To:**
|
||||
```python
|
||||
website = "https://aethex.io" # or your domain
|
||||
website = "https://aethex.dev" # or your domain
|
||||
```
|
||||
|
||||
**Result:** Help → Visit Website will go to your site
|
||||
|
|
|
|||
|
|
@ -256,30 +256,30 @@ See [API Reference](API_REFERENCE.md) for complete documentation.
|
|||
|
||||
### Official
|
||||
|
||||
- **Website:** [https://aethex.io](https://aethex.io)
|
||||
- **Download:** [https://aethex.io/download](https://aethex.io/download)
|
||||
- **Website:** [https://aethex.dev](https://aethex.dev)
|
||||
- **Download:** [https://aethex.dev/download](https://aethex.dev/download)
|
||||
- **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
|
||||
|
||||
- **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)
|
||||
- **Twitter:** [@AeThexEngine](https://twitter.com/AeThexEngine)
|
||||
|
||||
### Learning
|
||||
|
||||
- **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)
|
||||
|
||||
### Support
|
||||
|
||||
- **Documentation:** You're here!
|
||||
- **FAQ:** [https://aethex.io/faq](https://aethex.io/faq)
|
||||
- **Email:** [support@aethex.io](mailto:support@aethex.io)
|
||||
- **Pro Support:** [https://aethex.io/pro](https://aethex.io/pro)
|
||||
- **FAQ:** [https://aethex.dev/faq](https://aethex.dev/faq)
|
||||
- **Email:** [support@aethex.dev](mailto:support@aethex.dev)
|
||||
- **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
|
||||
2. **Ask Community:** Discord is very active
|
||||
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?**
|
||||
- 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)
|
||||
3. Study [Studio Integration](STUDIO_INTEGRATION.md) (20 min)
|
||||
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
|
||||
|
||||
|
|
|
|||
263
docs/UNIFIED_ARCHITECTURE.md
Normal file
263
docs/UNIFIED_ARCHITECTURE.md
Normal 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.**
|
||||
|
|
@ -528,7 +528,7 @@ func _on_button_pressed():
|
|||
|
||||
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**
|
||||
3. **View dashboards:**
|
||||
- Overview: DAU, MAU, retention
|
||||
|
|
@ -758,7 +758,7 @@ func test_analytics():
|
|||
|
||||
- **[Publishing Guide](../PUBLISHING_GUIDE.md)** - Deploy your game with analytics
|
||||
- **[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)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@ Want to write a tutorial? See [CONTRIBUTING.md](../../engine/CONTRIBUTING.md) fo
|
|||
- Check the [API Reference](../API_REFERENCE.md)
|
||||
- Review [GDScript Basics](../GDSCRIPT_BASICS.md)
|
||||
- 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?**
|
||||
- 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)*
|
||||
- Explore [Advanced Topics](../ADVANCED_TOPICS.md) *(coming soon)*
|
||||
- Build your own game!
|
||||
- Share in the [Community Showcase](https://aethex.io/showcase)
|
||||
- Share in the [Community Showcase](https://aethex.dev/showcase)
|
||||
|
||||
Happy building! 🚀
|
||||
|
|
|
|||
BIN
engine/build_output.txt
Normal file
BIN
engine/build_output.txt
Normal file
Binary file not shown.
|
|
@ -56,6 +56,7 @@
|
|||
#include "main/main.h"
|
||||
#include "scene/gui/check_box.h"
|
||||
#include "scene/gui/flow_container.h"
|
||||
#include "scene/gui/item_list.h"
|
||||
#include "scene/gui/line_edit.h"
|
||||
#include "scene/gui/margin_container.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");
|
||||
}
|
||||
|
||||
// ===== 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.
|
||||
|
||||
ProjectManager::ProjectManager() {
|
||||
|
|
@ -1507,6 +1609,31 @@ ProjectManager::ProjectManager() {
|
|||
main_view_container->set_v_size_flags(Control::SIZE_EXPAND_FILL);
|
||||
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.
|
||||
{
|
||||
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)."));
|
||||
}
|
||||
|
||||
// ===== 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.
|
||||
{
|
||||
HBoxContainer *footer_bar = memnew(HBoxContainer);
|
||||
|
|
|
|||
|
|
@ -30,9 +30,15 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "modules/modules_enabled.gen.h" // For aethex_launcher module detection
|
||||
#include "scene/gui/dialogs.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 EditorAbout;
|
||||
class EditorAssetLibrary;
|
||||
|
|
@ -95,12 +101,16 @@ class ProjectManager : public Control {
|
|||
Button *quick_settings_button = nullptr;
|
||||
|
||||
enum MainViewTab {
|
||||
MAIN_VIEW_LAUNCHER, // AeThex tab - first!
|
||||
MAIN_VIEW_PROJECTS,
|
||||
MAIN_VIEW_ASSETLIB,
|
||||
MAIN_VIEW_TERMINAL,
|
||||
MAIN_VIEW_COMMUNITY,
|
||||
MAIN_VIEW_CLOUD,
|
||||
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, Button *> main_view_toggle_map;
|
||||
|
||||
|
|
@ -114,6 +124,36 @@ class ProjectManager : public Control {
|
|||
VBoxContainer *local_projects_vb = 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;
|
||||
|
||||
void _show_about();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* AETHEX ENGINE */
|
||||
/* https://aethex.io */
|
||||
/* https://aethex.dev */
|
||||
/**************************************************************************/
|
||||
|
||||
#ifndef AI_ASSISTANT_H
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* AETHEX ENGINE */
|
||||
/* https://aethex.io */
|
||||
/* https://aethex.dev */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2026-present AeThex Labs. */
|
||||
/**************************************************************************/
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* AETHEX ENGINE */
|
||||
/* https://aethex.io */
|
||||
/* https://aethex.dev */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2026-present AeThex Labs. */
|
||||
/* */
|
||||
|
|
|
|||
|
|
@ -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("disconnect_from_gateway"), &AethexCloud::disconnect_from_gateway);
|
||||
ClassDB::bind_method(D_METHOD("get_status"), &AethexCloud::get_status);
|
||||
ClassDB::bind_method(D_METHOD("is_connected"), &AethexCloud::is_connected);
|
||||
ClassDB::bind_method(D_METHOD("is_cloud_connected"), &AethexCloud::is_cloud_connected);
|
||||
|
||||
// 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);
|
||||
telemetry->set_cloud(this);
|
||||
|
||||
// Initialize WebSocket
|
||||
websocket.instantiate();
|
||||
// Initialize WebSocket using factory method (abstract class)
|
||||
websocket = Ref<WebSocketPeer>(WebSocketPeer::create());
|
||||
}
|
||||
|
||||
AethexCloud::~AethexCloud() {
|
||||
|
|
@ -158,7 +158,7 @@ AethexCloud::ConnectionStatus AethexCloud::get_status() const {
|
|||
return status;
|
||||
}
|
||||
|
||||
bool AethexCloud::is_connected() const {
|
||||
bool AethexCloud::is_cloud_connected() const {
|
||||
return status == STATUS_CONNECTED;
|
||||
}
|
||||
|
||||
|
|
@ -182,8 +182,12 @@ Dictionary AethexCloud::make_request(const String &p_endpoint, HTTPClient::Metho
|
|||
Dictionary result;
|
||||
result["success"] = false;
|
||||
|
||||
Ref<HTTPClient> http;
|
||||
http.instantiate();
|
||||
// Use factory method for abstract HTTPClient class
|
||||
Ref<HTTPClient> http = Ref<HTTPClient>(HTTPClient::create());
|
||||
if (http.is_null()) {
|
||||
result["error"] = "Failed to create HTTP client";
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse gateway URL
|
||||
String host = gateway_url.replace("https://", "").replace("http://", "");
|
||||
|
|
@ -212,7 +216,7 @@ Dictionary AethexCloud::make_request(const String &p_endpoint, HTTPClient::Metho
|
|||
// Prepare headers
|
||||
Vector<String> headers;
|
||||
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()) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Make request
|
||||
err = http->request(p_method, p_endpoint, headers, body_str);
|
||||
// Make request - convert String body to bytes for the API
|
||||
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) {
|
||||
result["error"] = "Request failed";
|
||||
return result;
|
||||
|
|
@ -302,7 +307,7 @@ void AethexCloud::_on_websocket_message(const PackedByteArray &p_data) {
|
|||
Dictionary auth_msg;
|
||||
auth_msg["type"] = "auth";
|
||||
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));
|
||||
}
|
||||
} else if (type == "auth_success") {
|
||||
|
|
@ -327,7 +332,8 @@ void AethexCloud::process(double p_delta) {
|
|||
switch (ws_state) {
|
||||
case WebSocketPeer::STATE_OPEN: {
|
||||
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);
|
||||
}
|
||||
} break;
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ public:
|
|||
Error connect_to_gateway();
|
||||
void disconnect_from_gateway();
|
||||
ConnectionStatus get_status() const;
|
||||
bool is_connected() const;
|
||||
bool is_cloud_connected() const;
|
||||
|
||||
// Auth token (set after login)
|
||||
void set_auth_token(const String &p_token);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include "aethex_cloud.h"
|
||||
#include "core/io/http_client.h"
|
||||
#include "core/os/os.h"
|
||||
#include "core/os/time.h"
|
||||
#include "core/version.h"
|
||||
|
||||
void AethexTelemetry::_bind_methods() {
|
||||
|
|
@ -57,7 +58,7 @@ void AethexTelemetry::track_event(const String &p_event_name, const Dictionary &
|
|||
event["properties"] = p_properties;
|
||||
event["timestamp"] = Time::get_singleton()->get_datetime_string_from_system(true);
|
||||
event["platform"] = OS::get_singleton()->get_name();
|
||||
event["engineVersion"] = VERSION_FULL_CONFIG;
|
||||
event["engineVersion"] = AETHEX_VERSION_FULL_CONFIG;
|
||||
|
||||
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["context"] = p_context;
|
||||
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);
|
||||
|
||||
// Crash reports are sent immediately, not buffered
|
||||
|
|
|
|||
12
engine/modules/aethex_launcher/SCsub
Normal file
12
engine/modules/aethex_launcher/SCsub
Normal 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")
|
||||
692
engine/modules/aethex_launcher/aethex_launcher.cpp
Normal file
692
engine/modules/aethex_launcher/aethex_launcher.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
117
engine/modules/aethex_launcher/aethex_launcher.h
Normal file
117
engine/modules/aethex_launcher/aethex_launcher.h
Normal 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
|
||||
18
engine/modules/aethex_launcher/config.py
Normal file
18
engine/modules/aethex_launcher/config.py
Normal 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"
|
||||
723
engine/modules/aethex_launcher/data/download_manager.cpp
Normal file
723
engine/modules/aethex_launcher/data/download_manager.cpp
Normal 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
|
||||
}
|
||||
179
engine/modules/aethex_launcher/data/download_manager.h
Normal file
179
engine/modules/aethex_launcher/data/download_manager.h
Normal 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
|
||||
402
engine/modules/aethex_launcher/data/friend_system.cpp
Normal file
402
engine/modules/aethex_launcher/data/friend_system.cpp
Normal 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());
|
||||
}
|
||||
165
engine/modules/aethex_launcher/data/friend_system.h
Normal file
165
engine/modules/aethex_launcher/data/friend_system.h
Normal 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
|
||||
623
engine/modules/aethex_launcher/data/game_library.cpp
Normal file
623
engine/modules/aethex_launcher/data/game_library.cpp
Normal 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);
|
||||
}
|
||||
213
engine/modules/aethex_launcher/data/game_library.h
Normal file
213
engine/modules/aethex_launcher/data/game_library.h
Normal 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
|
||||
299
engine/modules/aethex_launcher/data/launcher_profile.cpp
Normal file
299
engine/modules/aethex_launcher/data/launcher_profile.cpp
Normal 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();
|
||||
}
|
||||
179
engine/modules/aethex_launcher/data/launcher_profile.h
Normal file
179
engine/modules/aethex_launcher/data/launcher_profile.h
Normal 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
|
||||
684
engine/modules/aethex_launcher/data/launcher_store.cpp
Normal file
684
engine/modules/aethex_launcher/data/launcher_store.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
264
engine/modules/aethex_launcher/data/launcher_store.h
Normal file
264
engine/modules/aethex_launcher/data/launcher_store.h
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
73
engine/modules/aethex_launcher/register_types.cpp
Normal file
73
engine/modules/aethex_launcher/register_types.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
engine/modules/aethex_launcher/register_types.h
Normal file
16
engine/modules/aethex_launcher/register_types.h
Normal 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
|
||||
420
engine/modules/aethex_launcher/ui/auth_panel.cpp
Normal file
420
engine/modules/aethex_launcher/ui/auth_panel.cpp
Normal 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
|
||||
129
engine/modules/aethex_launcher/ui/auth_panel.h
Normal file
129
engine/modules/aethex_launcher/ui/auth_panel.h
Normal 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
|
||||
442
engine/modules/aethex_launcher/ui/downloads_panel.cpp
Normal file
442
engine/modules/aethex_launcher/ui/downloads_panel.cpp
Normal 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
|
||||
111
engine/modules/aethex_launcher/ui/downloads_panel.h
Normal file
111
engine/modules/aethex_launcher/ui/downloads_panel.h
Normal 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
|
||||
426
engine/modules/aethex_launcher/ui/friends_panel.cpp
Normal file
426
engine/modules/aethex_launcher/ui/friends_panel.cpp
Normal 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
|
||||
136
engine/modules/aethex_launcher/ui/friends_panel.h
Normal file
136
engine/modules/aethex_launcher/ui/friends_panel.h
Normal 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
|
||||
590
engine/modules/aethex_launcher/ui/launcher_panel.cpp
Normal file
590
engine/modules/aethex_launcher/ui/launcher_panel.cpp
Normal 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 §ion, 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
|
||||
121
engine/modules/aethex_launcher/ui/launcher_panel.h
Normal file
121
engine/modules/aethex_launcher/ui/launcher_panel.h
Normal 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
|
||||
566
engine/modules/aethex_launcher/ui/library_panel.cpp
Normal file
566
engine/modules/aethex_launcher/ui/library_panel.cpp
Normal 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
|
||||
176
engine/modules/aethex_launcher/ui/library_panel.h
Normal file
176
engine/modules/aethex_launcher/ui/library_panel.h
Normal 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
|
||||
391
engine/modules/aethex_launcher/ui/profile_panel.cpp
Normal file
391
engine/modules/aethex_launcher/ui/profile_panel.cpp
Normal 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
|
||||
99
engine/modules/aethex_launcher/ui/profile_panel.h
Normal file
99
engine/modules/aethex_launcher/ui/profile_panel.h
Normal 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
|
||||
615
engine/modules/aethex_launcher/ui/store_panel.cpp
Normal file
615
engine/modules/aethex_launcher/ui/store_panel.cpp
Normal 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
|
||||
164
engine/modules/aethex_launcher/ui/store_panel.h
Normal file
164
engine/modules/aethex_launcher/ui/store_panel.h
Normal 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
|
||||
|
|
@ -151,7 +151,7 @@ public:
|
|||
};
|
||||
|
||||
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 user_token;
|
||||
String user_id;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
#include "core/os/os.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/";
|
||||
|
||||
AeThexTemplateManager *AeThexTemplateManager::singleton = nullptr;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ DiscordRPC *DiscordRPC::get_singleton() {
|
|||
void DiscordRPC::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("initialize", "application_id"), &DiscordRPC::initialize);
|
||||
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("set_details", "details"), &DiscordRPC::set_details);
|
||||
|
|
@ -327,8 +327,7 @@ void DiscordRPC::_thread_func() {
|
|||
}
|
||||
|
||||
// Check for READY event
|
||||
Variant parsed;
|
||||
JSON::parse(response, parsed);
|
||||
Variant parsed = JSON::parse_string(response);
|
||||
Dictionary resp = parsed;
|
||||
|
||||
if (resp.get("evt", "") != "READY") {
|
||||
|
|
@ -359,7 +358,8 @@ void DiscordRPC::initialize(const String &p_app_id) {
|
|||
state = CONNECTING;
|
||||
running = true;
|
||||
|
||||
thread = Thread::create(_thread_wrapper, this);
|
||||
thread = memnew(Thread);
|
||||
thread->start(_thread_wrapper, this);
|
||||
}
|
||||
|
||||
void DiscordRPC::shutdown() {
|
||||
|
|
@ -369,7 +369,7 @@ void DiscordRPC::shutdown() {
|
|||
running = false;
|
||||
|
||||
if (thread) {
|
||||
Thread::wait_to_finish(thread);
|
||||
thread->wait_to_finish();
|
||||
memdelete(thread);
|
||||
thread = nullptr;
|
||||
}
|
||||
|
|
@ -378,7 +378,7 @@ void DiscordRPC::shutdown() {
|
|||
state = DISCONNECTED;
|
||||
}
|
||||
|
||||
bool DiscordRPC::is_connected() const {
|
||||
bool DiscordRPC::is_rpc_connected() const {
|
||||
return state == CONNECTED;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ public:
|
|||
// Initialization
|
||||
void initialize(const String &p_app_id);
|
||||
void shutdown();
|
||||
bool is_connected() const;
|
||||
bool is_rpc_connected() const;
|
||||
ConnectionState get_state() const;
|
||||
|
||||
// Presence setters
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//*********************************************************
|
||||
//*********************************************************
|
||||
//
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License (MIT).
|
||||
|
|
|
|||
|
|
@ -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-License-Identifier: MIT
|
||||
|
||||
|
|
|
|||
|
|
@ -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-License-Identifier: MIT
|
||||
|
||||
|
|
|
|||
8
engine/thirdparty/misc/bcdec.h
vendored
8
engine/thirdparty/misc/bcdec.h
vendored
|
|
@ -1,4 +1,4 @@
|
|||
/* bcdec.h - v0.97
|
||||
/* bcdec.h - v0.97
|
||||
provides functions to decompress blocks of BC compressed images
|
||||
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
|
||||
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 block either doesn’t have an index selection bit or that bit is zero, and from the primary index bits otherwise. */
|
||||
the block either doesn’t have an index selection bit or that bit is zero, and from the primary index bits otherwise. */
|
||||
if (!indexSelectionBit) {
|
||||
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);
|
||||
|
|
@ -1264,10 +1264,10 @@ BCDECDEF void bcdec_bc7(const void* compressedBlock, void* decompressedBlock, in
|
|||
}
|
||||
|
||||
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);
|
||||
} 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);
|
||||
} break;
|
||||
case 3: { /* 11 - Block format is Scalar(B) Vector(RGA) - swap A and B */
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*
|
||||
* Copyright (c) 2021 - 2024 the ThorVG project. All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
|
|||
39
services/auth-service/.env
Normal file
39
services/auth-service/.env
Normal 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=
|
||||
|
|
@ -3,11 +3,11 @@ version: '3.8'
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: aethex-auth-db
|
||||
container_name: aethex-postgres
|
||||
environment:
|
||||
POSTGRES_USER: aethex
|
||||
POSTGRES_PASSWORD: dev_password_change_in_prod
|
||||
POSTGRES_DB: aethex_auth
|
||||
POSTGRES_PASSWORD: aethex_dev
|
||||
POSTGRES_DB: aethex_dev
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
|
|
@ -18,32 +18,8 @@ services:
|
|||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
auth-service:
|
||||
build:
|
||||
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
|
||||
# Run auth-service locally with: npm run dev
|
||||
# This docker-compose only runs the database
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
|
|
|||
16
services/auth-service/node_modules/.bin/baseline-browser-mapping
generated
vendored
Normal file
16
services/auth-service/node_modules/.bin/baseline-browser-mapping
generated
vendored
Normal 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
|
||||
17
services/auth-service/node_modules/.bin/baseline-browser-mapping.cmd
generated
vendored
Normal file
17
services/auth-service/node_modules/.bin/baseline-browser-mapping.cmd
generated
vendored
Normal 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" %*
|
||||
28
services/auth-service/node_modules/.bin/baseline-browser-mapping.ps1
generated
vendored
Normal file
28
services/auth-service/node_modules/.bin/baseline-browser-mapping.ps1
generated
vendored
Normal 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
16
services/auth-service/node_modules/.bin/browserslist
generated
vendored
Normal 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
|
||||
17
services/auth-service/node_modules/.bin/browserslist.cmd
generated
vendored
Normal file
17
services/auth-service/node_modules/.bin/browserslist.cmd
generated
vendored
Normal 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" %*
|
||||
28
services/auth-service/node_modules/.bin/browserslist.ps1
generated
vendored
Normal file
28
services/auth-service/node_modules/.bin/browserslist.ps1
generated
vendored
Normal 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
16
services/auth-service/node_modules/.bin/color-support
generated
vendored
Normal 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
|
||||
17
services/auth-service/node_modules/.bin/color-support.cmd
generated
vendored
Normal file
17
services/auth-service/node_modules/.bin/color-support.cmd
generated
vendored
Normal 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" %*
|
||||
28
services/auth-service/node_modules/.bin/color-support.ps1
generated
vendored
Normal file
28
services/auth-service/node_modules/.bin/color-support.ps1
generated
vendored
Normal 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
16
services/auth-service/node_modules/.bin/create-jest
generated
vendored
Normal 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
|
||||
17
services/auth-service/node_modules/.bin/create-jest.cmd
generated
vendored
Normal file
17
services/auth-service/node_modules/.bin/create-jest.cmd
generated
vendored
Normal 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" %*
|
||||
28
services/auth-service/node_modules/.bin/create-jest.ps1
generated
vendored
Normal file
28
services/auth-service/node_modules/.bin/create-jest.ps1
generated
vendored
Normal 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
16
services/auth-service/node_modules/.bin/esbuild
generated
vendored
Normal 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
17
services/auth-service/node_modules/.bin/esbuild.cmd
generated
vendored
Normal 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
28
services/auth-service/node_modules/.bin/esbuild.ps1
generated
vendored
Normal 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
16
services/auth-service/node_modules/.bin/esparse
generated
vendored
Normal 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
17
services/auth-service/node_modules/.bin/esparse.cmd
generated
vendored
Normal 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
28
services/auth-service/node_modules/.bin/esparse.ps1
generated
vendored
Normal 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
16
services/auth-service/node_modules/.bin/esvalidate
generated
vendored
Normal 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
17
services/auth-service/node_modules/.bin/esvalidate.cmd
generated
vendored
Normal 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
28
services/auth-service/node_modules/.bin/esvalidate.ps1
generated
vendored
Normal 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
16
services/auth-service/node_modules/.bin/handlebars
generated
vendored
Normal 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
17
services/auth-service/node_modules/.bin/handlebars.cmd
generated
vendored
Normal 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
28
services/auth-service/node_modules/.bin/handlebars.ps1
generated
vendored
Normal 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
|
||||
16
services/auth-service/node_modules/.bin/import-local-fixture
generated
vendored
Normal file
16
services/auth-service/node_modules/.bin/import-local-fixture
generated
vendored
Normal 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
|
||||
17
services/auth-service/node_modules/.bin/import-local-fixture.cmd
generated
vendored
Normal file
17
services/auth-service/node_modules/.bin/import-local-fixture.cmd
generated
vendored
Normal 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" %*
|
||||
28
services/auth-service/node_modules/.bin/import-local-fixture.ps1
generated
vendored
Normal file
28
services/auth-service/node_modules/.bin/import-local-fixture.ps1
generated
vendored
Normal 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
16
services/auth-service/node_modules/.bin/jest
generated
vendored
Normal 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
17
services/auth-service/node_modules/.bin/jest.cmd
generated
vendored
Normal 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
28
services/auth-service/node_modules/.bin/jest.ps1
generated
vendored
Normal 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
16
services/auth-service/node_modules/.bin/js-yaml
generated
vendored
Normal 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
17
services/auth-service/node_modules/.bin/js-yaml.cmd
generated
vendored
Normal 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
28
services/auth-service/node_modules/.bin/js-yaml.ps1
generated
vendored
Normal 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
16
services/auth-service/node_modules/.bin/jsesc
generated
vendored
Normal 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
17
services/auth-service/node_modules/.bin/jsesc.cmd
generated
vendored
Normal 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
28
services/auth-service/node_modules/.bin/jsesc.ps1
generated
vendored
Normal 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
16
services/auth-service/node_modules/.bin/json5
generated
vendored
Normal 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
Loading…
Reference in a new issue