fix(engine): degrade gracefully when the platform data dir is missing

user_theme_dir() panicked on desktop when dirs::data_dir() returned
None (broken $HOME / $XDG_*). Return an empty path instead — exactly
what the wasm32 branch already did — so theme discovery reports 'no
user themes' and the bundled default keeps working. Warns once with
the set_user_theme_dir() workaround. CLAUDE.md §2.3: no panic! in
runtime logic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-07-07 11:00:20 -07:00
parent 113a933170
commit 757c35e4a0
+27 -24
View File
@@ -53,12 +53,12 @@ pub fn set_user_theme_dir(path: PathBuf) -> Result<(), PathBuf> {
/// Returns the absolute path of the user-themes directory on the /// Returns the absolute path of the user-themes directory on the
/// current platform. /// current platform.
/// ///
/// # Panics /// When [`solitaire_data::data_dir`] returns `None` (broken `$HOME` /
/// /// `$XDG_*` on desktop; always on wasm32, which has no filesystem) this
/// Panics if [`solitaire_data::data_dir`] returns `None`, which on /// returns an empty path — callers treat that as "no user themes" and
/// desktop indicates a broken `$HOME` / `$XDG_*` configuration. /// the bundled default theme still works. A warning naming the
/// Android always returns `Some`. The panic message names the /// [`set_user_theme_dir`] workaround is logged once. Android always
/// supported workaround ([`set_user_theme_dir`]). /// resolves.
pub fn user_theme_dir() -> PathBuf { pub fn user_theme_dir() -> PathBuf {
if let Some(p) = USER_THEME_DIR_OVERRIDE.get() { if let Some(p) = USER_THEME_DIR_OVERRIDE.get() {
return p.clone(); return p.clone();
@@ -76,29 +76,32 @@ fn user_theme_dir_for(data_dir: PathBuf) -> PathBuf {
/// Per-target-os resolution of the platform's data dir. Delegates /// Per-target-os resolution of the platform's data dir. Delegates
/// to [`solitaire_data::data_dir`] which encapsulates the /// to [`solitaire_data::data_dir`] which encapsulates the
/// per-target shape (desktop: `dirs::data_dir()`; android: the /// per-target shape (desktop: `dirs::data_dir()`; android: the
/// hardcoded `/data/data/<package>/files` sandbox path). Panics /// hardcoded `/data/data/<package>/files` sandbox path).
/// only when the underlying resolver returns `None`, which on ///
/// desktop indicates a broken `$HOME` / `$XDG_*` configuration — /// When the resolver returns `None` — always on wasm32 (no
/// the panic message names the supported workaround. /// filesystem), or a broken `$HOME` / `$XDG_*` configuration on
/// desktop — this degrades to an empty path, which downstream theme
/// scanning treats as "no user themes"; the bundled default theme is
/// unaffected. CLAUDE.md §2.3 forbids panicking here: losing custom
/// themes must not take the whole game down with it.
fn detected_platform_data_dir() -> PathBuf { fn detected_platform_data_dir() -> PathBuf {
solitaire_data::data_dir().unwrap_or_else(|| { 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"))] #[cfg(not(target_arch = "wasm32"))]
{ {
panic!( use std::sync::Once;
"user_theme_dir(): platform data directory is unavailable. \ static WARN_ONCE: Once = Once::new();
On Linux check $XDG_DATA_HOME or $HOME; on macOS / Windows \ WARN_ONCE.call_once(|| {
the OS reported no Application Support / AppData path. \ bevy::log::warn!(
As a workaround call solitaire_engine::assets::user_dir::\ "user_theme_dir(): platform data directory is unavailable; \
set_user_theme_dir() before App::run()." user themes are disabled. 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()."
);
});
} }
PathBuf::new()
}) })
} }