feat(engine): bundle Rusty Pixel as a built-in theme
The pixel-art card theme generated via Claude Design (53 PNGs at
256x384, ~340 KB total) now ships embedded in the binary alongside
the existing default SVG theme. Players see the new theme in the
picker out of the box without needing to drop files into
~/.local/share/solitaire_quest/themes/.
solitaire_engine/assets/themes/rusty-pixel/:
- 53 PNGs (52 face cards + 1 back) at 256x384
- theme.ron declaring meta.id = "rusty-pixel",
card_aspect = (2, 3), pixel_art = true
assets/sources.rs:
- New constants RUSTY_PIXEL_THEME_MANIFEST_URL,
RUSTY_PIXEL_THEME_MANIFEST_PATH,
RUSTY_PIXEL_THEME_MANIFEST_BYTES.
- New embed_rusty_pixel_png! macro mirroring embed_default_svg!.
- New RUSTY_PIXEL_THEME_PNGS table — 53 entries, one per file.
- New rusty_pixel_theme_png_bytes(filename) lookup helper
mirroring default_theme_svg_bytes for the thumbnail cache.
- New populate_embedded_rusty_pixel_theme(app) registers the
manifest + every PNG into Bevy's EmbeddedAssetRegistry.
- AssetSourcesPlugin::build now calls both populate functions
so the picker has both themes loadable from the binary alone.
theme/registry.rs:
- New rusty_pixel_entry() returns the bundled metadata.
- build_registry now inserts default + rusty-pixel ahead of the
user-dir scan, and filters user themes whose id collides with
a bundled built-in. Bundled wins on collision because it's
guaranteed complete; the user's overriding copy may be partial
or stale.
- Updated existing tests for the new len()=2-instead-of-1 baseline.
- New test user_theme_id_collision_with_bundled_is_dropped pins
the dedup contract.
theme/plugin.rs:
- load_initial_theme + react_to_settings_theme_change now both
consult a new manifest_url_for(theme_id) helper that routes
bundled built-ins through embedded:// and unknown ids through
themes://. Drops the previous hard-coded "default →
DEFAULT_THEME_MANIFEST_URL else themes://" branch.
- read_theme_preview_bytes also checks the rusty-pixel embed
table before falling through to the user-dir filesystem read,
so the picker chip's thumbnail works on a fresh install where
the user-dir doesn't exist.
Workspace: 1172 passing tests / 0 failing, was 1171 (+1 net from
the new collision test). cargo clippy --workspace --all-targets
-- -D warnings clean. Binary grows by ~340 KB (the 53 bundled
PNGs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,10 @@ pub mod svg_loader;
|
||||
pub mod user_dir;
|
||||
|
||||
pub use sources::{
|
||||
default_theme_svg_bytes, populate_embedded_default_theme, register_theme_asset_sources,
|
||||
AssetSourcesPlugin, DEFAULT_THEME_MANIFEST_URL, USER_THEMES,
|
||||
default_theme_svg_bytes, populate_embedded_default_theme,
|
||||
populate_embedded_rusty_pixel_theme, register_theme_asset_sources,
|
||||
rusty_pixel_theme_png_bytes, AssetSourcesPlugin, DEFAULT_THEME_MANIFEST_URL,
|
||||
RUSTY_PIXEL_THEME_MANIFEST_URL, USER_THEMES,
|
||||
};
|
||||
pub use svg_loader::{rasterize_svg, SvgLoader, SvgLoaderError, SvgLoaderSettings};
|
||||
pub use user_dir::{set_user_theme_dir, user_theme_dir};
|
||||
|
||||
@@ -155,6 +155,100 @@ const DEFAULT_THEME_SVGS: &[(&str, &[u8])] = &[
|
||||
embed_default_svg!("spades_king.svg"),
|
||||
];
|
||||
|
||||
/// Stable embedded asset URL of the bundled rusty-pixel theme manifest.
|
||||
///
|
||||
/// `theme/plugin.rs::manifest_url_for` uses this when the player
|
||||
/// selects "Rusty Pixel" so the manifest loads from the binary's
|
||||
/// embedded asset registry rather than `themes://` (which would
|
||||
/// require a user-supplied copy on disk).
|
||||
pub const RUSTY_PIXEL_THEME_MANIFEST_URL: &str =
|
||||
"embedded://solitaire_engine/assets/themes/rusty-pixel/theme.ron";
|
||||
|
||||
/// Path the embedded rusty-pixel theme manifest registers under,
|
||||
/// relative to the `embedded://` source root. Kept in lockstep with
|
||||
/// [`RUSTY_PIXEL_THEME_MANIFEST_URL`] by the unit test
|
||||
/// `rusty_pixel_theme_url_constant_matches_embedded_path`.
|
||||
const RUSTY_PIXEL_THEME_MANIFEST_PATH: &str =
|
||||
"solitaire_engine/assets/themes/rusty-pixel/theme.ron";
|
||||
|
||||
/// Bytes of the bundled rusty-pixel theme manifest. Mirrors the
|
||||
/// default-theme embed pattern — `include_bytes!` resolves at compile
|
||||
/// time so the binary ships the manifest even on machines whose
|
||||
/// `solitaire_engine/assets/` directory is absent at runtime.
|
||||
const RUSTY_PIXEL_THEME_MANIFEST_BYTES: &[u8] =
|
||||
include_bytes!("../../assets/themes/rusty-pixel/theme.ron");
|
||||
|
||||
/// Generates a `(stable_path, bytes)` entry for one rusty-pixel
|
||||
/// theme PNG. Mirrors [`embed_default_svg!`] for the second bundled
|
||||
/// theme — the path matches what `theme.ron` references.
|
||||
macro_rules! embed_rusty_pixel_png {
|
||||
($name:literal) => {
|
||||
(
|
||||
concat!("solitaire_engine/assets/themes/rusty-pixel/", $name),
|
||||
include_bytes!(concat!("../../assets/themes/rusty-pixel/", $name)) as &[u8],
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Every rusty-pixel theme PNG bundled into the binary. 53 entries:
|
||||
/// 52 face cards + 1 back. The macro pulls each PNG via
|
||||
/// `include_bytes!` so adding a new file is a one-line append.
|
||||
const RUSTY_PIXEL_THEME_PNGS: &[(&str, &[u8])] = &[
|
||||
embed_rusty_pixel_png!("back.png"),
|
||||
embed_rusty_pixel_png!("clubs_ace.png"),
|
||||
embed_rusty_pixel_png!("clubs_2.png"),
|
||||
embed_rusty_pixel_png!("clubs_3.png"),
|
||||
embed_rusty_pixel_png!("clubs_4.png"),
|
||||
embed_rusty_pixel_png!("clubs_5.png"),
|
||||
embed_rusty_pixel_png!("clubs_6.png"),
|
||||
embed_rusty_pixel_png!("clubs_7.png"),
|
||||
embed_rusty_pixel_png!("clubs_8.png"),
|
||||
embed_rusty_pixel_png!("clubs_9.png"),
|
||||
embed_rusty_pixel_png!("clubs_10.png"),
|
||||
embed_rusty_pixel_png!("clubs_jack.png"),
|
||||
embed_rusty_pixel_png!("clubs_queen.png"),
|
||||
embed_rusty_pixel_png!("clubs_king.png"),
|
||||
embed_rusty_pixel_png!("diamonds_ace.png"),
|
||||
embed_rusty_pixel_png!("diamonds_2.png"),
|
||||
embed_rusty_pixel_png!("diamonds_3.png"),
|
||||
embed_rusty_pixel_png!("diamonds_4.png"),
|
||||
embed_rusty_pixel_png!("diamonds_5.png"),
|
||||
embed_rusty_pixel_png!("diamonds_6.png"),
|
||||
embed_rusty_pixel_png!("diamonds_7.png"),
|
||||
embed_rusty_pixel_png!("diamonds_8.png"),
|
||||
embed_rusty_pixel_png!("diamonds_9.png"),
|
||||
embed_rusty_pixel_png!("diamonds_10.png"),
|
||||
embed_rusty_pixel_png!("diamonds_jack.png"),
|
||||
embed_rusty_pixel_png!("diamonds_queen.png"),
|
||||
embed_rusty_pixel_png!("diamonds_king.png"),
|
||||
embed_rusty_pixel_png!("hearts_ace.png"),
|
||||
embed_rusty_pixel_png!("hearts_2.png"),
|
||||
embed_rusty_pixel_png!("hearts_3.png"),
|
||||
embed_rusty_pixel_png!("hearts_4.png"),
|
||||
embed_rusty_pixel_png!("hearts_5.png"),
|
||||
embed_rusty_pixel_png!("hearts_6.png"),
|
||||
embed_rusty_pixel_png!("hearts_7.png"),
|
||||
embed_rusty_pixel_png!("hearts_8.png"),
|
||||
embed_rusty_pixel_png!("hearts_9.png"),
|
||||
embed_rusty_pixel_png!("hearts_10.png"),
|
||||
embed_rusty_pixel_png!("hearts_jack.png"),
|
||||
embed_rusty_pixel_png!("hearts_queen.png"),
|
||||
embed_rusty_pixel_png!("hearts_king.png"),
|
||||
embed_rusty_pixel_png!("spades_ace.png"),
|
||||
embed_rusty_pixel_png!("spades_2.png"),
|
||||
embed_rusty_pixel_png!("spades_3.png"),
|
||||
embed_rusty_pixel_png!("spades_4.png"),
|
||||
embed_rusty_pixel_png!("spades_5.png"),
|
||||
embed_rusty_pixel_png!("spades_6.png"),
|
||||
embed_rusty_pixel_png!("spades_7.png"),
|
||||
embed_rusty_pixel_png!("spades_8.png"),
|
||||
embed_rusty_pixel_png!("spades_9.png"),
|
||||
embed_rusty_pixel_png!("spades_10.png"),
|
||||
embed_rusty_pixel_png!("spades_jack.png"),
|
||||
embed_rusty_pixel_png!("spades_queen.png"),
|
||||
embed_rusty_pixel_png!("spades_king.png"),
|
||||
];
|
||||
|
||||
/// Registers asset sources that must be in place *before*
|
||||
/// `AssetPlugin` is built.
|
||||
///
|
||||
@@ -191,6 +285,7 @@ pub struct AssetSourcesPlugin;
|
||||
impl Plugin for AssetSourcesPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
populate_embedded_default_theme(app);
|
||||
populate_embedded_rusty_pixel_theme(app);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,6 +349,43 @@ pub fn populate_embedded_default_theme(app: &mut App) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the embedded PNG bytes for a single rusty-pixel theme file
|
||||
/// (e.g. `"back.png"` or `"spades_ace.png"`), or `None` when the
|
||||
/// filename is not bundled. Mirrors [`default_theme_svg_bytes`] for
|
||||
/// the second bundled theme so the picker thumbnail cache can read
|
||||
/// preview-sized art without going through the async asset graph.
|
||||
pub fn rusty_pixel_theme_png_bytes(filename: &str) -> Option<&'static [u8]> {
|
||||
let suffix = format!("/{filename}");
|
||||
RUSTY_PIXEL_THEME_PNGS
|
||||
.iter()
|
||||
.find(|(path, _)| path.ends_with(&suffix))
|
||||
.map(|(_, bytes)| *bytes)
|
||||
}
|
||||
|
||||
/// Pushes the bundled rusty-pixel theme manifest + every face/back
|
||||
/// PNG into the [`EmbeddedAssetRegistry`]. Pairs with
|
||||
/// [`populate_embedded_default_theme`] — both are called from
|
||||
/// [`AssetSourcesPlugin::build`] after `AssetPlugin` has set up the
|
||||
/// embedded source.
|
||||
pub fn populate_embedded_rusty_pixel_theme(app: &mut App) {
|
||||
let registry = app
|
||||
.world_mut()
|
||||
.get_resource_or_insert_with(EmbeddedAssetRegistry::default);
|
||||
|
||||
registry.insert_asset(
|
||||
std::path::PathBuf::from(RUSTY_PIXEL_THEME_MANIFEST_PATH),
|
||||
std::path::Path::new(RUSTY_PIXEL_THEME_MANIFEST_PATH),
|
||||
RUSTY_PIXEL_THEME_MANIFEST_BYTES,
|
||||
);
|
||||
for (path, bytes) in RUSTY_PIXEL_THEME_PNGS {
|
||||
registry.insert_asset(
|
||||
std::path::PathBuf::from(*path),
|
||||
std::path::Path::new(*path),
|
||||
*bytes,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user