- Add avatar_plugin: AvatarPlugin, AvatarResource, AvatarFetchEvent
- After AvatarFetchEvent fires, spawns an async reqwest download task
- On completion, decodes image bytes via image::load_from_memory →
Image::from_dynamic and inserts into Assets<Image>
- Expand auth task to also call fetch_me_with_token immediately after
login/register so avatar_url is available without a second round-trip
- poll_auth_task fires AvatarFetchEvent when avatar_url is Some, building
the full URL from base_url + relative avatar path
- Profile modal shows 48px circular avatar ImageNode when AvatarResource
is populated, or an initials disc (first letter of username) as fallback
- Add image = "0.25" and reqwest to solitaire_engine deps
- Add fetch_me_with_token helper to SolitaireServerClient for use when
the access token hasn't been persisted to keychain yet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace all display-name occurrences across web pages, Rust source,
docs, and Cargo metadata. Update localStorage token key from sq_token
to fs_token. Tagline "Klondike Solitaire" retained as genre descriptor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds SyncSetupPlugin: a three-field (URL / Username / Password) modal
that handles both login and register flows via an async task on
AsyncComputeTaskPool wrapped in a Tokio single-thread runtime (same
pattern as the existing sync push/pull). On success, tokens are stored
to the OS keychain / Android Keystore and SyncProviderResource is
hot-swapped so subsequent pull/push use the new credentials immediately.
Settings sync section now shows Connect (when Local) or Sync Now +
Disconnect + username label (when SolitaireServer). SyncAuthResultEvent
stub registered for future re-auth prompt wiring.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`AssetPlugin::file_path = "../assets"` was set unconditionally to
make `cargo run -p solitaire_app` find the workspace-root assets
directory from inside `solitaire_app/`. On Android cargo-apk packages
the same directory into the APK at `assets/`, and Bevy's
AndroidAssetReader is already rooted there — prepending `../` walked
the reader out of the APK assets root and every load failed silently.
The cascade: CardImageSet handles were inserted but pointed at
non-existent paths, so `card_sprite` saw `Some(set)` but the textures
never resolved. The face-down branch then rendered with `Color::WHITE`
over a missing texture — which on hardware showed as the
`card_back_colour(0)` solid-red brick fallback that's *supposed* to
only fire under MinimalPlugins in tests.
Gates the `file_path` override behind
`#[cfg(not(target_os = "android"))]` so Android picks up the default
empty path. Closes P0 #3 of docs/android/PLAYABILITY_TODO.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds SafeAreaInsets resource + SafeAreaInsetsPlugin that report the
OS-reserved regions (status bar, gesture/nav bar, display cutout)
around the playable surface. Desktop reports all zeros; Android
queries WindowInsets.getInsets(systemBars()) via JNI on the decor
view, polling for up to 120 frames since getRootWindowInsets()
returns null until the view is laid out.
UI that should respect the top inset carries a SafeAreaAnchoredTop
{ base_top } marker. A change-detection system re-applies
`base_top + insets.top` whenever the resource changes, so late
inset arrival (frame 1-3 on Android) and future orientation
changes flow through without re-spawning entities.
Wires the three top-anchored HUD spawn sites — hud_band, hud
column, action button row — to the new pattern. Spawn systems
take Option<Res<SafeAreaInsets>> so HudPlugin still works
standalone in unit tests (mirrors the existing FontResource
pattern).
Closes P0 #1 of docs/android/PLAYABILITY_TODO.md. Resolves the
status-bar/HUD collision visible in the v0.22.3 hardware
screenshot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These three bevy::window types are only referenced by
apply_smart_default_window_size, which is already cfg(not(android)).
The unconditional import triggered -D unused-imports on the Android
cross-compile. Split into a separate cfg-gated use statement.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both symbols are desktop-only: the variable feeds apply_smart_default_
window_size which is only registered inside a cfg(not(android)) block.
Without the matching cfg gate on the declaration / definition, the
Android cross-compile emits unused-variable and dead-code errors
(-D warnings turns them into hard failures).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds DifficultyLevel (Easy/Medium/Hard/Expert/Grandmaster/Random) to
solitaire_core::game_state alongside GameMode::Difficulty(DifficultyLevel).
Five seed catalogs (40 seeds each) are pre-verified by the new
gen_difficulty_seeds binary using tiered solver budgets (1K–200K moves).
DifficultyPlugin resolves StartDifficultyRequestEvent → catalog seed →
NewGameRequestEvent; Random uses a system-time seed and bypasses the
winnable-only filter. The home overlay gets an expandable Difficulty section
between Draw Mode and the mode grid; last-played tier persists in Settings.
Difficulty wins pool into Classic stats. 5 unit tests in difficulty_plugin.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a numeric-input modal (PlayBySeedPlugin) that lets the player type
a decimal seed and receive an instant solver-verified verdict before the
hand is dealt. A new HomeMode::PlayBySeed card surfaces it in the home
overlay, matched by the StartPlayBySeedRequestEvent carrier.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three changes to get the APK past the NativeActivity launch crash:
1. Export `android_main` — NativeActivity dlopen-s libsolitaire_app.so
and calls `android_main` as its entry point. Without the symbol the
app crashed immediately with UnsatisfiedLinkError. The function sets
bevy::android::ANDROID_APP (required by WinitPlugin) then delegates
to the existing `run()`.
2. Gate `resize_constraints` to non-Android — on Android max_width and
max_height default to 0.0; Bevy's clamp panicked with min=800 > max=0.
3. Gate `apply_smart_default_window_size` to non-Android — the system
calls `.clamp(800.0, logical_w)` which panics when the window surface
reports zero dimensions during early Android lifecycle events. Window
sizing is OS-controlled on Android so the system is irrelevant there.
Verified: app boots on x86_64 Android 14 emulator (Pixel_7 AVD,
SwiftShader Vulkan), runs for 2+ minutes without crashing. Desktop
build: clippy clean, 1282 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`NonSend<WinitWindows>` failed system-param validation on the
first few frames before `WinitWindows` was populated, panicking
the Update system before any logic could run. Bevy 0.18's
stricter validation panics rather than skips when a non-send
resource is absent, with an error message spelling out the fix:
*"wrap the parameter in `Option<T>` and handle `None` when it
happens."*
Wraps `winit_windows` as `Option<NonSend<WinitWindows>>` and
early-returns on `None`, mirroring the same lifecycle handling
already applied to `winit_windows.get_window(primary_entity)` —
both fail in the same window of frames before winit's `Resumed`
event fires.
Repro from the user's `cargo run` log:
```
thread 'Compute Task Pool (2)' panicked at .../bevy_ecs-0.18.1/src/error/handler.rs:125:1:
Encountered an error in system ...: Parameter ... failed validation:
Non-send resource does not exist
```
Workspace clippy + cargo test --workspace clean, 1185 passing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes Resume-prompt Option A (the post-v0.21.0 first option).
Half-day desktop work, no cert dependency.
Three deliverables:
1. **SVG-authored icon** (`solitaire_engine/src/assets/icon_svg.rs`)
— square Terminal mark: `#151515` background, brick-red
`#a54242` 1 px border, brick-red ▌ cursor block centered, "RS"
monogram in `#d0d0d0` foreground gray beneath. Same shape that
already lives on the splash boot screen and card-back monogram,
reused as the project's signature visual mark. Authored in a
64-unit logical box so it scales cleanly at every rasterisation
target.
2. **9-size PNG hierarchy** (16, 24, 32, 48, 64, 128, 256, 512, 1024
px) regenerated by `solitaire_engine/examples/icon_generator.rs`
into `assets/icon/icon_<size>.png`. Sizes cover Linux hicolor
(16, 24, 32, 48, 64, 128, 256, 512), Windows .ico targets (16,
32, 48, 256), and macOS .icns targets (16, 32, 64, 128, 256,
512, 1024). The runtime path uses just the 256 px slot; the
smaller sizes are pre-rendered for downstream packaging.
3. **Runtime `Window::icon` wiring** (`solitaire_app/src/lib.rs`).
Bevy 0.18 has no `Window::icon` field — the icon is set through
the underlying `winit::window::Window` via the `WinitWindows`
resource. `set_window_icon` runs each Update tick, retries
silently until `WinitWindows` is populated (typically frame 1
or 2), decodes the embedded 256 px PNG via `tiny_skia`, builds
a `winit::window::Icon`, and self-disables via `Local<bool>`.
Same one-shot pattern as `apply_smart_default_window_size`.
Desktop-only — Android draws its launcher icon from the APK
manifest, so the system is target-gated to
`cfg(not(target_os = "android"))`.
Dep changes (CLAUDE.md §8 user-confirmed):
- `winit = "0.30"` promoted from a transitive Bevy dep to a direct
dep on `solitaire_app` so `winit::window::Icon` is in scope —
bevy_winit 0.18 doesn't re-export it. Version pinned to whatever
Bevy uses; if Bevy bumps winit, this line bumps in lockstep.
- `tiny-skia` added as a direct dep on `solitaire_app` for PNG →
RGBA decode. Already in workspace deps for `solitaire_engine`;
no version drift risk.
- Both new deps target-gated to non-Android only.
Test infrastructure: `solitaire_engine/tests/icon_svg_pin.rs`
hashes the rasterised RGBA bytes at all 9 sizes via FNV-1a (same
shape as `card_face_svg_pin`). Bootstrap pattern (empty EXPECTED
→ panic with hashes formatted as Rust source → paste back in)
handles future intentional builder edits cleanly.
Workspace clippy + cargo test --workspace clean. 1185 passing
(+1 from v0.21.0's 1184 baseline — the icon pin's
`rasterised_icon_bytes_match_pinned_hashes`).
Out of scope for this commit: `.icns` / `.ico` bundling for
macOS / Windows app packaging. Both are packaging-time concerns
(set via bundle manifests, not runtime calls) and would need new
deps (`ico` and `icns` crates) — separate followup if/when the
project ships as a packaged macOS / Windows app rather than just
`cargo run`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wires the workspace through `cargo apk build`. After this commit
`cargo apk build -p solitaire_app --target x86_64-linux-android`
produces a debug-signed APK at `target/debug/apk/solitaire-quest.apk`
containing all assets and `lib/x86_64/libsolitaire_app.so` — runnable
on the AVD or a physical x86_64 device.
The five gating points discovered by iterating compile cycles:
1. solitaire_app split into bin + lib. cargo-apk needs a `cdylib`
to bundle as `libmain.so`; pure-bin crates panic with
"Bin is not compatible with Cdylib". `src/lib.rs` carries the
ECS bootstrap as `pub fn run`; `src/main.rs` is a 3-line shim
that delegates for the desktop `cargo run` path.
2. `[package.metadata.android]` pins target SDK 34 / min SDK 26
so cargo-apk doesn't probe for whatever default it ships
(which on this machine was an uninstalled API 30). `assets =
"../assets"` lets the same asset directory feed both desktop
and APK.
3. Workspace `bevy` features add `android-native-activity` (the
Bevy-side glue that pairs with cargo-apk's NativeActivity
wrapper). The feature is target-gated inside bevy_internal so
desktop builds compile it out.
4. `arboard` (clipboard, used by Stats's "Copy share link") has
no Android backend — `cargo apk build` fails with E0433 on
`platform::Clipboard` if unconditional. Target-gated to
`cfg(not(target_os = "android"))`; the system surfaces an
informational toast on Android until JNI ClipboardManager is
wired in the Phase-Android round.
5. `keyring` + `keyring-core` cannot compile for android — the
transitive `rpassword` uses `libc::__errno_location` which
bionic doesn't expose. Both crates target-gated; `auth_tokens`
ships a stub on Android that returns `KeychainUnavailable` for
every call, matching how callers already handle a Linux box
without Secret Service.
Cosmetic post-pass panic: cargo-apk panics AFTER the APK is signed
when it tries to also wrap the bin target. The APK on disk is
unaffected. Working around this with `cargo apk build --lib` is
the next small step.
What's verified:
- Desktop `cargo build`, `cargo clippy --workspace --all-targets`,
and `cargo test --workspace` all clean.
- `cargo apk build -p solitaire_app --target x86_64-linux-android`
produces 54 MB debug APK with libsolitaire_app.so + assets.
What's NOT yet verified:
- Whether the APK actually launches on the AVD / a phone (next
step: `adb install` + `adb logcat` against the bevy_test AVD).
- Whether `dirs::data_dir()` on Android returns a usable path
(sync / persistence will surface this if not).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>