835a48fe9d
Build and Deploy / build-and-push (push) Failing after 58s
Adds a new `solitaire_web` crate that compiles the full `solitaire_engine` to `wasm32-unknown-unknown` and renders to a `<canvas id="bevy-canvas">` element in `play.html` — the same ECS code path as desktop and Android. Changes to enable the WASM target: - .cargo/config.toml: add wasm32-unknown-unknown rustflags for getrandom - Workspace Cargo.toml: add solitaire_web member - solitaire_data/Cargo.toml: gate tokio/reqwest/dirs/keyring to non-wasm - solitaire_data/src: add wasm32 branch to data_dir() (returns None); cfg-gate sync_client network types, auth_tokens, matomo_client - solitaire_engine/Cargo.toml: gate tokio/reqwest/kira/arboard/dirs/zip to non-wasm (mio/cpal/arboard don't compile for wasm32-unknown-unknown) - solitaire_engine/src/lib.rs: cfg-gate module declarations and re-exports for analytics, audio, sync, sync_setup, avatar, leaderboard plugins - solitaire_engine/src/core_game_plugin.rs: cfg-gate plugin registrations that require TokioRuntime (audio, sync, analytics, leaderboard, avatar) - solitaire_engine/src/resources.rs: cfg-gate TokioRuntimeResource - solitaire_engine/src/game_plugin.rs: cfg-gate std::fs::remove_file (x10) - solitaire_engine/src/theme/mod.rs: cfg-gate importer module (uses dirs+zip) - solitaire_engine/src/settings_plugin.rs: cfg-gate theme ZIP import UI - solitaire_engine/src/assets/sources.rs: cfg-gate FileAssetReader/user_theme_dir - solitaire_engine/src/auto_complete_plugin.rs: cfg-gate audio system - solitaire_engine/src/daily_challenge_plugin.rs: cfg-gate server fetch - solitaire_engine/src/hud_plugin.rs: cfg-gate AvatarResource import - solitaire_engine/src/profile_plugin.rs: cfg-gate AvatarResource import - solitaire_server/web/play.html: minimal HTML canvas shell - solitaire_web/: new crate (Cargo.toml + src/lib.rs) - build_wasm.sh: add Bevy WASM build step (cargo + wasm-bindgen + wasm-opt) All tests pass; clippy --workspace -- -D warnings clean; native build (solitaire_engine, solitaire_app) unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
180 lines
7.0 KiB
Rust
180 lines
7.0 KiB
Rust
use async_trait::async_trait;
|
|
use solitaire_sync::{ChallengeGoal, LeaderboardEntry, SyncPayload, SyncResponse};
|
|
use thiserror::Error;
|
|
|
|
/// All errors that can arise during sync operations.
|
|
#[derive(Debug, Error)]
|
|
pub enum SyncError {
|
|
#[error("unsupported platform for this sync backend")]
|
|
UnsupportedPlatform,
|
|
#[error("network error: {0}")]
|
|
Network(String),
|
|
#[error("authentication error: {0}")]
|
|
Auth(String),
|
|
#[error("serialization error: {0}")]
|
|
Serialization(String),
|
|
}
|
|
|
|
/// Every sync backend implements this trait. The SyncPlugin only calls these
|
|
/// methods — it never matches on a backend enum variant.
|
|
#[async_trait]
|
|
pub trait SyncProvider: Send + Sync {
|
|
/// Fetch the remote sync payload. Returns the latest server state for merging.
|
|
async fn pull(&self) -> Result<SyncPayload, SyncError>;
|
|
/// Push the local payload to the backend. Returns the merged server response.
|
|
async fn push(&self, payload: &SyncPayload) -> Result<SyncResponse, SyncError>;
|
|
/// Human-readable name of this backend, used in settings UI and logs.
|
|
fn backend_name(&self) -> &'static str;
|
|
/// Returns true if the user is currently authenticated with this backend.
|
|
fn is_authenticated(&self) -> bool;
|
|
/// Fetch the global leaderboard from this backend. Returns an empty list
|
|
/// for backends that do not support leaderboards (e.g. `LocalOnlyProvider`).
|
|
async fn fetch_leaderboard(&self) -> Result<Vec<LeaderboardEntry>, SyncError> {
|
|
Ok(vec![])
|
|
}
|
|
/// Fetch today's daily challenge from the server. Returns `None` for
|
|
/// backends that don't support it, or on any non-fatal network failure.
|
|
async fn fetch_daily_challenge(&self) -> Result<Option<ChallengeGoal>, SyncError> {
|
|
Ok(None)
|
|
}
|
|
/// Opt the authenticated player into the leaderboard with the given
|
|
/// display name. No-op for backends that don't support leaderboards.
|
|
async fn opt_in_leaderboard(&self, _display_name: &str) -> Result<(), SyncError> {
|
|
Ok(())
|
|
}
|
|
/// Remove the authenticated player from the leaderboard.
|
|
/// No-op for backends that don't support leaderboards.
|
|
async fn opt_out_leaderboard(&self) -> Result<(), SyncError> {
|
|
Ok(())
|
|
}
|
|
/// Permanently delete the authenticated player's account and all server
|
|
/// data. No-op for backends that don't support account management.
|
|
async fn delete_account(&self) -> Result<(), SyncError> {
|
|
Ok(())
|
|
}
|
|
/// Upload a winning replay to the backend. On success, returns the
|
|
/// shareable web URL the player can copy to their clipboard
|
|
/// (`<server>/replays/<id>`). Default returns `UnsupportedPlatform`
|
|
/// so backends without a server (e.g. `LocalOnlyProvider`) are
|
|
/// silently no-op'd by the engine's push-on-win system, matching
|
|
/// the same pattern `pull` / `push` follow.
|
|
async fn push_replay(&self, _replay: &crate::replay::Replay) -> Result<String, SyncError> {
|
|
Err(SyncError::UnsupportedPlatform)
|
|
}
|
|
}
|
|
|
|
/// Blanket impl so `Box<dyn SyncProvider + Send + Sync>` (returned by
|
|
/// `provider_for_backend`) can be passed directly to `SyncPlugin::new`.
|
|
#[async_trait]
|
|
impl SyncProvider for Box<dyn SyncProvider + Send + Sync> {
|
|
async fn pull(&self) -> Result<SyncPayload, SyncError> {
|
|
(**self).pull().await
|
|
}
|
|
async fn push(&self, payload: &SyncPayload) -> Result<SyncResponse, SyncError> {
|
|
(**self).push(payload).await
|
|
}
|
|
fn backend_name(&self) -> &'static str {
|
|
(**self).backend_name()
|
|
}
|
|
fn is_authenticated(&self) -> bool {
|
|
(**self).is_authenticated()
|
|
}
|
|
async fn fetch_leaderboard(&self) -> Result<Vec<LeaderboardEntry>, SyncError> {
|
|
(**self).fetch_leaderboard().await
|
|
}
|
|
async fn fetch_daily_challenge(&self) -> Result<Option<ChallengeGoal>, SyncError> {
|
|
(**self).fetch_daily_challenge().await
|
|
}
|
|
async fn opt_in_leaderboard(&self, display_name: &str) -> Result<(), SyncError> {
|
|
(**self).opt_in_leaderboard(display_name).await
|
|
}
|
|
async fn opt_out_leaderboard(&self) -> Result<(), SyncError> {
|
|
(**self).opt_out_leaderboard().await
|
|
}
|
|
async fn delete_account(&self) -> Result<(), SyncError> {
|
|
(**self).delete_account().await
|
|
}
|
|
async fn push_replay(&self, replay: &crate::replay::Replay) -> Result<String, SyncError> {
|
|
(**self).push_replay(replay).await
|
|
}
|
|
}
|
|
|
|
pub mod stats;
|
|
pub use stats::{StatsExt, StatsSnapshot};
|
|
|
|
pub mod storage;
|
|
pub use storage::{
|
|
TimeAttackSession, cleanup_orphaned_tmp_files, delete_game_state_at,
|
|
delete_time_attack_session_at, game_state_file_path, load_game_state_from, load_stats,
|
|
load_stats_from, load_time_attack_session_from, load_time_attack_session_from_at,
|
|
save_game_state_to, save_stats, save_stats_to, save_time_attack_session_to, stats_file_path,
|
|
time_attack_session_path, time_attack_session_with_now,
|
|
};
|
|
|
|
pub mod achievements;
|
|
pub use achievements::{
|
|
AchievementRecord, achievements_file_path, load_achievements_from, save_achievements_to,
|
|
};
|
|
|
|
pub mod progress;
|
|
pub use progress::{
|
|
PlayerProgress, daily_seed_for, level_for_xp, load_progress_from, progress_file_path,
|
|
save_progress_to, xp_for_win,
|
|
};
|
|
|
|
pub mod weekly;
|
|
pub use weekly::{
|
|
WEEKLY_GOAL_XP, WEEKLY_GOALS, WeeklyGoalContext, WeeklyGoalDef, WeeklyGoalKind,
|
|
current_iso_week_key, weekly_goal_by_id,
|
|
};
|
|
|
|
pub mod challenge;
|
|
pub use challenge::{CHALLENGE_SEEDS, challenge_count, challenge_seed_for};
|
|
|
|
pub mod difficulty_seeds;
|
|
pub use difficulty_seeds::{DifficultySeeds, seeds_for};
|
|
|
|
pub mod settings;
|
|
pub use settings::{
|
|
AnimSpeed, REPLAY_MOVE_INTERVAL_MAX_SECS, REPLAY_MOVE_INTERVAL_MIN_SECS,
|
|
REPLAY_MOVE_INTERVAL_STEP_SECS, SOLVER_DEAL_RETRY_CAP, Settings, SyncBackend,
|
|
TIME_BONUS_MULTIPLIER_MAX, TIME_BONUS_MULTIPLIER_MIN, TIME_BONUS_MULTIPLIER_STEP,
|
|
TOOLTIP_DELAY_MAX_SECS, TOOLTIP_DELAY_MIN_SECS, TOOLTIP_DELAY_STEP_SECS, Theme, WindowGeometry,
|
|
load_settings_from, save_settings_to, settings_file_path,
|
|
};
|
|
|
|
#[cfg(target_os = "android")]
|
|
mod android_keystore;
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub mod auth_tokens;
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub use auth_tokens::{
|
|
TokenError, delete_tokens, load_access_token, load_refresh_token, store_tokens,
|
|
};
|
|
|
|
pub mod sync_client;
|
|
pub use sync_client::LocalOnlyProvider;
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub use sync_client::{SolitaireServerClient, provider_for_backend};
|
|
|
|
pub mod replay;
|
|
pub use replay::{
|
|
REPLAY_HISTORY_CAP, REPLAY_HISTORY_SCHEMA_VERSION, REPLAY_SCHEMA_VERSION, Replay,
|
|
ReplayHistory, ReplayMove, append_replay_to_history, load_replay_history_from,
|
|
migrate_legacy_latest_replay, replay_history_path, save_replay_history_to,
|
|
};
|
|
#[allow(deprecated)]
|
|
pub use replay::{latest_replay_path, load_latest_replay_from, save_latest_replay_to};
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub mod matomo_client;
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub use matomo_client::MatomoClient;
|
|
|
|
pub mod platform;
|
|
pub use platform::data_dir;
|
|
|
|
/// Application data subdirectory name, shared by all persistence modules.
|
|
pub(crate) const APP_DIR_NAME: &str = "ferrous_solitaire";
|