Files
Ferrous-Solitaire/solitaire_engine/src/assets/user_dir.rs
T
funman300 a92ac066a6
Build and Deploy / build-and-push (push) Failing after 1m6s
fix(web): resolve wasm32 runtime panics; game boots and renders in Firefox
Fixes found while testing the Bevy WASM build in a real browser:

1. chrono wasmbind: add `wasmbind` feature to workspace chrono dep so
   Local::now()/Utc::now() use js-sys::Date on wasm32 (previously
   fell through to std::time::SystemTime which panics on wasm32).

2. std::time::SystemTime: replace all remaining direct SystemTime::now()
   calls (4 sites across game_plugin, difficulty_plugin, time_attack_plugin,
   solitaire_data/storage) with chrono::Utc::now() which is wasm32-safe.

3. user_dir: return empty PathBuf (instead of panicking) when data_dir()
   is None on wasm32; there is no filesystem in the browser so user themes
   are unsupported and a benign empty path is correct.

4. ThemeRegistryPlugin: gate build_registry_on_startup to non-wasm32
   (the filesystem scan for user themes has nothing to scan in the browser;
   only the bundled embedded themes are available).

5. AssetMetaCheck::Never: configure AssetPlugin in solitaire_web to skip
   `.meta` sidecar fetches — we don't ship .meta files, so the default
   AssetMetaCheck::Always produced a 404 flood on every card/background asset.

Result: `http://localhost:<port>/play` boots in Firefox with zero errors
and renders the full Bevy game — home screen, onboarding modal, HUD all
visible. Assets load correctly from /assets/. Chromium has a separate
wgpu-27/ANGLE/GLES shader translation bug (not in our code); Firefox works.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:16:19 -07:00

149 lines
6.1 KiB
Rust

//! Per-platform resolution of the user-themes directory.
//!
//! The path is determined exactly once and exposed via
//! [`user_theme_dir`]. The base directory comes from
//! [`solitaire_data::data_dir`] (desktop: `dirs::data_dir()`;
//! Android: the hardcoded `/data/data/<package>/files` sandbox
//! path). Mobile entry points may still override the path via
//! [`set_user_theme_dir`] when they need to point at a non-default
//! location (e.g. tests, custom AssetManager wiring).
//!
//! # Why panic instead of returning Result?
//!
//! User-theme resolution is bootstrap-time configuration, not game
//! logic, so per CLAUDE.md panics are acceptable here. Returning
//! `Result` would force every caller (the registry, the asset source,
//! the importer) to plumb an error through systems that have no
//! recovery path: there is no useful state to display if we can't
//! find the user themes directory at all.
use std::path::PathBuf;
use std::sync::OnceLock;
/// Override slot populated by mobile entry points (Android's
/// `android_main`, iOS's launch handler) before the Bevy `App` starts.
/// Desktop platforms ignore the override and fall through to
/// [`desktop_theme_dir`].
static USER_THEME_DIR_OVERRIDE: OnceLock<PathBuf> = OnceLock::new();
/// Sub-folder under `dirs::data_dir()` where the project keeps every
/// per-user file. Matches the existing convention used by
/// `solitaire_data` for `settings.json`, `stats.json`, etc.
const APP_DIR_NAME: &str = "ferrous_solitaire";
/// Sub-folder under [`APP_DIR_NAME`] dedicated to user themes.
const THEME_DIR_NAME: &str = "themes";
/// Sets the user-themes directory at runtime — escape hatch for
/// embedders or tests that need to override the platform default.
///
/// Returns `Err` containing the rejected path if the override has
/// already been set. The first caller wins and subsequent calls are
/// silently a no-op-with-feedback so a mis-configured embedder can't
/// flip the path mid-session.
///
/// Mostly unnecessary now that [`solitaire_data::data_dir`] handles
/// every supported target — the override is kept for tests and for
/// embedders that want a non-default location (e.g. a sandboxed
/// AssetManager root on a future iOS port).
pub fn set_user_theme_dir(path: PathBuf) -> Result<(), PathBuf> {
USER_THEME_DIR_OVERRIDE.set(path)
}
/// Returns the absolute path of the user-themes directory on the
/// current platform.
///
/// # Panics
///
/// Panics if [`solitaire_data::data_dir`] returns `None`, which on
/// desktop indicates a broken `$HOME` / `$XDG_*` configuration.
/// Android always returns `Some`. The panic message names the
/// supported workaround ([`set_user_theme_dir`]).
pub fn user_theme_dir() -> PathBuf {
if let Some(p) = USER_THEME_DIR_OVERRIDE.get() {
return p.clone();
}
user_theme_dir_for(detected_platform_data_dir())
}
/// Composition helper that takes the platform data dir as input so the
/// pure path-joining behaviour is unit-testable without depending on
/// the user's actual `$HOME`.
fn user_theme_dir_for(data_dir: PathBuf) -> PathBuf {
data_dir.join(APP_DIR_NAME).join(THEME_DIR_NAME)
}
/// Per-target-os resolution of the platform's data dir. Delegates
/// to [`solitaire_data::data_dir`] which encapsulates the
/// per-target shape (desktop: `dirs::data_dir()`; android: the
/// hardcoded `/data/data/<package>/files` sandbox path). Panics
/// only when the underlying resolver returns `None`, which on
/// desktop indicates a broken `$HOME` / `$XDG_*` configuration —
/// the panic message names the supported workaround.
fn detected_platform_data_dir() -> PathBuf {
solitaire_data::data_dir().unwrap_or_else(|| {
// 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()."
)
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn user_theme_dir_for_appends_ferrous_solitaire_themes() {
let dir = user_theme_dir_for(PathBuf::from("/tmp/data"));
assert_eq!(
dir,
PathBuf::from("/tmp/data/ferrous_solitaire/themes"),
"user dir must nest under ferrous_solitaire/themes"
);
}
#[test]
fn user_theme_dir_for_handles_empty_root() {
let dir = user_theme_dir_for(PathBuf::new());
assert_eq!(dir, PathBuf::from("ferrous_solitaire/themes"));
}
#[test]
fn detected_data_dir_yields_a_path_with_a_parent() {
// On every supported target the platform resolver
// (`solitaire_data::data_dir`) returns a usable directory:
// desktop targets via `dirs::data_dir()` (the test machine
// already has a `$HOME` for it to discover), Android via
// the hardcoded `/data/data/<package>/files` sandbox path.
// We don't pin the exact value because it depends on the
// user's `$HOME` on desktop, but it must at least be a
// non-empty path with a parent component.
let dir = detected_platform_data_dir();
assert!(
dir.parent().is_some(),
"data dir {dir:?} should be absolute"
);
}
// The OnceLock-based override is intentionally NOT covered here:
// setting it once would pollute every subsequent test in the
// process that called `user_theme_dir()`. The override's
// first-write-wins semantics come from `std::sync::OnceLock` which
// is already well-tested upstream; the behaviour we add on top is
// a trivial early-return that's covered by code review.
}