From 00a65954e63400609bdada679e5175e601644676 Mon Sep 17 00:00:00 2001 From: Ryan Walters Date: Thu, 11 Sep 2025 01:11:00 -0500 Subject: [PATCH] refactor: unify cross-platform asset loading, avoid hard-coding with folder-based asset embedding for desktop --- Cargo.lock | 136 ++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 +- src/asset.rs | 54 ++++++++++----- src/audio.rs | 11 +-- src/error.rs | 2 - src/game.rs | 6 +- src/platform/desktop.rs | 29 ++------ src/platform/emscripten.rs | 19 +----- tests/common.rs | 4 +- 9 files changed, 193 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d0f5dd..a3dd92a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,6 +228,15 @@ dependencies = [ "serde", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -268,6 +277,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -289,6 +307,16 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "deprecate-until" version = "0.1.1" @@ -337,6 +365,16 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "disqualified" version = "1.0.0" @@ -408,6 +446,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -663,7 +711,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "pacman" -version = "0.79.2" +version = "0.79.3" dependencies = [ "anyhow", "bevy_ecs", @@ -678,6 +726,7 @@ dependencies = [ "phf", "pretty_assertions", "rand", + "rust-embed", "sdl2", "serde", "serde_json", @@ -907,6 +956,40 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rust-embed" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -925,6 +1008,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -994,6 +1086,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -1253,6 +1356,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicode-ident" version = "1.0.11" @@ -1306,6 +1415,22 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" @@ -1396,6 +1521,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.0", +] + [[package]] name = "windows" version = "0.62.0" diff --git a/Cargo.toml b/Cargo.toml index 6169366..798c8f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pacman" -version = "0.79.2" +version = "0.79.3" authors = ["Xevion"] edition = "2021" rust-version = "1.86.0" @@ -50,6 +50,7 @@ windows-sys = { version = "0.61.0", features = ["Win32_System_Console"] } # On desktop platforms, build SDL2 with cargo-vcpkg sdl2 = { version = "0.38", default-features = false, features = ["image", "ttf", "gfx", "mixer", "unsafe_textures", "static-link", "use-vcpkg"] } rand = { version = "0.9.2", default-features = false, features = ["thread_rng"] } +rust-embed = "8.7.2" spin_sleep = "1.3.3" # Browser-specific dependencies diff --git a/src/asset.rs b/src/asset.rs index ef7ebf8..0d595bd 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -5,6 +5,7 @@ use std::borrow::Cow; use std::iter; use crate::audio::Sound; +use crate::error::AssetError; /// Enumeration of all game assets with cross-platform loading support. /// @@ -66,7 +67,6 @@ impl Asset { /// Paths are consistent across platforms and used by the Emscripten backend /// for filesystem loading. Desktop builds embed assets directly and don't /// use these paths at runtime. - #[allow(dead_code)] pub fn path(&self) -> &str { use Asset::*; match self { @@ -84,34 +84,54 @@ impl Asset { Font => "TerminalVector.ttf", } } -} - -mod imp { - use super::*; - use crate::error::AssetError; - use crate::platform; - use tracing::trace; /// Loads asset bytes using the appropriate platform-specific method. /// - /// On desktop platforms, returns embedded compile-time data via `include_bytes!`. + /// On desktop platforms, returns embedded compile-time data via `rust-embed`. /// On Emscripten, loads from the filesystem using the asset's path. The returned /// `Cow` allows zero-copy access to embedded data while supporting owned data /// when loaded from disk. /// /// # Errors /// - /// Returns `AssetError::NotFound` if the asset file cannot be located (Emscripten only), + /// Returns `AssetError::NotFound` if the asset file cannot be located, /// or `AssetError::Io` for filesystem I/O failures. - pub fn get_asset_bytes(asset: Asset) -> Result, AssetError> { - trace!(asset = ?asset, "Loading game asset"); - let result = platform::get_asset_bytes(asset); + pub fn get_bytes(&self) -> Result, AssetError> { + use tracing::trace; + trace!(asset = ?self, "Loading game asset"); + let result = self.get_bytes_platform(); match &result { - Ok(bytes) => trace!(asset = ?asset, size_bytes = bytes.len(), "Asset loaded successfully"), - Err(e) => trace!(asset = ?asset, error = ?e, "Asset loading failed"), + Ok(bytes) => trace!(asset = ?self, size_bytes = bytes.len(), "Asset loaded successfully"), + Err(e) => trace!(asset = ?self, error = ?e, "Asset loading failed"), } result } -} -pub use imp::get_asset_bytes; + #[cfg(not(target_os = "emscripten"))] + fn get_bytes_platform(&self) -> Result, AssetError> { + #[derive(rust_embed::Embed)] + #[folder = "assets/game/"] + struct EmbeddedAssets; + + let path = self.path(); + EmbeddedAssets::get(path) + .map(|file| file.data) + .ok_or_else(|| AssetError::NotFound(path.to_string())) + } + + #[cfg(target_os = "emscripten")] + fn get_bytes_platform(&self) -> Result, AssetError> { + use sdl2::rwops::RWops; + use std::io::{self, Read}; + + let path = format!("assets/game/{}", self.path()); + let mut rwops = RWops::from_file(&path, "rb").map_err(|_| AssetError::NotFound(self.path().to_string()))?; + + let len = rwops.len().ok_or_else(|| AssetError::NotFound(self.path().to_string()))?; + + let mut buf = vec![0u8; len]; + rwops.read_exact(&mut buf).map_err(|e| AssetError::Io(io::Error::other(e)))?; + + Ok(Cow::Owned(buf)) + } +} diff --git a/src/audio.rs b/src/audio.rs index f7fe4ee..8957e95 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -1,7 +1,7 @@ //! This module handles the audio playback for the game. use std::collections::HashMap; -use crate::asset::{get_asset_bytes, Asset}; +use crate::asset::Asset; use sdl2::{ mixer::{self, Chunk, InitFlag, LoaderRWops, AUDIO_S16LSB, DEFAULT_CHANNELS}, rwops::RWops, @@ -117,12 +117,13 @@ impl Audio { // Try to load sounds, but don't panic if any fail let mut sounds = HashMap::new(); - for (i, asset) in Sound::iter().enumerate() { - match get_asset_bytes(Asset::SoundFile(asset)) { + for (i, sound_type) in Sound::iter().enumerate() { + let asset = Asset::SoundFile(sound_type); + match asset.get_bytes() { Ok(data) => match RWops::from_bytes(&data) { Ok(rwops) => match rwops.load_wav() { Ok(chunk) => { - sounds.insert(asset, chunk); + sounds.insert(sound_type, chunk); } Err(e) => { tracing::warn!("Failed to load sound {} from asset API: {}", i + 1, e); @@ -138,7 +139,7 @@ impl Audio { } } - let death_sound = match get_asset_bytes(Asset::SoundFile(Sound::PacmanDeath)) { + let death_sound = match Asset::SoundFile(Sound::PacmanDeath).get_bytes() { Ok(data) => match RWops::from_bytes(&data) { Ok(rwops) => match rwops.load_wav() { Ok(chunk) => Some(chunk), diff --git a/src/error.rs b/src/error.rs index a1f063b..ca6df6c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -46,8 +46,6 @@ pub enum AssetError { #[error("IO error: {0}")] Io(#[from] io::Error), - // This error is only possible on Emscripten, as the assets are loaded from a 'filesystem' of sorts (while on Desktop, they are included in the binary at compile time) - #[allow(dead_code)] #[error("Asset not found: {0}")] NotFound(String), } diff --git a/src/game.rs b/src/game.rs index 1b5efe2..d42c0ca 100644 --- a/src/game.rs +++ b/src/game.rs @@ -40,7 +40,7 @@ use sdl2::video::{Window, WindowContext}; use sdl2::EventPump; use crate::{ - asset::{get_asset_bytes, Asset}, + asset::Asset, events::GameCommand, map::render::MapRenderer, systems::{BatchedLinesResource, Bindings, CursorPosition, TtfAtlasResource}, @@ -247,7 +247,7 @@ impl Game { debug_texture.set_blend_mode(BlendMode::Blend); debug_texture.set_scale_mode(ScaleMode::Nearest); - let font_data: &'static [u8] = get_asset_bytes(Asset::Font)?.to_vec().leak(); + let font_data: &'static [u8] = Asset::Font.get_bytes()?.to_vec().leak(); let font_asset = RWops::from_bytes(font_data).map_err(|_| GameError::Sdl("Failed to load font".to_string()))?; let debug_font = ttf_context .load_font_from_rwops(font_asset, constants::ui::DEBUG_FONT_SIZE) @@ -261,7 +261,7 @@ impl Game { fn load_atlas_and_map_tiles(texture_creator: &TextureCreator) -> GameResult<(SpriteAtlas, Vec)> { trace!("Loading atlas image from embedded assets"); - let atlas_bytes = get_asset_bytes(Asset::AtlasImage)?; + let atlas_bytes = Asset::AtlasImage.get_bytes()?; let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| { if e.to_string().contains("format") || e.to_string().contains("unsupported") { GameError::Texture(crate::error::TextureError::InvalidFormat(format!( diff --git a/src/platform/desktop.rs b/src/platform/desktop.rs index addb3a7..e1ee2c8 100644 --- a/src/platform/desktop.rs +++ b/src/platform/desktop.rs @@ -1,13 +1,15 @@ //! Desktop platform implementation. -use std::borrow::Cow; use std::time::Duration; use rand::rngs::ThreadRng; +use rust_embed::Embed; -use crate::asset::Asset; -use crate::audio::Sound; -use crate::error::{AssetError, PlatformError}; +use crate::error::PlatformError; + +#[derive(Embed)] +#[folder = "assets/game/"] +struct EmbeddedAssets; /// Desktop platform implementation. pub fn sleep(duration: Duration, focused: bool) { @@ -58,25 +60,6 @@ pub fn init_console() -> Result<(), PlatformError> { Ok(()) } -pub fn get_asset_bytes(asset: Asset) -> Result, AssetError> { - match asset { - Asset::SoundFile(sound) => match sound { - Sound::Waka(0) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/1.ogg"))), - Sound::Waka(1) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/2.ogg"))), - Sound::Waka(2) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/3.ogg"))), - Sound::Waka(3..=u8::MAX) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/4.ogg"))), - Sound::PacmanDeath => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/death.ogg"))), - Sound::ExtraLife => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/extra_life.ogg"))), - Sound::Fruit => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/fruit.ogg"))), - Sound::Ghost => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/ghost.ogg"))), - Sound::Beginning => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/begin.ogg"))), - Sound::Intermission => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/intermission.ogg"))), - }, - Asset::AtlasImage => Ok(Cow::Borrowed(include_bytes!("../../assets/game/atlas.png"))), - Asset::Font => Ok(Cow::Borrowed(include_bytes!("../../assets/game/TerminalVector.ttf"))), - } -} - pub fn rng() -> ThreadRng { rand::rng() } diff --git a/src/platform/emscripten.rs b/src/platform/emscripten.rs index d4c4574..06e6017 100644 --- a/src/platform/emscripten.rs +++ b/src/platform/emscripten.rs @@ -1,13 +1,10 @@ //! Emscripten platform implementation. -use crate::asset::Asset; -use crate::error::{AssetError, PlatformError}; +use crate::error::PlatformError; use crate::formatter::CustomFormatter; use rand::{rngs::SmallRng, SeedableRng}; -use sdl2::rwops::RWops; -use std::borrow::Cow; use std::ffi::CString; -use std::io::{self, Read, Write}; +use std::io::{self, Write}; use std::time::Duration; // Emscripten FFI functions @@ -62,18 +59,6 @@ impl Write for EmscriptenConsoleWriter { } } -pub fn get_asset_bytes(asset: Asset) -> Result, AssetError> { - let path = format!("assets/game/{}", asset.path()); - let mut rwops = RWops::from_file(&path, "rb").map_err(|_| AssetError::NotFound(asset.path().to_string()))?; - - let len = rwops.len().ok_or_else(|| AssetError::NotFound(asset.path().to_string()))?; - - let mut buf = vec![0u8; len]; - rwops.read_exact(&mut buf).map_err(|e| AssetError::Io(io::Error::other(e)))?; - - Ok(Cow::Owned(buf)) -} - pub fn rng() -> SmallRng { SmallRng::from_os_rng() } diff --git a/tests/common.rs b/tests/common.rs index d114742..4f69c31 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -3,7 +3,7 @@ use bevy_ecs::{entity::Entity, event::Events, schedule::Schedule, world::World}; use glam::{U16Vec2, Vec2}; use pacman::{ - asset::{get_asset_bytes, Asset}, + asset::Asset, constants::RAW_BOARD, events::{CollisionTrigger, GameEvent}, game::ATLAS_FRAMES, @@ -43,7 +43,7 @@ pub fn setup_sdl() -> Result<(Canvas, TextureCreator, Sdl pub fn create_atlas(canvas: &mut sdl2::render::Canvas) -> SpriteAtlas { let texture_creator = canvas.texture_creator(); - let atlas_bytes = get_asset_bytes(Asset::AtlasImage).unwrap(); + let atlas_bytes = Asset::AtlasImage.get_bytes().unwrap(); let texture = texture_creator.load_texture_bytes(&atlas_bytes).unwrap();