diff --git a/src/app.rs b/src/app.rs index 02b938f..d7af82e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,19 +10,7 @@ use tracing::{error, event}; use crate::constants::{CANVAS_SIZE, LOOP_TIME, SCALE}; use crate::game::Game; - -#[cfg(target_os = "emscripten")] -use crate::emscripten; - -#[cfg(not(target_os = "emscripten"))] -fn sleep(value: Duration) { - spin_sleep::sleep(value); -} - -#[cfg(target_os = "emscripten")] -fn sleep(value: Duration) { - emscripten::emscripten::sleep(value.as_millis() as u32); -} +use crate::platform::get_platform; pub struct App<'a> { game: Game, @@ -35,6 +23,9 @@ pub struct App<'a> { impl App<'_> { pub fn new() -> Result { + // Initialize platform-specific console + get_platform().init_console().map_err(|e| anyhow!(e))?; + let sdl_context = sdl2::init().map_err(|e| anyhow!(e))?; let video_subsystem = sdl_context.video().map_err(|e| anyhow!(e))?; let audio_subsystem = sdl_context.audio().map_err(|e| anyhow!(e))?; @@ -138,7 +129,7 @@ impl App<'_> { if start.elapsed() < LOOP_TIME { let time = LOOP_TIME.saturating_sub(start.elapsed()); if time != Duration::ZERO { - sleep(time); + get_platform().sleep(time); } } else { event!( diff --git a/src/asset.rs b/src/asset.rs index c340f28..1a1c140 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -42,40 +42,12 @@ impl Asset { } } -#[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/game/sound/waka/1.ogg")), - Asset::Wav2 => Cow::Borrowed(include_bytes!("../assets/game/sound/waka/2.ogg")), - Asset::Wav3 => Cow::Borrowed(include_bytes!("../assets/game/sound/waka/3.ogg")), - Asset::Wav4 => Cow::Borrowed(include_bytes!("../assets/game/sound/waka/4.ogg")), - Asset::Atlas => Cow::Borrowed(include_bytes!("../assets/game/atlas.png")), - Asset::AtlasJson => Cow::Borrowed(include_bytes!("../assets/game/atlas.json")), - } - }; - } - pub fn get_asset_bytes(asset: Asset) -> Result, AssetError> { - Ok(asset_bytes_enum!(asset)) - } -} + use crate::platform::get_platform; -#[cfg(target_os = "emscripten")] -mod imp { - use super::*; - use sdl2::rwops::RWops; - use std::io::Read; 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(std::io::Error::new(std::io::ErrorKind::Other, e)))?; - Ok(Cow::Owned(buf)) + get_platform().get_asset_bytes(asset) } } diff --git a/src/emscripten.rs b/src/emscripten.rs deleted file mode 100644 index b928519..0000000 --- a/src/emscripten.rs +++ /dev/null @@ -1,31 +0,0 @@ -#[allow(dead_code)] -#[cfg(target_os = "emscripten")] -pub mod emscripten { - use std::os::raw::c_uint; - - extern "C" { - pub fn emscripten_get_now() -> f64; - pub fn emscripten_sleep(ms: c_uint); - pub fn emscripten_get_element_css_size(target: *const u8, width: *mut f64, height: *mut f64) -> i32; - } - - // milliseconds since start of program - pub fn now() -> f64 { - unsafe { emscripten_get_now() } - } - - pub fn sleep(ms: u32) { - unsafe { - emscripten_sleep(ms); - } - } - - pub fn get_canvas_size() -> (u32, u32) { - let mut width = 0.0; - let mut height = 0.0; - unsafe { - emscripten_get_element_css_size("canvas\0".as_ptr(), &mut width, &mut height); - } - (width as u32, height as u32) - } -} diff --git a/src/lib.rs b/src/lib.rs index 1dc07f5..5427046 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,9 +4,9 @@ pub mod app; pub mod asset; pub mod audio; pub mod constants; -pub mod emscripten; pub mod entity; pub mod game; pub mod helpers; pub mod map; +pub mod platform; pub mod texture; diff --git a/src/main.rs b/src/main.rs index b33b802..695e3d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,59 +5,16 @@ use tracing::info; use tracing_error::ErrorLayer; use tracing_subscriber::layer::SubscriberExt; -#[cfg(windows)] -use winapi::{ - shared::ntdef::NULL, - um::{ - fileapi::{CreateFileA, OPEN_EXISTING}, - handleapi::INVALID_HANDLE_VALUE, - processenv::SetStdHandle, - winbase::{STD_ERROR_HANDLE, STD_OUTPUT_HANDLE}, - wincon::{AttachConsole, GetConsoleWindow}, - winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE}, - }, -}; - -/// Attaches the process to the parent console on Windows. -/// -/// This allows the application to print to the console when run from a terminal, -/// which is useful for debugging purposes. If the application is not run from a -/// terminal, this function does nothing. -#[cfg(windows)] -unsafe fn attach_console() { - if !std::ptr::eq(GetConsoleWindow(), std::ptr::null_mut()) { - return; - } - - if AttachConsole(winapi::um::wincon::ATTACH_PARENT_PROCESS) != 0 { - let handle = CreateFileA( - c"CONOUT$".as_ptr(), - GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - std::ptr::null_mut(), - OPEN_EXISTING, - 0, - NULL, - ); - - if handle != INVALID_HANDLE_VALUE { - SetStdHandle(STD_OUTPUT_HANDLE, handle); - SetStdHandle(STD_ERROR_HANDLE, handle); - } - } - // Do NOT call AllocConsole here - we don't want a console when launched from Explorer -} - mod app; mod asset; mod audio; mod constants; -#[cfg(target_os = "emscripten")] -mod emscripten; + mod entity; mod game; mod helpers; mod map; +mod platform; mod texture; /// The main entry point of the application. @@ -65,12 +22,6 @@ mod texture; /// This function initializes SDL, the window, the game state, and then enters /// the main game loop. pub fn main() { - // Attaches the console on Windows for debugging purposes. - #[cfg(windows)] - unsafe { - attach_console(); - } - // Setup tracing let subscriber = tracing_subscriber::fmt() .with_ansi(cfg!(not(target_os = "emscripten"))) diff --git a/src/platform/desktop.rs b/src/platform/desktop.rs new file mode 100644 index 0000000..ae00cc0 --- /dev/null +++ b/src/platform/desktop.rs @@ -0,0 +1,77 @@ +//! Desktop platform implementation. + +use std::borrow::Cow; +use std::time::Duration; + +use crate::asset::{Asset, AssetError}; +use crate::platform::{Platform, PlatformError}; + +/// Desktop platform implementation. +pub struct DesktopPlatform; + +impl Platform for DesktopPlatform { + fn sleep(&self, duration: Duration) { + spin_sleep::sleep(duration); + } + + fn get_time(&self) -> f64 { + std::time::Instant::now().elapsed().as_secs_f64() + } + + fn init_console(&self) -> Result<(), PlatformError> { + #[cfg(windows)] + { + unsafe { + use winapi::{ + shared::ntdef::NULL, + um::{ + fileapi::{CreateFileA, OPEN_EXISTING}, + handleapi::INVALID_HANDLE_VALUE, + processenv::SetStdHandle, + winbase::{STD_ERROR_HANDLE, STD_OUTPUT_HANDLE}, + wincon::{AttachConsole, GetConsoleWindow}, + winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE}, + }, + }; + + if !std::ptr::eq(GetConsoleWindow(), std::ptr::null_mut()) { + return Ok(()); + } + + if AttachConsole(winapi::um::wincon::ATTACH_PARENT_PROCESS) != 0 { + let handle = CreateFileA( + c"CONOUT$".as_ptr(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + std::ptr::null_mut(), + OPEN_EXISTING, + 0, + NULL, + ); + + if handle != INVALID_HANDLE_VALUE { + SetStdHandle(STD_OUTPUT_HANDLE, handle); + SetStdHandle(STD_ERROR_HANDLE, handle); + } + } + } + } + + Ok(()) + } + + fn get_canvas_size(&self) -> Option<(u32, u32)> { + None // Desktop doesn't need this + } + + fn get_asset_bytes(&self, asset: Asset) -> Result, AssetError> { + match asset { + Asset::Wav1 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/1.ogg"))), + Asset::Wav2 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/2.ogg"))), + Asset::Wav3 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/3.ogg"))), + Asset::Wav4 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/4.ogg"))), + Asset::Atlas => Ok(Cow::Borrowed(include_bytes!("../../assets/game/atlas.png"))), + Asset::AtlasJson => Ok(Cow::Borrowed(include_bytes!("../../assets/game/atlas.json"))), + } + } +} diff --git a/src/platform/emscripten.rs b/src/platform/emscripten.rs new file mode 100644 index 0000000..eabdc6a --- /dev/null +++ b/src/platform/emscripten.rs @@ -0,0 +1,61 @@ +//! Emscripten platform implementation. + +use std::borrow::Cow; +use std::time::Duration; + +use crate::asset::{Asset, AssetError}; +use crate::platform::{Platform, PlatformError}; + +/// Emscripten platform implementation. +pub struct EmscriptenPlatform; + +impl Platform for EmscriptenPlatform { + fn sleep(&self, duration: Duration) { + unsafe { + emscripten_sleep(duration.as_millis() as u32); + } + } + + fn get_time(&self) -> f64 { + unsafe { emscripten_get_now() } + } + + fn init_console(&self) -> Result<(), PlatformError> { + Ok(()) // No-op for Emscripten + } + + fn get_canvas_size(&self) -> Option<(u32, u32)> { + Some(unsafe { get_canvas_size() }) + } + + fn get_asset_bytes(&self, asset: Asset) -> Result, AssetError> { + use sdl2::rwops::RWops; + use std::io::Read; + + 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(std::io::Error::new(std::io::ErrorKind::Other, e)))?; + + Ok(Cow::Owned(buf)) + } +} + +// Emscripten FFI functions +extern "C" { + fn emscripten_get_now() -> f64; + fn emscripten_sleep(ms: u32); + fn emscripten_get_element_css_size(target: *const u8, width: *mut f64, height: *mut f64) -> i32; +} + +unsafe fn get_canvas_size() -> (u32, u32) { + let mut width = 0.0; + let mut height = 0.0; + emscripten_get_element_css_size("canvas\0".as_ptr(), &mut width, &mut height); + (width as u32, height as u32) +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs new file mode 100644 index 0000000..7f4e437 --- /dev/null +++ b/src/platform/mod.rs @@ -0,0 +1,58 @@ +//! Platform abstraction layer for cross-platform functionality. + +use std::borrow::Cow; +use std::time::Duration; + +use crate::asset::{Asset, AssetError}; + +pub mod desktop; +pub mod emscripten; + +/// Platform abstraction trait that defines cross-platform functionality. +pub trait Platform { + /// Sleep for the specified duration using platform-appropriate method. + fn sleep(&self, duration: Duration); + + /// Get the current time in seconds since some reference point. + /// This is available for future use in timing and performance monitoring. + #[allow(dead_code)] + fn get_time(&self) -> f64; + + /// Initialize platform-specific console functionality. + fn init_console(&self) -> Result<(), PlatformError>; + + /// Get canvas size for platforms that need it (e.g., Emscripten). + /// This is available for future use in responsive design. + #[allow(dead_code)] + fn get_canvas_size(&self) -> Option<(u32, u32)>; + + /// Load asset bytes using platform-appropriate method. + fn get_asset_bytes(&self, asset: Asset) -> Result, AssetError>; +} + +/// Platform-specific errors. +#[derive(Debug, thiserror::Error)] +#[allow(dead_code)] +pub enum PlatformError { + #[error("Console initialization failed: {0}")] + ConsoleInit(String), + #[error("Platform-specific error: {0}")] + Other(String), +} + +/// Get the current platform implementation. +#[allow(dead_code)] +pub fn get_platform() -> &'static dyn Platform { + static DESKTOP: desktop::DesktopPlatform = desktop::DesktopPlatform; + static EMSCRIPTEN: emscripten::EmscriptenPlatform = emscripten::EmscriptenPlatform; + + #[cfg(not(target_os = "emscripten"))] + { + &DESKTOP + } + + #[cfg(target_os = "emscripten")] + { + &EMSCRIPTEN + } +}