chore(data,engine,docs): remove Google Play Games Services sync backend
Deletes the solitaire_gpgs crate and all GPGS references from settings, sync client, profile plugin, CLAUDE.md, and ARCHITECTURE.md. The self-hosted server covers all sync needs without the Android-only backend. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+37
-156
@@ -16,28 +16,25 @@
|
|||||||
5. [Game Engine Architecture](#5-game-engine-architecture)
|
5. [Game Engine Architecture](#5-game-engine-architecture)
|
||||||
6. [Persistence & Sync Architecture](#6-persistence--sync-architecture)
|
6. [Persistence & Sync Architecture](#6-persistence--sync-architecture)
|
||||||
7. [Sync Server Architecture](#7-sync-server-architecture)
|
7. [Sync Server Architecture](#7-sync-server-architecture)
|
||||||
8. [Google Play Games Services (Android Future)](#8-google-play-games-services-android-future)
|
8. [Data Models](#8-data-models)
|
||||||
9. [Data Models](#9-data-models)
|
9. [API Reference](#9-api-reference)
|
||||||
10. [API Reference](#10-api-reference)
|
10. [Merge Strategy](#10-merge-strategy)
|
||||||
11. [Merge Strategy](#11-merge-strategy)
|
11. [Achievement System](#11-achievement-system)
|
||||||
12. [Achievement System](#12-achievement-system)
|
12. [Progression System](#12-progression-system)
|
||||||
13. [Progression System](#13-progression-system)
|
13. [Audio System](#13-audio-system)
|
||||||
14. [Audio System](#14-audio-system)
|
14. [Asset Pipeline](#14-asset-pipeline)
|
||||||
15. [Asset Pipeline](#15-asset-pipeline)
|
15. [Platform Targets](#15-platform-targets)
|
||||||
16. [Platform Targets](#16-platform-targets)
|
16. [Build & Development Guide](#16-build--development-guide)
|
||||||
17. [Build & Development Guide](#17-build--development-guide)
|
17. [Deployment Guide](#17-deployment-guide)
|
||||||
18. [Deployment Guide](#18-deployment-guide)
|
18. [Security Model](#18-security-model)
|
||||||
19. [Security Model](#19-security-model)
|
19. [Testing Strategy](#19-testing-strategy)
|
||||||
20. [Testing Strategy](#20-testing-strategy)
|
20. [Decision Log](#20-decision-log)
|
||||||
21. [Decision Log](#21-decision-log)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Project Overview
|
## 1. Project Overview
|
||||||
|
|
||||||
Solitaire Quest is a cross-platform Klondike Solitaire game written in Rust, targeting macOS, Windows, and Linux desktops (iOS/Android as a stretch goal). It features a full progression system with XP, levels, achievements, daily challenges, and an optional self-hosted sync server so statistics and progress are available across all of a player's devices.
|
Solitaire Quest is a cross-platform Klondike Solitaire game written in Rust, targeting macOS, Windows, and Linux desktops. It features a full progression system with XP, levels, achievements, daily challenges, and an optional self-hosted sync server so statistics and progress are available across all of a player's devices.
|
||||||
|
|
||||||
On Android (stretch goal), sync is enhanced with Google Play Games Services (GPGS) for native achievement popups, leaderboards, and cloud saves — sitting on top of the same underlying sync payload so data stays consistent regardless of which backend was used last.
|
|
||||||
|
|
||||||
### Sync Backend by Platform
|
### Sync Backend by Platform
|
||||||
|
|
||||||
@@ -46,8 +43,6 @@ On Android (stretch goal), sync is enhanced with Google Play Games Services (GPG
|
|||||||
| macOS | Self-hosted server | Full feature set |
|
| macOS | Self-hosted server | Full feature set |
|
||||||
| Windows | Self-hosted server | Full feature set |
|
| Windows | Self-hosted server | Full feature set |
|
||||||
| Linux | Self-hosted server | Full feature set |
|
| Linux | Self-hosted server | Full feature set |
|
||||||
| Android (stretch) | Google Play Games Services | + server as fallback |
|
|
||||||
| iOS (stretch) | Self-hosted server | GPGS not supported on iOS |
|
|
||||||
|
|
||||||
### Design Principles
|
### Design Principles
|
||||||
|
|
||||||
@@ -91,7 +86,6 @@ solitaire_quest/
|
|||||||
├── solitaire_data/ # Persistence, sync client, settings
|
├── solitaire_data/ # Persistence, sync client, settings
|
||||||
├── solitaire_engine/ # Bevy ECS systems, components, plugins
|
├── solitaire_engine/ # Bevy ECS systems, components, plugins
|
||||||
├── solitaire_server/ # Self-hosted sync server (Axum + SQLite)
|
├── solitaire_server/ # Self-hosted sync server (Axum + SQLite)
|
||||||
├── solitaire_gpgs/ # Google Play Games Services bridge (Android only, stub until stretch goal)
|
|
||||||
└── solitaire_app/ # Main binary entry point
|
└── solitaire_app/ # Main binary entry point
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -135,22 +129,7 @@ Owns:
|
|||||||
- `SyncBackend` enum and backend selection
|
- `SyncBackend` enum and backend selection
|
||||||
- Solitaire Server sync client (JWT auth, auto-refresh)
|
- Solitaire Server sync client (JWT auth, auto-refresh)
|
||||||
- OS keychain integration (`keyring`)
|
- OS keychain integration (`keyring`)
|
||||||
- `SyncProvider` trait — implemented by both `SolitaireServerClient` and `GpgsClient` (Android)
|
- `SyncProvider` trait — implemented by `SolitaireServerClient`
|
||||||
|
|
||||||
### `solitaire_gpgs` *(stub — implement when targeting Android)*
|
|
||||||
**Dependencies:** `solitaire_sync`, `jni` (Android only), `solitaire_data` trait impls.
|
|
||||||
|
|
||||||
Android-only crate, compiled only when `target_os = "android"`. Bridges the Google Play Games Services Java SDK via JNI.
|
|
||||||
|
|
||||||
Owns:
|
|
||||||
- `GpgsClient` implementing the `SyncProvider` trait from `solitaire_data`
|
|
||||||
- GPGS Saved Games API calls (load/save cloud save slot)
|
|
||||||
- GPGS Achievements API calls (unlock, reveal, increment)
|
|
||||||
- GPGS Leaderboards API calls (submit score, load scores)
|
|
||||||
- Google Sign-In token management (via JNI into Android SDK)
|
|
||||||
- Conversion between GPGS cloud save blob ↔ `SyncPayload`
|
|
||||||
|
|
||||||
> **Note:** This crate contains only a trait stub and compile-time stub implementations until Android support is actively developed. Do not implement JNI bindings until Phase: Android.
|
|
||||||
|
|
||||||
### `solitaire_engine`
|
### `solitaire_engine`
|
||||||
**Dependencies:** `bevy`, `bevy_kira_audio`, `solitaire_core`, `solitaire_data`.
|
**Dependencies:** `bevy`, `bevy_kira_audio`, `solitaire_core`, `solitaire_data`.
|
||||||
@@ -223,8 +202,7 @@ SyncPlugin::on_startup()
|
|||||||
│ spawns AsyncComputeTask
|
│ spawns AsyncComputeTask
|
||||||
▼
|
▼
|
||||||
solitaire_data::sync_pull() ← dispatches to active SyncProvider
|
solitaire_data::sync_pull() ← dispatches to active SyncProvider
|
||||||
│ SolitaireServerClient (desktop / iOS)
|
│ SolitaireServerClient
|
||||||
│ GpgsClient (Android, future)
|
|
||||||
▼
|
▼
|
||||||
solitaire_sync::merge(local, remote)
|
solitaire_sync::merge(local, remote)
|
||||||
│
|
│
|
||||||
@@ -245,7 +223,7 @@ SyncPlugin::on_exit()
|
|||||||
│ blocking push (acceptable on exit, not on main loop)
|
│ blocking push (acceptable on exit, not on main loop)
|
||||||
▼
|
▼
|
||||||
active SyncProvider::push(local)
|
active SyncProvider::push(local)
|
||||||
│ POST to server — or — GPGS Saved Games PUT (Android)
|
│ POST to server
|
||||||
▼
|
▼
|
||||||
Done
|
Done
|
||||||
```
|
```
|
||||||
@@ -382,7 +360,6 @@ Implementations:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `LocalOnlyProvider` | No-op (default) | All |
|
| `LocalOnlyProvider` | No-op (default) | All |
|
||||||
| `SolitaireServerClient` | Self-hosted server | All |
|
| `SolitaireServerClient` | Self-hosted server | All |
|
||||||
| `GpgsClient` *(future)* | Google Play Games Services | Android only |
|
|
||||||
|
|
||||||
Sync always runs on `bevy::tasks::AsyncComputeTaskPool` — the game thread is never blocked.
|
Sync always runs on `bevy::tasks::AsyncComputeTaskPool` — the game thread is never blocked.
|
||||||
|
|
||||||
@@ -397,9 +374,6 @@ pub enum SyncBackend {
|
|||||||
// JWT access + refresh tokens stored in OS keychain
|
// JWT access + refresh tokens stored in OS keychain
|
||||||
// key: "solitaire_quest_server_{username}"
|
// key: "solitaire_quest_server_{username}"
|
||||||
},
|
},
|
||||||
GooglePlayGames,
|
|
||||||
// No credentials stored locally — auth managed by Google Sign-In SDK via JNI
|
|
||||||
// Android only; selecting this on non-Android falls back to Local silently
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -411,10 +385,6 @@ On exit: `POST /api/sync/push` with payload
|
|||||||
On 401: automatically attempt `POST /api/auth/refresh`, retry once, then surface error to user.
|
On 401: automatically attempt `POST /api/auth/refresh`, retry once, then surface error to user.
|
||||||
Credentials stored in OS keychain via `keyring` — never in plaintext on disk.
|
Credentials stored in OS keychain via `keyring` — never in plaintext on disk.
|
||||||
|
|
||||||
### Google Play Games Sync *(Android — future, see Section 8)*
|
|
||||||
|
|
||||||
Implemented in `solitaire_gpgs` crate. Uses the GPGS Saved Games API with named slot `"solitaire_quest_sync"`. The `GpgsClient` struct implements `SyncProvider` — the `SyncPlugin` treats it identically to `SolitaireServerClient`. The same `solitaire_sync::merge()` function applies regardless of which provider returned the remote data.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Sync Server Architecture
|
## 7. Sync Server Architecture
|
||||||
@@ -501,89 +471,7 @@ This ensures all players worldwide get the same challenge for a given date, rega
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Google Play Games Services (Android Future)
|
## 8. Data Models
|
||||||
|
|
||||||
> **Status: Stub only.** Do not implement JNI bindings until Android is actively targeted. The `solitaire_gpgs` crate exists in the workspace with a trait stub so the compiler enforces the interface contract from day one.
|
|
||||||
|
|
||||||
### Why GPGS on Android
|
|
||||||
|
|
||||||
Google Play Games Services provides first-class Android features that would otherwise require significant backend work:
|
|
||||||
|
|
||||||
| Feature | GPGS Provides | Our Alternative |
|
|
||||||
|---|---|---|
|
|
||||||
| Cloud saves | Saved Games API | Self-hosted server |
|
|
||||||
| Achievements | Native popups + Play profile | In-game toasts only |
|
|
||||||
| Leaderboards | Hosted by Google, visible in Play app | Server leaderboard |
|
|
||||||
| Auth | Google Sign-In, no registration | Username + password |
|
|
||||||
|
|
||||||
On Android, GPGS is the **primary** sync provider. The self-hosted server is the **fallback** if the player is not signed in or has no server configured. Both can be active simultaneously — a win pushes to both, pull merges from whichever responded last.
|
|
||||||
|
|
||||||
### Compatibility Reality
|
|
||||||
|
|
||||||
| Platform | GPGS Support |
|
|
||||||
|---|---|
|
|
||||||
| Android | ✅ Full |
|
|
||||||
| Windows | ✅ GPGS for PC (optional, separate SDK) |
|
|
||||||
| macOS | ❌ Not supported |
|
|
||||||
| Linux | ❌ Not supported |
|
|
||||||
| iOS | ❌ Not supported |
|
|
||||||
|
|
||||||
macOS, Linux, and iOS users always use the self-hosted server. This is why the server is the primary design and GPGS is an enhancement layer.
|
|
||||||
|
|
||||||
### `solitaire_gpgs` Crate Design
|
|
||||||
|
|
||||||
The crate is compiled only on Android (`#[cfg(target_os = "android")]`). On all other platforms the crate exports only the stub.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// solitaire_gpgs/src/lib.rs
|
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
mod android;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
|
||||||
mod stub;
|
|
||||||
|
|
||||||
pub use stub::GpgsClient; // stub on desktop
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
pub use android::GpgsClient; // real impl on Android
|
|
||||||
```
|
|
||||||
|
|
||||||
### JNI Bridge (Android implementation — future)
|
|
||||||
|
|
||||||
The real `GpgsClient` uses the `jni` crate to call into the GPGS Android SDK:
|
|
||||||
|
|
||||||
```
|
|
||||||
Rust GpgsClient
|
|
||||||
│ jni::JNIEnv
|
|
||||||
▼
|
|
||||||
Java: com.google.android.gms.games.PlayGames
|
|
||||||
├── getSnapshotsClient() → Saved Games (sync payload)
|
|
||||||
├── getAchievementsClient() → unlock / reveal
|
|
||||||
└── getLeaderboardsClient() → submit score
|
|
||||||
```
|
|
||||||
|
|
||||||
Steps required when Android work begins:
|
|
||||||
1. Add `cargo-mobile2` to the build toolchain
|
|
||||||
2. Implement `GpgsClient` with `jni` bindings in `solitaire_gpgs/src/android.rs`
|
|
||||||
3. Add `GpgsClient: SyncProvider` impl — pull/push map to Saved Games load/save
|
|
||||||
4. Mirror achievement unlocks: on `AchievementUnlockedEvent`, call GPGS unlock API alongside in-game toast
|
|
||||||
5. Submit scores to GPGS leaderboard on `GameWonEvent`
|
|
||||||
6. Add Google Sign-In button to the Settings screen (Android build only, `#[cfg]` gated)
|
|
||||||
|
|
||||||
### Dual-Sync on Android
|
|
||||||
|
|
||||||
When both GPGS and the self-hosted server are configured, the `SyncPlugin` runs both providers concurrently and merges all three payloads (local + GPGS + server) using the same `solitaire_sync::merge()` function applied twice:
|
|
||||||
|
|
||||||
```
|
|
||||||
local ──────┐
|
|
||||||
├── merge() ──► intermediate ──┐
|
|
||||||
gpgs ────────┘ ├── merge() ──► final
|
|
||||||
server ──────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Data Models
|
|
||||||
|
|
||||||
### Core Game Models (`solitaire_core`)
|
### Core Game Models (`solitaire_core`)
|
||||||
|
|
||||||
@@ -677,14 +565,14 @@ pub struct Settings {
|
|||||||
pub music_volume: f32,
|
pub music_volume: f32,
|
||||||
pub animation_speed: AnimSpeed,
|
pub animation_speed: AnimSpeed,
|
||||||
pub theme: Theme,
|
pub theme: Theme,
|
||||||
pub sync_backend: SyncBackend, // Local | SolitaireServer | GooglePlayGames
|
pub sync_backend: SyncBackend, // Local | SolitaireServer
|
||||||
pub first_run_complete: bool,
|
pub first_run_complete: bool,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. API Reference
|
## 9. API Reference
|
||||||
|
|
||||||
All endpoints are under the base URL configured by the user (e.g., `https://solitaire.example.com`).
|
All endpoints are under the base URL configured by the user (e.g., `https://solitaire.example.com`).
|
||||||
|
|
||||||
@@ -727,9 +615,9 @@ All endpoints are under the base URL configured by the user (e.g., `https://soli
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Merge Strategy
|
## 10. Merge Strategy
|
||||||
|
|
||||||
Used identically by the `SolitaireServerClient`, `GpgsClient`, and server-side handler. Lives in `solitaire_sync` as a pure function with no I/O. Called once per provider when multiple backends are active simultaneously (e.g. GPGS + server on Android).
|
Used by both `SolitaireServerClient` and the server-side handler. Lives in `solitaire_sync` as a pure function with no I/O.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
pub fn merge(local: &SyncPayload, remote: &SyncPayload) -> SyncPayload {
|
pub fn merge(local: &SyncPayload, remote: &SyncPayload) -> SyncPayload {
|
||||||
@@ -769,7 +657,7 @@ pub fn merge(local: &SyncPayload, remote: &SyncPayload) -> SyncPayload {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Achievement System
|
## 11. Achievement System
|
||||||
|
|
||||||
### Definition Structure
|
### Definition Structure
|
||||||
|
|
||||||
@@ -814,13 +702,9 @@ pub struct AchievementDef {
|
|||||||
|
|
||||||
Achievement conditions are evaluated by `AchievementPlugin` on every `GameWonEvent` and `StateChangedEvent`. The plugin calls `solitaire_core::check_achievements()` which returns a `Vec<AchievementDef>` of newly unlocked achievements. The plugin then fires `AchievementUnlockedEvent` for each, which the toast and persistence systems handle independently.
|
Achievement conditions are evaluated by `AchievementPlugin` on every `GameWonEvent` and `StateChangedEvent`. The plugin calls `solitaire_core::check_achievements()` which returns a `Vec<AchievementDef>` of newly unlocked achievements. The plugin then fires `AchievementUnlockedEvent` for each, which the toast and persistence systems handle independently.
|
||||||
|
|
||||||
### GPGS Mirroring *(Android, future)*
|
|
||||||
|
|
||||||
When the `GpgsClient` is active, every `AchievementUnlockedEvent` also triggers a GPGS `achievements.unlock()` call via JNI so the achievement appears in the player's Google Play profile. A static map in `solitaire_gpgs` maps our achievement IDs to GPGS achievement IDs (assigned in the Google Play Console). Mirroring is fire-and-forget — failures are logged but never block the in-game toast.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. Progression System
|
## 12. Progression System
|
||||||
|
|
||||||
### XP Sources
|
### XP Sources
|
||||||
|
|
||||||
@@ -849,7 +733,7 @@ Levels 11+: level = 10 + floor((total_xp - 5000) / 1000)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14. Audio System
|
## 13. Audio System
|
||||||
|
|
||||||
Audio uses `bevy_kira_audio`. All sound files are `.wav`.
|
Audio uses `bevy_kira_audio`. All sound files are `.wav`.
|
||||||
|
|
||||||
@@ -868,7 +752,7 @@ Audio systems listen for Bevy events and never block the game thread.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 15. Asset Pipeline
|
## 14. Asset Pipeline
|
||||||
|
|
||||||
All assets are loaded through Bevy's `AssetServer`. No bytes are hardcoded in source.
|
All assets are loaded through Bevy's `AssetServer`. No bytes are hardcoded in source.
|
||||||
|
|
||||||
@@ -890,21 +774,21 @@ Card backs: `assets/cards/backs/back_0.png` through `back_4.png`. Additional bac
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 16. Platform Targets
|
## 15. Platform Targets
|
||||||
|
|
||||||
| Platform | Status | Primary Sync | Notes |
|
| Platform | Status | Primary Sync | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| macOS | Primary | Self-hosted server | x86_64 + Apple Silicon (universal binary via `cargo-lipo`) |
|
| macOS | Primary | Self-hosted server | x86_64 + Apple Silicon (universal binary via `cargo-lipo`) |
|
||||||
| Windows | Primary | Self-hosted server | x86_64, MSVC toolchain; optional GPGS for PC (future) |
|
| Windows | Primary | Self-hosted server | x86_64, MSVC toolchain |
|
||||||
| Linux | Primary | Self-hosted server | x86_64, tested on Ubuntu 22.04+ and Fedora 39+ |
|
| Linux | Primary | Self-hosted server | x86_64, tested on Ubuntu 22.04+ and Fedora 39+ |
|
||||||
| Android | Stretch | Google Play Games + server | `cargo-mobile2`, touch input, GPGS via JNI |
|
| Android | Stretch | Self-hosted server | `cargo-mobile2`, touch input |
|
||||||
| iOS | Stretch | Self-hosted server | `cargo-mobile2`, touch input; GPGS unavailable on iOS |
|
| iOS | Stretch | Self-hosted server | `cargo-mobile2`, touch input |
|
||||||
|
|
||||||
Minimum Bevy window size enforced: 800×600. Desktop windows are freely resizable; layout recomputes on `WindowResized`.
|
Minimum Bevy window size enforced: 800×600. Desktop windows are freely resizable; layout recomputes on `WindowResized`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 17. Build & Development Guide
|
## 16. Build & Development Guide
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
@@ -965,7 +849,7 @@ Add `--features bevy/dynamic_linking` during development to dramatically reduce
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 18. Deployment Guide
|
## 17. Deployment Guide
|
||||||
|
|
||||||
### Docker Compose (Recommended)
|
### Docker Compose (Recommended)
|
||||||
|
|
||||||
@@ -1010,7 +894,7 @@ Migrations run automatically on startup via `sqlx::migrate!()`.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 19. Security Model
|
## 18. Security Model
|
||||||
|
|
||||||
| Concern | Mitigation |
|
| Concern | Mitigation |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -1026,7 +910,7 @@ Migrations run automatically on startup via `sqlx::migrate!()`.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 20. Testing Strategy
|
## 19. Testing Strategy
|
||||||
|
|
||||||
### Unit Tests (`solitaire_core`)
|
### Unit Tests (`solitaire_core`)
|
||||||
|
|
||||||
@@ -1065,12 +949,10 @@ Using `axum::test` and an in-memory SQLite database:
|
|||||||
- [ ] Achievement toast appears and dismisses
|
- [ ] Achievement toast appears and dismisses
|
||||||
- [ ] Server sync: register, login, push, pull on second machine
|
- [ ] Server sync: register, login, push, pull on second machine
|
||||||
- [ ] Server sync: JWT refresh on 401 works transparently
|
- [ ] Server sync: JWT refresh on 401 works transparently
|
||||||
- [ ] GPGS sync (Android only): sign in, unlock achievement, verify appears in Play Games app
|
|
||||||
- [ ] Dual sync (Android only): GPGS + server both configured, payloads merge correctly
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 21. Decision Log
|
## 20. Decision Log
|
||||||
|
|
||||||
| Decision | Rationale | Date |
|
| Decision | Rationale | Date |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -1082,7 +964,6 @@ Using `axum::test` and an in-memory SQLite database:
|
|||||||
| bcrypt cost 12 | Balances security and registration latency (~300ms on modern hardware); higher than default 10 | 2026-04-19 |
|
| bcrypt cost 12 | Balances security and registration latency (~300ms on modern hardware); higher than default 10 | 2026-04-19 |
|
||||||
| No email required for server accounts | Reduces PII collected; simplifies self-hosted deployments; password reset handled by server admin if needed | 2026-04-19 |
|
| No email required for server accounts | Reduces PII collected; simplifies self-hosted deployments; password reset handled by server admin if needed | 2026-04-19 |
|
||||||
| Self-hosted server as primary sync (not WebDAV) | A proper Rust server gives us auth, leaderboards, and daily challenge seeding for minimal extra effort over WebDAV, and removes a redundant backend | 2026-04-20 |
|
| Self-hosted server as primary sync (not WebDAV) | A proper Rust server gives us auth, leaderboards, and daily challenge seeding for minimal extra effort over WebDAV, and removes a redundant backend | 2026-04-20 |
|
||||||
| `SyncProvider` trait, not `SyncBackend` match arms | Allows adding Google Play Games Services cleanly; `SyncPlugin` stays backend-agnostic and testable | 2026-04-20 |
|
| `SyncProvider` trait, not `SyncBackend` match arms | `SyncPlugin` stays backend-agnostic and testable; new backends can be added without touching the plugin | 2026-04-20 |
|
||||||
| GPGS as Android enhancement, not replacement | GPGS has no macOS/Linux support; the server must remain universal, with GPGS layered on top for Android players | 2026-04-20 |
|
|
||||||
| Dropped WebDAV backend | Redundant once the self-hosted server exists; removing it reduces surface area and simplifies settings UI | 2026-04-20 |
|
| Dropped WebDAV backend | Redundant once the self-hosted server exists; removing it reduces surface area and simplifies settings UI | 2026-04-20 |
|
||||||
| `solitaire_gpgs` crate stubbed from day one | Enforces the `SyncProvider` interface contract at compile time even before Android work begins; avoids architectural rework later | 2026-04-20 |
|
| Dropped GPGS backend | Redundant with the self-hosted server; adds JNI complexity for no user-visible benefit on the target platforms | 2026-04-28 |
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ solitaire_sync/ # Shared API types — NO Bevy, serde/uuid/chrono only
|
|||||||
solitaire_data/ # Persistence + SyncProvider trait + server client
|
solitaire_data/ # Persistence + SyncProvider trait + server client
|
||||||
solitaire_engine/ # Bevy ECS systems, components, plugins
|
solitaire_engine/ # Bevy ECS systems, components, plugins
|
||||||
solitaire_server/ # Axum sync server binary
|
solitaire_server/ # Axum sync server binary
|
||||||
solitaire_gpgs/ # Google Play Games bridge — STUB ONLY until Android phase
|
|
||||||
solitaire_app/ # Thin binary entry point
|
solitaire_app/ # Thin binary entry point
|
||||||
assets/ # Source assets — embedded at compile time via include_bytes!()
|
assets/ # Source assets — embedded at compile time via include_bytes!()
|
||||||
```
|
```
|
||||||
@@ -53,7 +52,6 @@ cargo clippy -p solitaire_core -- -D warnings
|
|||||||
- Passwords and tokens are stored in the OS keychain via the `keyring` crate — never in plaintext files or logs.
|
- Passwords and tokens are stored in the OS keychain via the `keyring` crate — never in plaintext files or logs.
|
||||||
- Sync runs on `AsyncComputeTaskPool` — never block the Bevy main thread.
|
- Sync runs on `AsyncComputeTaskPool` — never block the Bevy main thread.
|
||||||
- All sync backends implement the `SyncProvider` trait. The `SyncPlugin` is backend-agnostic — never `match` on `SyncBackend` inside a Bevy system.
|
- All sync backends implement the `SyncProvider` trait. The `SyncPlugin` is backend-agnostic — never `match` on `SyncBackend` inside a Bevy system.
|
||||||
- `solitaire_gpgs` is a stub until Android work begins. Do not write JNI bindings yet; keep the compile-time stub so the trait contract is enforced from day one.
|
|
||||||
- `cargo clippy --workspace -- -D warnings` must pass clean after every change.
|
- `cargo clippy --workspace -- -D warnings` must pass clean after every change.
|
||||||
- `cargo test --workspace` must pass after every change.
|
- `cargo test --workspace` must pass after every change.
|
||||||
|
|
||||||
|
|||||||
Generated
-9
@@ -6664,15 +6664,6 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "solitaire_gpgs"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"solitaire_data",
|
|
||||||
"solitaire_sync",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solitaire_server"
|
name = "solitaire_server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ members = [
|
|||||||
"solitaire_data",
|
"solitaire_data",
|
||||||
"solitaire_engine",
|
"solitaire_engine",
|
||||||
"solitaire_server",
|
"solitaire_server",
|
||||||
"solitaire_gpgs",
|
|
||||||
"solitaire_app",
|
"solitaire_app",
|
||||||
"solitaire_assetgen",
|
"solitaire_assetgen",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ pub enum Theme {
|
|||||||
|
|
||||||
/// Which sync backend the player has configured.
|
/// Which sync backend the player has configured.
|
||||||
///
|
///
|
||||||
/// JWT tokens for `SolitaireServer` are stored in the OS keychain via
|
/// `Local` keeps all progress on-device. `SolitaireServer` syncs via the
|
||||||
|
/// self-hosted server. JWT tokens are stored in the OS keychain via
|
||||||
/// `solitaire_data::auth_tokens` — **never** in this struct.
|
/// `solitaire_data::auth_tokens` — **never** in this struct.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||||
pub enum SyncBackend {
|
pub enum SyncBackend {
|
||||||
@@ -57,10 +58,7 @@ pub enum SyncBackend {
|
|||||||
username: String,
|
username: String,
|
||||||
// JWT tokens are stored in the OS keychain — not here.
|
// JWT tokens are stored in the OS keychain — not here.
|
||||||
},
|
},
|
||||||
/// Google Play Games Services (Android only). Selecting this on non-Android
|
|
||||||
/// platforms silently falls back to `Local` at runtime.
|
|
||||||
#[serde(rename = "google_play_games")]
|
|
||||||
GooglePlayGames,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persistent user settings.
|
/// Persistent user settings.
|
||||||
|
|||||||
@@ -412,19 +412,12 @@ async fn extract_push_body(resp: reqwest::Response) -> Result<SyncResponse, Sync
|
|||||||
/// This is the **one** place in the codebase that matches on [`SyncBackend`]
|
/// This is the **one** place in the codebase that matches on [`SyncBackend`]
|
||||||
/// variants. All other code receives a `Box<dyn SyncProvider + Send + Sync>`
|
/// variants. All other code receives a `Box<dyn SyncProvider + Send + Sync>`
|
||||||
/// and remains backend-agnostic.
|
/// and remains backend-agnostic.
|
||||||
///
|
|
||||||
/// `GooglePlayGames` is Android-only; on desktop it silently falls back to
|
|
||||||
/// [`LocalOnlyProvider`].
|
|
||||||
pub fn provider_for_backend(backend: &SyncBackend) -> Box<dyn SyncProvider + Send + Sync> {
|
pub fn provider_for_backend(backend: &SyncBackend) -> Box<dyn SyncProvider + Send + Sync> {
|
||||||
match backend {
|
match backend {
|
||||||
SyncBackend::Local => Box::new(LocalOnlyProvider),
|
SyncBackend::Local => Box::new(LocalOnlyProvider),
|
||||||
SyncBackend::SolitaireServer { url, username } => {
|
SyncBackend::SolitaireServer { url, username } => {
|
||||||
Box::new(SolitaireServerClient::new(url.clone(), username.clone()))
|
Box::new(SolitaireServerClient::new(url.clone(), username.clone()))
|
||||||
}
|
}
|
||||||
SyncBackend::GooglePlayGames => {
|
|
||||||
// GPGS is Android-only; fall back to no-op on desktop.
|
|
||||||
Box::new(LocalOnlyProvider)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,12 +463,6 @@ mod tests {
|
|||||||
assert_eq!(provider.backend_name(), "local");
|
assert_eq!(provider.backend_name(), "local");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn factory_gpgs_falls_back_to_local() {
|
|
||||||
let provider = provider_for_backend(&SyncBackend::GooglePlayGames);
|
|
||||||
assert_eq!(provider.backend_name(), "local");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn factory_server_returns_server_client() {
|
fn factory_server_returns_server_client() {
|
||||||
let provider = provider_for_backend(&SyncBackend::SolitaireServer {
|
let provider = provider_for_backend(&SyncBackend::SolitaireServer {
|
||||||
|
|||||||
@@ -260,7 +260,6 @@ fn sync_info(backend: &SyncBackend) -> (&'static str, String) {
|
|||||||
SyncBackend::SolitaireServer { username, .. } => {
|
SyncBackend::SolitaireServer { username, .. } => {
|
||||||
("Solitaire Server", username.clone())
|
("Solitaire Server", username.clone())
|
||||||
}
|
}
|
||||||
SyncBackend::GooglePlayGames => ("Google Play Games", "—".to_string()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "solitaire_gpgs"
|
|
||||||
version.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
solitaire_data = { workspace = true }
|
|
||||||
solitaire_sync = { workspace = true }
|
|
||||||
async-trait = { workspace = true }
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
// TODO (Phase: Android) — implement JNI bindings here.
|
|
||||||
//
|
|
||||||
// Steps:
|
|
||||||
// 1. Add `jni` dependency under [target.'cfg(target_os = "android")'.dependencies]
|
|
||||||
// 2. Implement GpgsClient using cargo-mobile2 JNI bridge
|
|
||||||
// 3. pull(): call PlayGames.getSnapshotsClient().open("solitaire_quest_sync")
|
|
||||||
// -> deserialize JSON blob into SyncPayload
|
|
||||||
// 4. push(): serialize SyncPayload to JSON -> write to Saved Game slot
|
|
||||||
// 5. mirror_achievement(id): call PlayGames.getAchievementsClient().unlock(map_id(id))
|
|
||||||
// 6. Maintain a static ID mapping: our &str IDs -> GPGS achievement IDs (from Play Console)
|
|
||||||
// 7. On GameWonEvent, submit score to GPGS leaderboard
|
|
||||||
// 8. Add Google Sign-In button to Settings screen (Android build only, #[cfg] gated)
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#[cfg(target_os = "android")]
|
|
||||||
mod android;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
|
||||||
mod stub;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
|
||||||
pub use stub::GpgsClient;
|
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
pub use android::GpgsClient;
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
use solitaire_data::{SyncError, SyncProvider};
|
|
||||||
use solitaire_sync::{ChallengeGoal, LeaderboardEntry, SyncPayload, SyncResponse};
|
|
||||||
|
|
||||||
/// Google Play Games Services sync client — desktop/iOS stub.
|
|
||||||
///
|
|
||||||
/// Always returns [`SyncError::UnsupportedPlatform`]. The real JNI implementation
|
|
||||||
/// lives in `android.rs` and is compiled only on Android (Phase: Android).
|
|
||||||
pub struct GpgsClient;
|
|
||||||
|
|
||||||
impl GpgsClient {
|
|
||||||
/// Creates a new `GpgsClient` stub. No-op on non-Android platforms.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for GpgsClient {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl SyncProvider for GpgsClient {
|
|
||||||
async fn pull(&self) -> Result<SyncPayload, SyncError> {
|
|
||||||
Err(SyncError::UnsupportedPlatform)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn push(&self, _payload: &SyncPayload) -> Result<SyncResponse, SyncError> {
|
|
||||||
Err(SyncError::UnsupportedPlatform)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn backend_name(&self) -> &'static str {
|
|
||||||
"Google Play Games (unavailable on this platform)"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_authenticated(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No-op stub — returns UnsupportedPlatform on non-Android targets.
|
|
||||||
async fn mirror_achievement(&self, _id: &str) -> Result<(), SyncError> {
|
|
||||||
Err(SyncError::UnsupportedPlatform)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No-op stub — returns UnsupportedPlatform on non-Android targets.
|
|
||||||
async fn fetch_leaderboard(&self) -> Result<Vec<LeaderboardEntry>, SyncError> {
|
|
||||||
Err(SyncError::UnsupportedPlatform)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No-op stub — returns UnsupportedPlatform on non-Android targets.
|
|
||||||
async fn fetch_daily_challenge(&self) -> Result<Option<ChallengeGoal>, SyncError> {
|
|
||||||
Err(SyncError::UnsupportedPlatform)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No-op stub — returns UnsupportedPlatform on non-Android targets.
|
|
||||||
async fn opt_in_leaderboard(&self, _display_name: &str) -> Result<(), SyncError> {
|
|
||||||
Err(SyncError::UnsupportedPlatform)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No-op stub — returns UnsupportedPlatform on non-Android targets.
|
|
||||||
async fn opt_out_leaderboard(&self) -> Result<(), SyncError> {
|
|
||||||
Err(SyncError::UnsupportedPlatform)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No-op stub — returns UnsupportedPlatform on non-Android targets.
|
|
||||||
async fn delete_account(&self) -> Result<(), SyncError> {
|
|
||||||
Err(SyncError::UnsupportedPlatform)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user