diff --git a/Cargo.toml b/Cargo.toml index 1cbb01f..b317d7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ rust-version = "1.95" serde = { version = "1", features = ["derive"] } serde_json = "1" uuid = { version = "1", features = ["v4", "serde"] } -chrono = { version = "0.4", features = ["serde"] } +chrono = { version = "0.4", features = ["serde", "wasmbind"] } thiserror = "2" rand = "0.9" async-trait = "0.1" diff --git a/solitaire_data/src/storage.rs b/solitaire_data/src/storage.rs index dba6644..0dff645 100644 --- a/solitaire_data/src/storage.rs +++ b/solitaire_data/src/storage.rs @@ -6,7 +6,7 @@ use std::fs; use std::io; use std::path::{Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH}; +use chrono::Utc; use serde::{Deserialize, Serialize}; use solitaire_core::game_state::{GAME_STATE_SCHEMA_VERSION, GameState}; @@ -234,9 +234,7 @@ pub fn load_time_attack_session_from_at( /// See [`load_time_attack_session_from_at`] for the rules under which /// the call returns `None` (missing file, corrupt JSON, expired window). pub fn load_time_attack_session_from(path: &Path) -> Option { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_or(0, |d| d.as_secs()); + let now = Utc::now().timestamp().max(0) as u64; load_time_attack_session_from_at(path, now) } @@ -254,9 +252,7 @@ pub fn delete_time_attack_session_at(path: &Path) -> io::Result<()> { /// current wall-clock time. Equivalent to constructing the struct /// manually and setting `saved_at_unix_secs` to `SystemTime::now()`. pub fn time_attack_session_with_now(remaining_secs: f32, wins: u32) -> TimeAttackSession { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_or(0, |d| d.as_secs()); + let now = Utc::now().timestamp().max(0) as u64; TimeAttackSession { remaining_secs, wins, diff --git a/solitaire_engine/src/assets/user_dir.rs b/solitaire_engine/src/assets/user_dir.rs index e5f81d7..020116a 100644 --- a/solitaire_engine/src/assets/user_dir.rs +++ b/solitaire_engine/src/assets/user_dir.rs @@ -82,13 +82,23 @@ fn user_theme_dir_for(data_dir: PathBuf) -> PathBuf { /// the panic message names the supported workaround. fn detected_platform_data_dir() -> PathBuf { solitaire_data::data_dir().unwrap_or_else(|| { - panic!( - "user_theme_dir(): platform data directory is unavailable. \ - On Linux check $XDG_DATA_HOME or $HOME; on macOS / Windows \ - the OS reported no Application Support / AppData path. \ - As a workaround call solitaire_engine::assets::user_dir::\ - set_user_theme_dir() before App::run()." - ) + // On wasm32, data_dir() always returns None — there is no filesystem. + // User themes are not supported in the browser build; return an empty + // path so callers produce a benign empty dir rather than panicking. + #[cfg(target_arch = "wasm32")] + { + PathBuf::new() + } + #[cfg(not(target_arch = "wasm32"))] + { + panic!( + "user_theme_dir(): platform data directory is unavailable. \ + On Linux check $XDG_DATA_HOME or $HOME; on macOS / Windows \ + the OS reported no Application Support / AppData path. \ + As a workaround call solitaire_engine::assets::user_dir::\ + set_user_theme_dir() before App::run()." + ) + } }) } diff --git a/solitaire_engine/src/difficulty_plugin.rs b/solitaire_engine/src/difficulty_plugin.rs index fa94ca7..fa21b57 100644 --- a/solitaire_engine/src/difficulty_plugin.rs +++ b/solitaire_engine/src/difficulty_plugin.rs @@ -14,7 +14,7 @@ //! because the starting position is effectively random (player-chosen timing //! determines which seed in the 40-entry catalog they start at). -use std::time::{SystemTime, UNIX_EPOCH}; +use chrono::Utc; use bevy::prelude::*; use solitaire_core::game_state::{DifficultyLevel, GameMode}; @@ -104,10 +104,9 @@ fn handle_difficulty_request( } fn seed_from_system_time() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_nanos() as u64) - .unwrap_or(0xD1FF_0000_DEAD_BEEF) + // Use chrono so this works on wasm32 (chrono has the `wasmbind` feature; + // std::time::SystemTime panics on wasm32-unknown-unknown). + Utc::now().timestamp_nanos_opt().unwrap_or(0) as u64 } // --------------------------------------------------------------------------- diff --git a/solitaire_engine/src/game_plugin.rs b/solitaire_engine/src/game_plugin.rs index aa91f5f..e6e5b00 100644 --- a/solitaire_engine/src/game_plugin.rs +++ b/solitaire_engine/src/game_plugin.rs @@ -7,12 +7,12 @@ //! file is deleted so the next launch starts fresh. use std::path::PathBuf; -use std::time::{SystemTime, UNIX_EPOCH}; + +use chrono::Utc; use bevy::prelude::*; use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future}; use bevy::window::AppLifecycle; -use chrono::Utc; use solitaire_core::game_state::{DrawMode, GameMode, GameState}; use klondike::KlondikePile; use solitaire_core::solver::{SolverConfig, SolverResult, try_solve}; @@ -312,9 +312,7 @@ fn tick_elapsed_time( } fn seed_from_system_time() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_or(0, |d| d.as_nanos() as u64) + Utc::now().timestamp_nanos_opt().unwrap_or(0) as u64 } /// Walks forward from `initial_seed` (incrementing by 1 with wrapping diff --git a/solitaire_engine/src/theme/registry.rs b/solitaire_engine/src/theme/registry.rs index b811bfc..00a2a18 100644 --- a/solitaire_engine/src/theme/registry.rs +++ b/solitaire_engine/src/theme/registry.rs @@ -85,13 +85,18 @@ pub struct ThemeRegistryPlugin; impl Plugin for ThemeRegistryPlugin { fn build(&self, app: &mut App) { - app.init_resource::() - .add_systems(Startup, build_registry_on_startup); + app.init_resource::(); + // User-themes directory scan requires a filesystem. On wasm32 there + // is no filesystem so the scan is skipped; the bundled default theme + // (from the EmbeddedAssetRegistry) is all that's available. + #[cfg(not(target_arch = "wasm32"))] + app.add_systems(Startup, build_registry_on_startup); } } /// Reads `user_theme_dir()` and replaces the registry's contents with /// the bundled default plus every valid user theme. +#[cfg(not(target_arch = "wasm32"))] fn build_registry_on_startup(mut registry: bevy::ecs::system::ResMut) { *registry = build_registry(&user_theme_dir()); } diff --git a/solitaire_engine/src/time_attack_plugin.rs b/solitaire_engine/src/time_attack_plugin.rs index 253cd33..1f181ab 100644 --- a/solitaire_engine/src/time_attack_plugin.rs +++ b/solitaire_engine/src/time_attack_plugin.rs @@ -22,7 +22,8 @@ //! was closed, the file is treated as missing. use std::path::PathBuf; -use std::time::{SystemTime, UNIX_EPOCH}; + +use chrono::Utc; use bevy::prelude::*; use solitaire_core::game_state::GameMode; @@ -222,9 +223,9 @@ fn auto_deal_on_time_attack_win( /// the system time predates the epoch (impossible under any sane clock, /// but the fallback keeps the function infallible). fn current_unix_secs() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_or(0, |d| d.as_secs()) + // Use chrono so this works on wasm32 (chrono has the `wasmbind` feature; + // std::time::SystemTime panics on wasm32-unknown-unknown). + Utc::now().timestamp().max(0) as u64 } /// Periodically persists the live `TimeAttackResource` to diff --git a/solitaire_server/web/pkg/canvas.js b/solitaire_server/web/pkg/canvas.js index f1826d4..da48023 100644 --- a/solitaire_server/web/pkg/canvas.js +++ b/solitaire_server/web/pkg/canvas.js @@ -1,5 +1,3 @@ -/* @ts-self-types="./canvas.d.ts" */ - export function start() { wasm.start(); } @@ -1651,62 +1649,62 @@ function __wbg_get_imports() { return ret; }, __wbindgen_cast_0000000000000001: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 114649, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 114646, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hf0188236128725a8); return ret; }, __wbindgen_cast_0000000000000002: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 9793, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 9790, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h038e9392efba509b); return ret; }, __wbindgen_cast_0000000000000003: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Array"), NamedExternref("ResizeObserver")], shim_idx: 9795, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Array"), NamedExternref("ResizeObserver")], shim_idx: 9792, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hb8334c8e03ee5ee1); return ret; }, __wbindgen_cast_0000000000000004: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Array")], shim_idx: 9793, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Array")], shim_idx: 9790, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h038e9392efba509b_3); return ret; }, __wbindgen_cast_0000000000000005: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 9793, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 9790, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h038e9392efba509b_4); return ret; }, __wbindgen_cast_0000000000000006: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("FocusEvent")], shim_idx: 9793, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("FocusEvent")], shim_idx: 9790, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h038e9392efba509b_5); return ret; }, __wbindgen_cast_0000000000000007: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 9793, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 9790, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h038e9392efba509b_6); return ret; }, __wbindgen_cast_0000000000000008: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PageTransitionEvent")], shim_idx: 9793, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PageTransitionEvent")], shim_idx: 9790, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h038e9392efba509b_7); return ret; }, __wbindgen_cast_0000000000000009: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PointerEvent")], shim_idx: 9793, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PointerEvent")], shim_idx: 9790, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h038e9392efba509b_8); return ret; }, __wbindgen_cast_000000000000000a: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("WheelEvent")], shim_idx: 9793, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("WheelEvent")], shim_idx: 9790, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h038e9392efba509b_9); return ret; }, __wbindgen_cast_000000000000000b: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Option(NamedExternref("Blob"))], shim_idx: 9803, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Option(NamedExternref("Blob"))], shim_idx: 9800, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h618c0cad9a289a93); return ret; }, __wbindgen_cast_000000000000000c: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 9797, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 9794, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h277d9d6b389a2871); return ret; }, diff --git a/solitaire_server/web/pkg/canvas_bg.wasm b/solitaire_server/web/pkg/canvas_bg.wasm index 65c3fc5..2b656de 100644 Binary files a/solitaire_server/web/pkg/canvas_bg.wasm and b/solitaire_server/web/pkg/canvas_bg.wasm differ diff --git a/solitaire_web/src/lib.rs b/solitaire_web/src/lib.rs index 0c08f3f..da57ebd 100644 --- a/solitaire_web/src/lib.rs +++ b/solitaire_web/src/lib.rs @@ -9,6 +9,7 @@ //! - Storage is handled automatically by `WasmStorage` (localStorage-backed), //! wired by `CoreGamePlugin` via `default_storage_backend()`. +use bevy::asset::AssetMetaCheck; use bevy::prelude::*; use bevy::window::{Window, WindowPlugin}; use solitaire_data::LocalOnlyProvider; @@ -21,19 +22,27 @@ pub fn start() { App::new() .add_plugins( - DefaultPlugins.set(WindowPlugin { - primary_window: Some(Window { - // Bind to the existing in play.html. - // Without this, Bevy appends its own canvas to . - canvas: Some("#bevy-canvas".into()), - // Let CSS size the canvas; Bevy follows the element's size. - fit_canvas_to_parent: true, - // Prevent the browser stealing keyboard events and scroll. - prevent_default_event_handling: true, + DefaultPlugins + .set(WindowPlugin { + primary_window: Some(Window { + // Bind to the existing in play.html. + // Without this, Bevy appends its own canvas to . + canvas: Some("#bevy-canvas".into()), + // Let CSS size the canvas; Bevy follows the element's size. + fit_canvas_to_parent: true, + // Prevent the browser stealing keyboard events and scroll. + prevent_default_event_handling: true, + ..default() + }), + ..default() + }) + // Bevy's default AssetPlugin fetches a `.meta` sidecar file for + // every asset before loading the asset itself. We don't ship + // `.meta` files, so skip the check to avoid a flood of 404s. + .set(AssetPlugin { + meta_check: AssetMetaCheck::Never, ..default() }), - ..default() - }), ) // LocalOnlyProvider disables cloud sync — correct for the web build // since SyncPlugin is cfg-gated out on wasm32 anyway.