Compare commits
15 Commits
v0.24.0
...
01d6b27e61
| Author | SHA1 | Date | |
|---|---|---|---|
| 01d6b27e61 | |||
| 3cffbc2c51 | |||
| 2ef25934ac | |||
| bb670d6cc6 | |||
| 76911c57c9 | |||
| 8391235a1a | |||
| 2f3a6b9586 | |||
| 4d20b70809 | |||
| bfadcf0e0d | |||
| 356dbebe57 | |||
| c90c783177 | |||
| bbf4b2c14a | |||
| 62be72e918 | |||
| 1f46785b31 | |||
| 2e5d82f83c |
@@ -12,7 +12,6 @@ on:
|
|||||||
- '**.md'
|
- '**.md'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
ANDROID_SDK_ROOT: /opt/android-sdk
|
|
||||||
NDK_VERSION: "25.2.9519653"
|
NDK_VERSION: "25.2.9519653"
|
||||||
BUILD_TOOLS_VERSION: "34.0.0"
|
BUILD_TOOLS_VERSION: "34.0.0"
|
||||||
|
|
||||||
@@ -28,39 +27,59 @@ jobs:
|
|||||||
id: meta
|
id: meta
|
||||||
run: echo "sha=${GITHUB_SHA::8}" >> "$GITHUB_OUTPUT"
|
run: echo "sha=${GITHUB_SHA::8}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
# ── Android SDK + NDK ──────────────────────────────────────────────
|
# ── Probe the container's existing Android SDK ─────────────────────
|
||||||
# Cache the entire SDK root so subsequent runs skip the ~2 GB download.
|
- name: Detect Android SDK
|
||||||
- name: Cache Android SDK
|
id: sdk
|
||||||
uses: actions/cache@v4
|
run: |
|
||||||
id: sdk-cache
|
# Gitea/GitHub ubuntu-latest runners may already have an Android
|
||||||
with:
|
# SDK at /usr/local/lib/android/sdk. If it has the platform and
|
||||||
path: ${{ env.ANDROID_SDK_ROOT }}
|
# NDK we need we use it directly; otherwise we install our own.
|
||||||
key: android-sdk-ndk${{ env.NDK_VERSION }}-bt${{ env.BUILD_TOOLS_VERSION }}
|
DEFAULT="${ANDROID_HOME:-/usr/local/lib/android/sdk}"
|
||||||
|
echo "container default ANDROID_HOME: $DEFAULT"
|
||||||
|
echo "sdk_path=$DEFAULT" >> "$GITHUB_OUTPUT"
|
||||||
|
ls "$DEFAULT/platforms/" 2>/dev/null || echo "(no platforms/ in default)"
|
||||||
|
ls "$DEFAULT/ndk/" 2>/dev/null || echo "(no ndk/ in default)"
|
||||||
|
|
||||||
|
# ── System dependencies (always needed for build tools + signing) ──
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: sudo apt-get install -y openjdk-17-jdk-headless unzip
|
|
||||||
|
|
||||||
- name: Install Android SDK + NDK
|
|
||||||
if: steps.sdk-cache.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools"
|
sudo apt-get update -y
|
||||||
curl -sL \
|
sudo apt-get install -y openjdk-17-jdk-headless unzip jq
|
||||||
"https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" \
|
|
||||||
-o /tmp/cmdtools.zip
|
# ── Install missing SDK components if needed ───────────────────────
|
||||||
unzip -q /tmp/cmdtools.zip -d /tmp/cmdtools
|
- name: Install SDK platform and NDK
|
||||||
mv /tmp/cmdtools/cmdline-tools "$ANDROID_SDK_ROOT/cmdline-tools/latest"
|
env:
|
||||||
# Accept all SDK licences non-interactively.
|
ANDROID_SDK: ${{ steps.sdk.outputs.sdk_path }}
|
||||||
yes | "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" --licenses \
|
run: |
|
||||||
|
# Ensure sdkmanager is on PATH
|
||||||
|
SDKMAN="$ANDROID_SDK/cmdline-tools/latest/bin/sdkmanager"
|
||||||
|
if [ ! -f "$SDKMAN" ]; then
|
||||||
|
echo "sdkmanager not found — installing cmdline-tools"
|
||||||
|
mkdir -p "$ANDROID_SDK/cmdline-tools"
|
||||||
|
curl -sL \
|
||||||
|
"https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" \
|
||||||
|
-o /tmp/cmdtools.zip
|
||||||
|
unzip -q /tmp/cmdtools.zip -d /tmp/cmdtools
|
||||||
|
sudo mv /tmp/cmdtools/cmdline-tools "$ANDROID_SDK/cmdline-tools/latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
yes | sudo "$SDKMAN" --sdk_root="$ANDROID_SDK" --licenses \
|
||||||
> /dev/null 2>&1 || true
|
> /dev/null 2>&1 || true
|
||||||
"$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" \
|
|
||||||
"build-tools;$BUILD_TOOLS_VERSION" \
|
|
||||||
"platforms;android-34" \
|
|
||||||
"ndk;$NDK_VERSION"
|
|
||||||
|
|
||||||
- name: Export Android environment
|
NEEDS=""
|
||||||
run: |
|
[ ! -f "$ANDROID_SDK/platforms/android-34/android.jar" ] && NEEDS="$NEEDS platforms;android-34"
|
||||||
echo "ANDROID_HOME=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV"
|
[ ! -d "$ANDROID_SDK/ndk/$NDK_VERSION" ] && NEEDS="$NEEDS ndk;$NDK_VERSION"
|
||||||
echo "ANDROID_NDK_HOME=$ANDROID_SDK_ROOT/ndk/$NDK_VERSION" >> "$GITHUB_ENV"
|
[ ! -d "$ANDROID_SDK/build-tools/$BUILD_TOOLS_VERSION" ] && NEEDS="$NEEDS build-tools;$BUILD_TOOLS_VERSION"
|
||||||
|
|
||||||
|
if [ -n "$NEEDS" ]; then
|
||||||
|
echo "Installing:$NEEDS"
|
||||||
|
sudo "$SDKMAN" --sdk_root="$ANDROID_SDK" $NEEDS
|
||||||
|
else
|
||||||
|
echo "All SDK components already present"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ANDROID_HOME=$ANDROID_SDK" >> "$GITHUB_ENV"
|
||||||
|
echo "ANDROID_NDK_HOME=$ANDROID_SDK/ndk/$NDK_VERSION" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
# ── Rust toolchain ─────────────────────────────────────────────────
|
# ── Rust toolchain ─────────────────────────────────────────────────
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ on:
|
|||||||
- 'v*.*.*'
|
- 'v*.*.*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
ANDROID_SDK_ROOT: /opt/android-sdk
|
ANDROID_HOME: /opt/android-sdk
|
||||||
|
ANDROID_NDK_HOME: /opt/android-sdk/ndk/25.2.9519653
|
||||||
NDK_VERSION: "25.2.9519653"
|
NDK_VERSION: "25.2.9519653"
|
||||||
BUILD_TOOLS_VERSION: "34.0.0"
|
BUILD_TOOLS_VERSION: "34.0.0"
|
||||||
GITEA_API: https://git.aleshym.co/api/v1
|
GITEA_API: https://git.aleshym.co/api/v1
|
||||||
@@ -31,35 +32,32 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
id: sdk-cache
|
id: sdk-cache
|
||||||
with:
|
with:
|
||||||
path: ${{ env.ANDROID_SDK_ROOT }}
|
path: ${{ env.ANDROID_HOME }}
|
||||||
key: android-sdk-ndk${{ env.NDK_VERSION }}-bt${{ env.BUILD_TOOLS_VERSION }}
|
key: v2-android-sdk-ndk${{ env.NDK_VERSION }}-bt${{ env.BUILD_TOOLS_VERSION }}
|
||||||
|
|
||||||
# Java and jq are always needed (apksigner requires Java even on cache hits).
|
# Java and jq are always needed (apksigner requires Java even on cache hits).
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: sudo apt-get install -y openjdk-17-jdk-headless unzip jq
|
run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y openjdk-17-jdk-headless unzip jq
|
||||||
|
|
||||||
- name: Install Android SDK + NDK
|
- name: Install Android SDK + NDK
|
||||||
if: steps.sdk-cache.outputs.cache-hit != 'true'
|
if: steps.sdk-cache.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools"
|
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
||||||
curl -sL \
|
curl -sL \
|
||||||
"https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" \
|
"https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" \
|
||||||
-o /tmp/cmdtools.zip
|
-o /tmp/cmdtools.zip
|
||||||
unzip -q /tmp/cmdtools.zip -d /tmp/cmdtools
|
unzip -q /tmp/cmdtools.zip -d /tmp/cmdtools
|
||||||
mv /tmp/cmdtools/cmdline-tools "$ANDROID_SDK_ROOT/cmdline-tools/latest"
|
mv /tmp/cmdtools/cmdline-tools "$ANDROID_HOME/cmdline-tools/latest"
|
||||||
yes | "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" --licenses \
|
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" --licenses \
|
||||||
> /dev/null 2>&1 || true
|
> /dev/null 2>&1 || true
|
||||||
"$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" \
|
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" \
|
||||||
"build-tools;$BUILD_TOOLS_VERSION" \
|
"build-tools;$BUILD_TOOLS_VERSION" \
|
||||||
"platforms;android-34" \
|
"platforms;android-34" \
|
||||||
"ndk;$NDK_VERSION"
|
"ndk;$NDK_VERSION"
|
||||||
|
|
||||||
- name: Export Android environment
|
# ── Rust toolchain ─────────────────────────────────────────────────
|
||||||
run: |
|
|
||||||
echo "ANDROID_HOME=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV"
|
|
||||||
echo "ANDROID_NDK_HOME=$ANDROID_SDK_ROOT/ndk/$NDK_VERSION" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
# ── Rust toolchain ─────────────────────────────────────────────────
|
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
run: |
|
run: |
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ resources:
|
|||||||
images:
|
images:
|
||||||
- name: solitaire-server
|
- name: solitaire-server
|
||||||
newName: git.aleshym.co/funman300/solitaire-server
|
newName: git.aleshym.co/funman300/solitaire-server
|
||||||
newTag: 0f650311
|
newTag: bb670d6c
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ pub mod svg_loader;
|
|||||||
pub mod user_dir;
|
pub mod user_dir;
|
||||||
|
|
||||||
pub use sources::{
|
pub use sources::{
|
||||||
bundled_theme_url, dark_theme_svg_bytes, populate_embedded_dark_theme,
|
bundled_theme_url, classic_theme_svg_bytes, dark_theme_svg_bytes,
|
||||||
register_theme_asset_sources, AssetSourcesPlugin, DARK_THEME_MANIFEST_URL, USER_THEMES,
|
populate_embedded_classic_theme, populate_embedded_dark_theme, register_theme_asset_sources,
|
||||||
|
AssetSourcesPlugin, CLASSIC_THEME_MANIFEST_URL, DARK_THEME_MANIFEST_URL, USER_THEMES,
|
||||||
};
|
};
|
||||||
pub use svg_loader::{rasterize_svg, SvgLoader, SvgLoaderError, SvgLoaderSettings};
|
pub use svg_loader::{rasterize_svg, SvgLoader, SvgLoaderError, SvgLoaderSettings};
|
||||||
pub use user_dir::{set_user_theme_dir, user_theme_dir};
|
pub use user_dir::{set_user_theme_dir, user_theme_dir};
|
||||||
|
|||||||
@@ -78,6 +78,20 @@ const DARK_THEME_MANIFEST_PATH: &str = "solitaire_engine/assets/themes/dark/them
|
|||||||
const DARK_THEME_MANIFEST_BYTES: &[u8] =
|
const DARK_THEME_MANIFEST_BYTES: &[u8] =
|
||||||
include_bytes!("../../assets/themes/dark/theme.ron");
|
include_bytes!("../../assets/themes/dark/theme.ron");
|
||||||
|
|
||||||
|
/// Stable embedded asset URL of the bundled Classic theme manifest.
|
||||||
|
pub const CLASSIC_THEME_MANIFEST_URL: &str =
|
||||||
|
"embedded://solitaire_engine/assets/themes/classic/theme.ron";
|
||||||
|
|
||||||
|
/// Path the embedded Classic-theme manifest registers under, relative
|
||||||
|
/// to the `embedded://` source root. Kept in lockstep with
|
||||||
|
/// [`CLASSIC_THEME_MANIFEST_URL`] by the unit test
|
||||||
|
/// `classic_theme_url_constant_matches_embedded_path`.
|
||||||
|
const CLASSIC_THEME_MANIFEST_PATH: &str = "solitaire_engine/assets/themes/classic/theme.ron";
|
||||||
|
|
||||||
|
/// Bytes of the bundled Classic theme manifest, embedded at compile time.
|
||||||
|
const CLASSIC_THEME_MANIFEST_BYTES: &[u8] =
|
||||||
|
include_bytes!("../../assets/themes/classic/theme.ron");
|
||||||
|
|
||||||
/// Generates a `(stable_path, bytes)` entry for one Dark-theme SVG.
|
/// Generates a `(stable_path, bytes)` entry for one Dark-theme SVG.
|
||||||
macro_rules! embed_dark_svg {
|
macro_rules! embed_dark_svg {
|
||||||
($name:literal) => {
|
($name:literal) => {
|
||||||
@@ -88,6 +102,16 @@ macro_rules! embed_dark_svg {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates a `(stable_path, bytes)` entry for one Classic-theme SVG.
|
||||||
|
macro_rules! embed_classic_svg {
|
||||||
|
($name:literal) => {
|
||||||
|
(
|
||||||
|
concat!("solitaire_engine/assets/themes/classic/", $name),
|
||||||
|
include_bytes!(concat!("../../assets/themes/classic/", $name)) as &[u8],
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Every Dark-theme SVG file bundled into the binary.
|
/// Every Dark-theme SVG file bundled into the binary.
|
||||||
const DARK_THEME_SVGS: &[(&str, &[u8])] = &[
|
const DARK_THEME_SVGS: &[(&str, &[u8])] = &[
|
||||||
embed_dark_svg!("back.svg"),
|
embed_dark_svg!("back.svg"),
|
||||||
@@ -145,6 +169,63 @@ const DARK_THEME_SVGS: &[(&str, &[u8])] = &[
|
|||||||
embed_dark_svg!("spades_king.svg"),
|
embed_dark_svg!("spades_king.svg"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Every Classic-theme SVG file bundled into the binary.
|
||||||
|
const CLASSIC_THEME_SVGS: &[(&str, &[u8])] = &[
|
||||||
|
embed_classic_svg!("back.svg"),
|
||||||
|
embed_classic_svg!("clubs_ace.svg"),
|
||||||
|
embed_classic_svg!("clubs_2.svg"),
|
||||||
|
embed_classic_svg!("clubs_3.svg"),
|
||||||
|
embed_classic_svg!("clubs_4.svg"),
|
||||||
|
embed_classic_svg!("clubs_5.svg"),
|
||||||
|
embed_classic_svg!("clubs_6.svg"),
|
||||||
|
embed_classic_svg!("clubs_7.svg"),
|
||||||
|
embed_classic_svg!("clubs_8.svg"),
|
||||||
|
embed_classic_svg!("clubs_9.svg"),
|
||||||
|
embed_classic_svg!("clubs_10.svg"),
|
||||||
|
embed_classic_svg!("clubs_jack.svg"),
|
||||||
|
embed_classic_svg!("clubs_queen.svg"),
|
||||||
|
embed_classic_svg!("clubs_king.svg"),
|
||||||
|
embed_classic_svg!("diamonds_ace.svg"),
|
||||||
|
embed_classic_svg!("diamonds_2.svg"),
|
||||||
|
embed_classic_svg!("diamonds_3.svg"),
|
||||||
|
embed_classic_svg!("diamonds_4.svg"),
|
||||||
|
embed_classic_svg!("diamonds_5.svg"),
|
||||||
|
embed_classic_svg!("diamonds_6.svg"),
|
||||||
|
embed_classic_svg!("diamonds_7.svg"),
|
||||||
|
embed_classic_svg!("diamonds_8.svg"),
|
||||||
|
embed_classic_svg!("diamonds_9.svg"),
|
||||||
|
embed_classic_svg!("diamonds_10.svg"),
|
||||||
|
embed_classic_svg!("diamonds_jack.svg"),
|
||||||
|
embed_classic_svg!("diamonds_queen.svg"),
|
||||||
|
embed_classic_svg!("diamonds_king.svg"),
|
||||||
|
embed_classic_svg!("hearts_ace.svg"),
|
||||||
|
embed_classic_svg!("hearts_2.svg"),
|
||||||
|
embed_classic_svg!("hearts_3.svg"),
|
||||||
|
embed_classic_svg!("hearts_4.svg"),
|
||||||
|
embed_classic_svg!("hearts_5.svg"),
|
||||||
|
embed_classic_svg!("hearts_6.svg"),
|
||||||
|
embed_classic_svg!("hearts_7.svg"),
|
||||||
|
embed_classic_svg!("hearts_8.svg"),
|
||||||
|
embed_classic_svg!("hearts_9.svg"),
|
||||||
|
embed_classic_svg!("hearts_10.svg"),
|
||||||
|
embed_classic_svg!("hearts_jack.svg"),
|
||||||
|
embed_classic_svg!("hearts_queen.svg"),
|
||||||
|
embed_classic_svg!("hearts_king.svg"),
|
||||||
|
embed_classic_svg!("spades_ace.svg"),
|
||||||
|
embed_classic_svg!("spades_2.svg"),
|
||||||
|
embed_classic_svg!("spades_3.svg"),
|
||||||
|
embed_classic_svg!("spades_4.svg"),
|
||||||
|
embed_classic_svg!("spades_5.svg"),
|
||||||
|
embed_classic_svg!("spades_6.svg"),
|
||||||
|
embed_classic_svg!("spades_7.svg"),
|
||||||
|
embed_classic_svg!("spades_8.svg"),
|
||||||
|
embed_classic_svg!("spades_9.svg"),
|
||||||
|
embed_classic_svg!("spades_10.svg"),
|
||||||
|
embed_classic_svg!("spades_jack.svg"),
|
||||||
|
embed_classic_svg!("spades_queen.svg"),
|
||||||
|
embed_classic_svg!("spades_king.svg"),
|
||||||
|
];
|
||||||
|
|
||||||
/// Registers asset sources that must be in place *before*
|
/// Registers asset sources that must be in place *before*
|
||||||
/// `AssetPlugin` is built.
|
/// `AssetPlugin` is built.
|
||||||
///
|
///
|
||||||
@@ -181,6 +262,7 @@ pub struct AssetSourcesPlugin;
|
|||||||
impl Plugin for AssetSourcesPlugin {
|
impl Plugin for AssetSourcesPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
populate_embedded_dark_theme(app);
|
populate_embedded_dark_theme(app);
|
||||||
|
populate_embedded_classic_theme(app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,11 +290,44 @@ pub fn dark_theme_svg_bytes(filename: &str) -> Option<&'static [u8]> {
|
|||||||
pub fn bundled_theme_url(id: &str) -> Option<&'static str> {
|
pub fn bundled_theme_url(id: &str) -> Option<&'static str> {
|
||||||
match id {
|
match id {
|
||||||
"dark" => Some(DARK_THEME_MANIFEST_URL),
|
"dark" => Some(DARK_THEME_MANIFEST_URL),
|
||||||
"classic" => Some("themes/classic/theme.ron"),
|
"classic" => Some(CLASSIC_THEME_MANIFEST_URL),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the embedded SVG bytes for a single Classic-theme file
|
||||||
|
/// (e.g. `"back.svg"` or `"spades_ace.svg"`), or `None` when the
|
||||||
|
/// filename is not bundled.
|
||||||
|
pub fn classic_theme_svg_bytes(filename: &str) -> Option<&'static [u8]> {
|
||||||
|
let suffix = format!("/{filename}");
|
||||||
|
CLASSIC_THEME_SVGS
|
||||||
|
.iter()
|
||||||
|
.find(|(path, _)| path.ends_with(&suffix))
|
||||||
|
.map(|(_, bytes)| *bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes every bundled Classic-theme file into the
|
||||||
|
/// [`EmbeddedAssetRegistry`] under its stable URL.
|
||||||
|
pub fn populate_embedded_classic_theme(app: &mut App) {
|
||||||
|
let registry = app
|
||||||
|
.world_mut()
|
||||||
|
.get_resource_or_insert_with(EmbeddedAssetRegistry::default);
|
||||||
|
|
||||||
|
registry.insert_asset(
|
||||||
|
std::path::PathBuf::from(CLASSIC_THEME_MANIFEST_PATH),
|
||||||
|
std::path::Path::new(CLASSIC_THEME_MANIFEST_PATH),
|
||||||
|
CLASSIC_THEME_MANIFEST_BYTES,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (path, bytes) in CLASSIC_THEME_SVGS {
|
||||||
|
registry.insert_asset(
|
||||||
|
std::path::PathBuf::from(*path),
|
||||||
|
std::path::Path::new(*path),
|
||||||
|
*bytes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Pushes every bundled Dark-theme file into the
|
/// Pushes every bundled Dark-theme file into the
|
||||||
/// [`EmbeddedAssetRegistry`] under its stable URL.
|
/// [`EmbeddedAssetRegistry`] under its stable URL.
|
||||||
pub fn populate_embedded_dark_theme(app: &mut App) {
|
pub fn populate_embedded_dark_theme(app: &mut App) {
|
||||||
@@ -305,4 +420,52 @@ mod tests {
|
|||||||
.expect("dark theme URL must use embedded:// scheme");
|
.expect("dark theme URL must use embedded:// scheme");
|
||||||
assert_eq!(url_tail, DARK_THEME_MANIFEST_PATH);
|
assert_eq!(url_tail, DARK_THEME_MANIFEST_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn populate_embedded_classic_theme_runs_without_asset_plugin() {
|
||||||
|
let mut app = App::new();
|
||||||
|
populate_embedded_classic_theme(&mut app);
|
||||||
|
assert!(app
|
||||||
|
.world()
|
||||||
|
.get_resource::<EmbeddedAssetRegistry>()
|
||||||
|
.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn embedded_classic_theme_manifest_validates() {
|
||||||
|
use crate::theme::ThemeManifest;
|
||||||
|
|
||||||
|
let manifest: ThemeManifest = ron::de::from_bytes(CLASSIC_THEME_MANIFEST_BYTES)
|
||||||
|
.expect("classic manifest must parse as RON");
|
||||||
|
let faces = manifest
|
||||||
|
.validate()
|
||||||
|
.expect("classic manifest must list all 52 faces");
|
||||||
|
assert_eq!(faces.len(), 52);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classic_theme_svg_bytes_finds_back_and_ace_of_spades() {
|
||||||
|
assert!(
|
||||||
|
classic_theme_svg_bytes("back.svg").is_some(),
|
||||||
|
"classic theme must bundle a back.svg"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
classic_theme_svg_bytes("spades_ace.svg").is_some(),
|
||||||
|
"classic theme must bundle a spades_ace.svg"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classic_theme_svg_bytes_returns_none_for_unknown_file() {
|
||||||
|
assert!(classic_theme_svg_bytes("nope.svg").is_none());
|
||||||
|
assert!(classic_theme_svg_bytes("").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classic_theme_url_constant_matches_embedded_path() {
|
||||||
|
let url_tail = CLASSIC_THEME_MANIFEST_URL
|
||||||
|
.strip_prefix("embedded://")
|
||||||
|
.expect("classic theme URL must use embedded:// scheme");
|
||||||
|
assert_eq!(url_tail, CLASSIC_THEME_MANIFEST_PATH);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use bevy::prelude::*;
|
|||||||
use solitaire_core::card::{Rank, Suit};
|
use solitaire_core::card::{Rank, Suit};
|
||||||
|
|
||||||
use crate::assets::{
|
use crate::assets::{
|
||||||
bundled_theme_url, dark_theme_svg_bytes, rasterize_svg, user_theme_dir,
|
bundled_theme_url, classic_theme_svg_bytes, dark_theme_svg_bytes, rasterize_svg, user_theme_dir,
|
||||||
};
|
};
|
||||||
use crate::card_plugin::CardImageSet;
|
use crate::card_plugin::CardImageSet;
|
||||||
use crate::events::StateChangedEvent;
|
use crate::events::StateChangedEvent;
|
||||||
@@ -306,22 +306,18 @@ const PREVIEW_BACK_FILENAME: &str = "back.svg";
|
|||||||
///
|
///
|
||||||
/// - For the embedded `dark` theme, reads from the in-binary table via
|
/// - For the embedded `dark` theme, reads from the in-binary table via
|
||||||
/// [`dark_theme_svg_bytes`]. No filesystem I/O.
|
/// [`dark_theme_svg_bytes`]. No filesystem I/O.
|
||||||
/// - For bundled non-embedded themes (e.g. `classic`), reads from the
|
/// - For the embedded `classic` theme, reads from the in-binary table via
|
||||||
/// `assets/themes/<id>/` directory.
|
/// [`classic_theme_svg_bytes`]. No filesystem I/O.
|
||||||
/// - For user themes, reads from `<user_theme_dir>/<id>/<filename>`.
|
/// - For user themes, reads from `<user_theme_dir>/<id>/<filename>`.
|
||||||
/// Returns `None` for any I/O failure.
|
/// Returns `None` for any I/O failure.
|
||||||
fn read_theme_preview_svg_bytes(theme_id: &str, filename: &str) -> Option<Vec<u8>> {
|
fn read_theme_preview_svg_bytes(theme_id: &str, filename: &str) -> Option<Vec<u8>> {
|
||||||
if theme_id == "dark" {
|
if theme_id == "dark" {
|
||||||
return dark_theme_svg_bytes(filename).map(|b| b.to_vec());
|
return dark_theme_svg_bytes(filename).map(|b| b.to_vec());
|
||||||
}
|
}
|
||||||
// Bundled non-embedded themes live alongside the binary in assets/.
|
if theme_id == "classic" {
|
||||||
let bundled_path = std::path::Path::new("assets/themes")
|
return classic_theme_svg_bytes(filename).map(|b| b.to_vec());
|
||||||
.join(theme_id)
|
|
||||||
.join(filename);
|
|
||||||
if let Ok(bytes) = std::fs::read(&bundled_path) {
|
|
||||||
return Some(bytes);
|
|
||||||
}
|
}
|
||||||
// Fall back to user theme dir.
|
// User themes live in the user theme dir.
|
||||||
let path = user_theme_dir().join(theme_id).join(filename);
|
let path = user_theme_dir().join(theme_id).join(filename);
|
||||||
std::fs::read(&path).ok()
|
std::fs::read(&path).ok()
|
||||||
}
|
}
|
||||||
@@ -577,6 +573,20 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `read_theme_preview_svg_bytes` for the classic theme always returns
|
||||||
|
/// embedded bytes for the canonical preview pair.
|
||||||
|
#[test]
|
||||||
|
fn read_classic_theme_preview_returns_some_for_canonical_files() {
|
||||||
|
assert!(
|
||||||
|
read_theme_preview_svg_bytes("classic", PREVIEW_BACK_FILENAME).is_some(),
|
||||||
|
"classic theme back.svg must be embedded"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
read_theme_preview_svg_bytes("classic", PREVIEW_FACE_FILENAME).is_some(),
|
||||||
|
"classic theme spades_ace.svg must be embedded"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// `ensure_theme_thumbnails` is idempotent: calling it twice with
|
/// `ensure_theme_thumbnails` is idempotent: calling it twice with
|
||||||
/// the same registry must not regenerate or replace already-cached
|
/// the same registry must not regenerate or replace already-cached
|
||||||
/// entries. This guards against the per-frame Update tick churning
|
/// entries. This guards against the per-frame Update tick churning
|
||||||
|
|||||||
@@ -62,9 +62,9 @@ COPY --from=builder /build/target/release/solitaire_server ./server
|
|||||||
# Static web assets are served via ServeDir at runtime from these paths:
|
# Static web assets are served via ServeDir at runtime from these paths:
|
||||||
# /app/solitaire_server/web → /web route
|
# /app/solitaire_server/web → /web route
|
||||||
# /app/assets → /assets route
|
# /app/assets → /assets route
|
||||||
|
# Card themes (dark + classic) are embedded in the binary; no theme files needed here.
|
||||||
COPY solitaire_server/web ./solitaire_server/web
|
COPY solitaire_server/web ./solitaire_server/web
|
||||||
COPY assets ./assets
|
COPY assets ./assets
|
||||||
COPY solitaire_engine/assets/themes/classic ./assets/themes/classic
|
|
||||||
|
|
||||||
ENV SERVER_PORT=8080
|
ENV SERVER_PORT=8080
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|||||||
Reference in New Issue
Block a user