From 50afd8c09fd33b1281ae83e45064324677f2ab12 Mon Sep 17 00:00:00 2001 From: Xevion Date: Wed, 23 Jul 2025 18:02:19 -0500 Subject: [PATCH] feat: improved emscripten-compatible asset loading api --- src/asset.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/audio.rs | 20 +++++++-------- src/game.rs | 65 ++++++++++++++++++++++++----------------------- src/main.rs | 1 + 4 files changed, 115 insertions(+), 42 deletions(-) create mode 100644 src/asset.rs diff --git a/src/asset.rs b/src/asset.rs new file mode 100644 index 0000000..4b70d81 --- /dev/null +++ b/src/asset.rs @@ -0,0 +1,71 @@ +//! Cross-platform asset loading abstraction. +//! On desktop, assets are embedded using include_bytes!; on Emscripten, assets are loaded from the filesystem. + +use std::borrow::Cow; +use std::io; + +#[derive(Debug)] +pub enum AssetError { + Io(io::Error), +} + +impl From for AssetError { + fn from(e: io::Error) -> Self { + AssetError::Io(e) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Asset { + Wav1, + Wav2, + Wav3, + Wav4, + Pacman, + Pellet, + Energizer, + Map, + FontKonami, + GhostBody, + GhostEyes, + // Add more as needed +} + +#[cfg(not(target_os = "emscripten"))] +mod imp { + use super::*; + macro_rules! asset_bytes_enum { + ( $asset:expr ) => { + match $asset { + Asset::Wav1 => Cow::Borrowed(include_bytes!("../assets/wav/1.ogg")), + Asset::Wav2 => Cow::Borrowed(include_bytes!("../assets/wav/2.ogg")), + Asset::Wav3 => Cow::Borrowed(include_bytes!("../assets/wav/3.ogg")), + Asset::Wav4 => Cow::Borrowed(include_bytes!("../assets/wav/4.ogg")), + Asset::Pacman => Cow::Borrowed(include_bytes!("../assets/32/pacman.png")), + Asset::Pellet => Cow::Borrowed(include_bytes!("../assets/24/pellet.png")), + Asset::Energizer => Cow::Borrowed(include_bytes!("../assets/24/energizer.png")), + Asset::Map => Cow::Borrowed(include_bytes!("../assets/map.png")), + Asset::FontKonami => Cow::Borrowed(include_bytes!("../assets/font/konami.ttf")), + Asset::GhostBody => Cow::Borrowed(include_bytes!("../assets/32/ghost_body.png")), + Asset::GhostEyes => Cow::Borrowed(include_bytes!("../assets/32/ghost_eyes.png")), + } + }; + } + pub fn get_asset_bytes(asset: Asset) -> Result, AssetError> { + Ok(asset_bytes_enum!(asset)) + } +} + +#[cfg(target_os = "emscripten")] +mod imp { + use super::*; + use std::fs; + use std::path::Path; + pub fn get_asset_bytes(asset: Asset) -> Result, AssetError> { + let path = Path::new("assets").join(asset.path()); + let bytes = fs::read(&path)?; + Ok(Cow::Owned(bytes)) + } +} + +pub use imp::get_asset_bytes; diff --git a/src/audio.rs b/src/audio.rs index 649aa21..619e7c1 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -1,16 +1,11 @@ //! This module handles the audio playback for the game. +use crate::asset::{get_asset_bytes, Asset}; use sdl2::{ mixer::{self, Chunk, InitFlag, LoaderRWops, DEFAULT_FORMAT}, rwops::RWops, }; -const SOUND_1_DATA: &[u8] = include_bytes!("../assets/wav/1.ogg"); -const SOUND_2_DATA: &[u8] = include_bytes!("../assets/wav/2.ogg"); -const SOUND_3_DATA: &[u8] = include_bytes!("../assets/wav/3.ogg"); -const SOUND_4_DATA: &[u8] = include_bytes!("../assets/wav/4.ogg"); - -/// An array of all the sound effect data. -const SOUND_DATA: [&[u8]; 4] = [SOUND_1_DATA, SOUND_2_DATA, SOUND_3_DATA, SOUND_4_DATA]; +const SOUND_ASSETS: [Asset; 4] = [Asset::Wav1, Asset::Wav2, Asset::Wav3, Asset::Wav4]; /// The audio system for the game. /// @@ -39,13 +34,16 @@ impl Audio { let mixer_context = mixer::init(InitFlag::OGG).expect("Failed to initialize SDL2_mixer"); - let sounds: Vec = SOUND_DATA + let sounds: Vec = SOUND_ASSETS .iter() .enumerate() - .map(|(i, data)| { - let rwops = RWops::from_bytes(data) + .map(|(i, asset)| { + let data = get_asset_bytes(*asset).expect("Failed to load sound asset"); + let rwops = RWops::from_bytes(&data) .unwrap_or_else(|_| panic!("Failed to create RWops for sound {}", i + 1)); - rwops.load_wav().unwrap_or_else(|_| panic!("Failed to load sound {} from embedded data", i + 1)) + rwops + .load_wav() + .unwrap_or_else(|_| panic!("Failed to load sound {} from asset API", i + 1)) }) .collect(); diff --git a/src/game.rs b/src/game.rs index 5bb0cdd..82b874f 100644 --- a/src/game.rs +++ b/src/game.rs @@ -12,6 +12,7 @@ use sdl2::video::WindowContext; use sdl2::{pixels::Color, render::Canvas, video::Window}; use crate::animation::AtlasTexture; +use crate::asset::{get_asset_bytes, Asset}; use crate::audio::Audio; use crate::constants::RAW_BOARD; use crate::debug::{DebugMode, DebugRenderer}; @@ -22,17 +23,6 @@ use crate::ghosts::blinky::Blinky; use crate::map::Map; use crate::pacman::Pacman; -// Embed texture data directly into the executable -static PACMAN_TEXTURE_DATA: &[u8] = include_bytes!("../assets/32/pacman.png"); -static PELLET_TEXTURE_DATA: &[u8] = include_bytes!("../assets/24/pellet.png"); -static POWER_PELLET_TEXTURE_DATA: &[u8] = include_bytes!("../assets/24/energizer.png"); -static MAP_TEXTURE_DATA: &[u8] = include_bytes!("../assets/map.png"); -static FONT_DATA: &[u8] = include_bytes!("../assets/font/konami.ttf"); - -// Add ghost texture data -static GHOST_BODY_TEXTURE_DATA: &[u8] = include_bytes!("../assets/32/ghost_body.png"); -static GHOST_EYES_TEXTURE_DATA: &[u8] = include_bytes!("../assets/32/ghost_eyes.png"); - /// The main game state. /// /// This struct contains all the information necessary to run the game, including @@ -69,10 +59,11 @@ impl<'a> Game<'a> { ) -> Game<'a> { let map = Rc::new(RefCell::new(Map::new(RAW_BOARD))); - // Load Pacman texture from embedded data + // Load Pacman texture from asset API + let pacman_bytes = get_asset_bytes(Asset::Pacman).expect("Failed to load asset"); let pacman_atlas = texture_creator - .load_texture_bytes(PACMAN_TEXTURE_DATA) - .expect("Could not load pacman texture from embedded data"); + .load_texture_bytes(&pacman_bytes) + .expect("Could not load pacman texture from asset API"); let pacman = Rc::new(RefCell::new(Pacman::new( (1, 1), pacman_atlas, @@ -80,12 +71,14 @@ impl<'a> Game<'a> { ))); // Load ghost textures + let ghost_body_bytes = get_asset_bytes(Asset::GhostBody).expect("Failed to load asset"); let ghost_body = texture_creator - .load_texture_bytes(GHOST_BODY_TEXTURE_DATA) - .expect("Could not load ghost body texture from embedded data"); + .load_texture_bytes(&ghost_body_bytes) + .expect("Could not load ghost body texture from asset API"); + let ghost_eyes_bytes = get_asset_bytes(Asset::GhostEyes).expect("Failed to load asset"); let ghost_eyes = texture_creator - .load_texture_bytes(GHOST_EYES_TEXTURE_DATA) - .expect("Could not load ghost eyes texture from embedded data"); + .load_texture_bytes(&ghost_eyes_bytes) + .expect("Could not load ghost eyes texture from asset API"); // Create Blinky let blinky = Blinky::new( @@ -96,38 +89,48 @@ impl<'a> Game<'a> { Rc::clone(&pacman), ); - // Load pellet texture from embedded data + // Load pellet texture from asset API + let pellet_bytes = get_asset_bytes(Asset::Pellet).expect("Failed to load asset"); let pellet_texture = Rc::new(AtlasTexture::new( texture_creator - .load_texture_bytes(PELLET_TEXTURE_DATA) - .expect("Could not load pellet texture from embedded data"), + .load_texture_bytes(&pellet_bytes) + .expect("Could not load pellet texture from asset API"), 1, 24, 24, None, )); + let power_pellet_bytes = get_asset_bytes(Asset::Energizer).expect("Failed to load asset"); let power_pellet_texture = Rc::new(AtlasTexture::new( texture_creator - .load_texture_bytes(POWER_PELLET_TEXTURE_DATA) - .expect("Could not load power pellet texture from embedded data"), + .load_texture_bytes(&power_pellet_bytes) + .expect("Could not load power pellet texture from asset API"), 1, 24, 24, None, )); - // Load font from embedded data - let font_rwops = RWops::from_bytes(FONT_DATA).expect("Failed to create RWops for font"); - let font = ttf_context - .load_font_from_rwops(font_rwops, 24) - .expect("Could not load font from embedded data"); + // Load font from asset API + let font = { + let font_bytes = get_asset_bytes(Asset::FontKonami) + .expect("Failed to load asset") + .into_owned(); + let font_bytes_static: &'static [u8] = Box::leak(font_bytes.into_boxed_slice()); + let font_rwops = + RWops::from_bytes(font_bytes_static).expect("Failed to create RWops for font"); + ttf_context + .load_font_from_rwops(font_rwops, 24) + .expect("Could not load font from asset API") + }; let audio = Audio::new(); - // Load map texture from embedded data + // Load map texture from asset API + let map_bytes = get_asset_bytes(Asset::Map).expect("Failed to load asset"); let mut map_texture = texture_creator - .load_texture_bytes(MAP_TEXTURE_DATA) - .expect("Could not load map texture from embedded data"); + .load_texture_bytes(&map_bytes) + .expect("Could not load map texture from asset API"); map_texture.set_color_mod(0, 0, 255); let edibles = reconstruct_edibles( diff --git a/src/main.rs b/src/main.rs index b71de0e..ae46e7b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -66,6 +66,7 @@ mod helper; mod map; mod modulation; mod pacman; +mod asset; /// The main entry point of the application. ///