diff --git a/Cargo.lock b/Cargo.lock index 9e4a5e16..fdc76626 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -362,7 +362,7 @@ dependencies = [ [[package]] name = "cargo-playdate" -version = "0.3.1" +version = "0.3.2" dependencies = [ "anyhow", "byteorder", @@ -2507,7 +2507,7 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "playdate" -version = "0.1.0" +version = "0.1.1" dependencies = [ "playdate-controls", "playdate-display", @@ -2579,7 +2579,7 @@ dependencies = [ [[package]] name = "playdate-display" -version = "0.1.3" +version = "0.2.0" dependencies = [ "playdate-sys", ] @@ -2593,7 +2593,7 @@ dependencies = [ [[package]] name = "playdate-graphics" -version = "0.2.2" +version = "0.2.3" dependencies = [ "playdate-color", "playdate-fs", @@ -2609,7 +2609,7 @@ dependencies = [ [[package]] name = "playdate-sound" -version = "0.1.4" +version = "0.1.5" dependencies = [ "playdate-fs", "playdate-sys", @@ -2625,7 +2625,7 @@ dependencies = [ [[package]] name = "playdate-sys" -version = "0.1.10" +version = "0.1.11" dependencies = [ "heapless", "playdate-bindgen", @@ -2634,7 +2634,7 @@ dependencies = [ [[package]] name = "playdate-system" -version = "0.1.2" +version = "0.2.0" dependencies = [ "playdate-sys", ] diff --git a/Cargo.toml b/Cargo.toml index 1a02a405..1d7de7ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,13 +21,13 @@ repository = "https://github.com/boozook/playdate.git" [workspace.dependencies] color = { version = "0.1", path = "api/color", package = "playdate-color", default-features = false } ctrl = { version = "0.1", path = "api/ctrl", package = "playdate-controls", default-features = false } -display = { version = "0.1", path = "api/display", package = "playdate-display", default-features = false } +display = { version = "0.2", path = "api/display", package = "playdate-display", default-features = false } fs = { version = "0.1", path = "api/fs", package = "playdate-fs", default-features = false } gfx = { version = "0.2", path = "api/gfx", package = "playdate-graphics", default-features = false } menu = { version = "0.1", path = "api/menu", package = "playdate-menu", default-features = false } sound = { version = "0.1", path = "api/sound", package = "playdate-sound", default-features = false } sprite = { version = "0.1", path = "api/sprite", package = "playdate-sprite", default-features = false } -system = { version = "0.1", path = "api/system", package = "playdate-system", default-features = false } +system = { version = "0.2", path = "api/system", package = "playdate-system", default-features = false } sys = { version = "0.1", path = "api/sys", package = "playdate-sys", default-features = false } build = { version = "0.2", path = "support/build", package = "playdate-build", default-features = false } diff --git a/api/display/Cargo.toml b/api/display/Cargo.toml index 5ca706bc..e20d85b5 100644 --- a/api/display/Cargo.toml +++ b/api/display/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "playdate-display" -version = "0.1.3" +version = "0.2.0" readme = "README.md" description = "High-level Display API built on-top of Playdate API" keywords = ["playdate", "sdk", "api", "gamedev"] diff --git a/api/display/src/lib.rs b/api/display/src/lib.rs index 7f716b9e..a2a41540 100644 --- a/api/display/src/lib.rs +++ b/api/display/src/lib.rs @@ -1,7 +1,6 @@ #![cfg_attr(not(test), no_std)] extern crate sys; -use core::ffi::c_char; use core::ffi::c_float; use core::ffi::c_int; use core::ffi::c_uint; @@ -10,6 +9,14 @@ use core::ffi::c_uint; #[derive(Debug, Clone, Copy)] pub struct Display(Api); +impl Display { + /// Creates default [`Display`] without type parameter requirement. + /// + /// Uses ZST [`api::Default`]. + #[allow(non_snake_case)] + pub fn Default() -> Self { Self(Default::default()) } +} + impl Default for Display { fn default() -> Self { Self(Default::default()) } } @@ -23,7 +30,24 @@ impl Display { } +impl Display { + pub const COLUMNS: u32 = sys::ffi::LCD_COLUMNS; + pub const ROWS: u32 = sys::ffi::LCD_ROWS; + pub const ROW_SIZE: u32 = sys::ffi::LCD_ROWSIZE; + pub const SCREEN_RECT: sys::ffi::LCDRect = sys::ffi::LCDRect { left: 0, + right: 0, + top: Self::COLUMNS as _, + bottom: Self::ROWS as _ }; +} + + impl Display { + /// Returns the width of the display, taking the current scale into account; + /// + /// e.g., if the scale is `2`, this function returns `200` instead of `400`. + /// + /// See also [`Display::COLUMNS`]. + /// /// Equivalent to [`sys::ffi::playdate_display::getWidth`] #[doc(alias = "sys::ffi::playdate_display::getWidth")] pub fn width(&self) -> c_int { @@ -31,6 +55,12 @@ impl Display { unsafe { f() } } + /// Returns the height of the display, taking the current scale into account; + /// + /// e.g., if the scale is `2`, this function returns `120` instead of `240`. + /// + /// See also [`Display::ROWS`] and [`Display::ROW_SIZE`]. + /// /// Equivalent to [`sys::ffi::playdate_display::getHeight`] #[doc(alias = "sys::ffi::playdate_display::getHeight")] pub fn height(&self) -> c_int { @@ -38,6 +68,10 @@ impl Display { unsafe { f() } } + /// Sets the nominal refresh rate in frames per second. + /// + /// Default is 20 fps, the maximum rate supported by the hardware for full-frame updates. + /// /// Equivalent to [`sys::ffi::playdate_display::setRefreshRate`] #[doc(alias = "sys::ffi::playdate_display::setRefreshRate")] pub fn set_refresh_rate(&self, rate: c_float) { @@ -45,6 +79,8 @@ impl Display { unsafe { f(rate) } } + /// If `value` is `true`, the frame buffer is drawn inverted—black instead of white, and vice versa. + /// /// Equivalent to [`sys::ffi::playdate_display::setInverted`] #[doc(alias = "sys::ffi::playdate_display::setInverted")] pub fn set_inverted(&self, value: bool) { @@ -52,13 +88,37 @@ impl Display { unsafe { f(value as _) } } + /// Sets the display scale factor. + /// + /// The top-left corner of the frame buffer is scaled up to fill the display; + /// + /// e.g., if the scale is set to [`DisplayScale::Quad`], + /// the pixels in rectangle `[0, 100] x [0, 60]` are drawn on the screen as `4 x 4` squares. + /// + /// Equivalent to [`sys::ffi::playdate_display::setScale`] + #[doc(alias = "sys::ffi::playdate_display::setScale")] + pub fn set_scale(&self, scale: DisplayScale) { self.set_scale_raw(scale.into()); } + + /// Sets the display scale factor. + /// + /// Valid values for `scale` are `1`, `2`, `4`, and `8`. + /// + /// The top-left corner of the frame buffer is scaled up to fill the display; + /// e.g., if the scale is set to `4`, the pixels in rectangle `[0, 100] x [0, 60]` are drawn on the screen as `4 x 4` squares. + /// + /// See also [`Display::set_scale`]. + /// /// Equivalent to [`sys::ffi::playdate_display::setScale`] #[doc(alias = "sys::ffi::playdate_display::setScale")] - pub fn set_scale(&self, scale: c_uint) { + pub fn set_scale_raw(&self, scale: c_uint) { let f = self.0.set_scale(); unsafe { f(scale) } } + /// Adds a mosaic effect to the display. + /// + /// Valid `x` and `y` values are between `0` and `3`, inclusive. + /// /// Equivalent to [`sys::ffi::playdate_display::setMosaic`] #[doc(alias = "sys::ffi::playdate_display::setMosaic")] pub fn set_mosaic(&self, x: c_uint, y: c_uint) { @@ -66,6 +126,8 @@ impl Display { unsafe { f(x, y) } } + /// Flips the display on the `x` or `y` axis, or both. + /// /// Equivalent to [`sys::ffi::playdate_display::setFlipped`] #[doc(alias = "sys::ffi::playdate_display::setFlipped")] pub fn set_flipped(&self, x: bool, y: bool) { @@ -73,6 +135,12 @@ impl Display { unsafe { f(x as _, y as _) } } + /// Offsets the display by the given amount. + /// + /// Areas outside of the displayed area are filled with the current background color. + /// + /// See also [`playdate-graphics::set_background_color`]. + /// /// Equivalent to [`sys::ffi::playdate_display::setOffset`] #[doc(alias = "sys::ffi::playdate_display::setOffset")] pub fn set_offset(&self, x: c_int, y: c_int) { @@ -82,8 +150,22 @@ impl Display { } +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DisplayScale { + Normal = 1, + Double = 2, + Quad = 4, + Eight = 8, +} + +impl Into for DisplayScale { + #[inline(always)] + fn into(self) -> c_uint { (self as u8).into() } +} + + pub mod api { - use core::ffi::c_char; use core::ffi::c_float; use core::ffi::c_int; use core::ffi::c_uint; diff --git a/api/gfx/Cargo.toml b/api/gfx/Cargo.toml index e3ec485e..a96d3cee 100644 --- a/api/gfx/Cargo.toml +++ b/api/gfx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "playdate-graphics" -version = "0.2.2" +version = "0.2.3" readme = "README.md" description = "High-level graphics API built on-top of Playdate API" keywords = ["playdate", "sdk", "api", "gamedev"] diff --git a/api/gfx/examples/font.rs b/api/gfx/examples/font.rs index d0f4a68e..91c5e8dd 100644 --- a/api/gfx/examples/font.rs +++ b/api/gfx/examples/font.rs @@ -98,13 +98,13 @@ impl State { for (i, code) in RUST.into_iter().enumerate() { let mut advance = 0; let (glyph, bitmap_ref) = text::get_page_glyph_with_bitmap(&page, code, &mut advance).unwrap(); - let mut char = bitmap_ref.into_bitmap(); let kern = RUST.get(i + 1) .map(|next| text::get_glyph_kerning(&glyph, code, *next)) .unwrap_or_default(); - let w = char.bitmap_data().map(|bd| bd.width).unwrap(); + let char = bitmap_ref.into_bitmap(); + let w = char.size().map(|(w, h)| w).unwrap(); let x = OFFSET + i as i32 * w; let y = OFFSET + kern; diff --git a/api/gfx/src/bitmap/bitmap.rs b/api/gfx/src/bitmap/bitmap.rs index 593c617e..8d18de3d 100644 --- a/api/gfx/src/bitmap/bitmap.rs +++ b/api/gfx/src/bitmap/bitmap.rs @@ -243,6 +243,32 @@ impl Bitmap { } + /// Returns `(width, height)` of the bitmap. + /// + /// Can return error if there is no bitmap-data or any internal error occurred. + /// + /// Calls [`sys::ffi::playdate_graphics::getBitmapData`]. + #[doc(alias = "sys::ffi::playdate_graphics::getBitmapData")] + pub fn size(&self) -> Result<(c_int, c_int), Error> { + let mut width: c_int = 0; + let mut height: c_int = 0; + let mut row_bytes: c_int = 0; + + let f = self.1.get_bitmap_data(); + unsafe { + f( + self.0, + &mut width, + &mut height, + &mut row_bytes, + core::ptr::null_mut(), + core::ptr::null_mut(), + ) + }; + + Ok((width, height)) + } + /// Returns mutable borrow of bitmap-data by this bitmap. /// /// Calls [`sys::ffi::playdate_graphics::getBitmapData`]. diff --git a/api/gfx/src/text.rs b/api/gfx/src/text.rs index 7fdae419..5e8d7bbd 100644 --- a/api/gfx/src/text.rs +++ b/api/gfx/src/text.rs @@ -53,13 +53,10 @@ pub fn draw_text_cstr(text: &CStr, encoding: StringEncoding, x: c_int, y: c_int) /// /// Equivalent to [`sys::ffi::playdate_graphics::getTextWidth`]. #[doc(alias = "sys::ffi::playdate_graphics::getTextWidth")] -pub fn get_text_width>(text: S, - font: Option<&LCDFont>, - tracking: c_int) - -> Result { +pub fn get_text_width>(text: S, font: Option<&Font>, tracking: c_int) -> Result { let s = CString::new(text.as_ref())?; let f = *sys::api!(graphics.getTextWidth); - let font = font.map(|font| font as *const LCDFont as *mut LCDFont) + let font = font.map(|font| unsafe { font.as_raw() }) .unwrap_or(core::ptr::null_mut()); let res = unsafe { f( diff --git a/api/playdate/Cargo.toml b/api/playdate/Cargo.toml index d1cf98d8..5085859f 100644 --- a/api/playdate/Cargo.toml +++ b/api/playdate/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "playdate" -version = "0.1.0" +version = "0.1.1" readme = "README.md" description = "High-level Playdate API" keywords = ["playdate", "sdk", "api", "gamedev"] @@ -36,51 +36,14 @@ default = [ "sound/default", "sprite/default", "system/default", + "entry-point", ] -lang-items = [ - "sys/lang-items", - "ctrl/lang-items", - "display/lang-items", - "fs/lang-items", - "gfx/lang-items", - "menu/lang-items", - "sound/lang-items", - "sprite/lang-items", - "system/lang-items", -] -allocator = [ - "sys/allocator", - "ctrl/allocator", - "display/allocator", - "fs/allocator", - "gfx/allocator", - "menu/allocator", - "sound/allocator", - "sprite/allocator", - "system/allocator", -] -panic-handler = [ - "sys/panic-handler", - "ctrl/panic-handler", - "display/panic-handler", - "fs/panic-handler", - "gfx/panic-handler", - "menu/panic-handler", - "sound/panic-handler", - "sprite/panic-handler", - "system/panic-handler", -] -eh-personality = [ - "sys/eh-personality", - "ctrl/eh-personality", - "display/eh-personality", - "fs/eh-personality", - "gfx/eh-personality", - "menu/eh-personality", - "sound/eh-personality", - "sprite/eh-personality", - "system/eh-personality", -] + +lang-items = ["sys/lang-items"] +allocator = ["sys/allocator"] +panic-handler = ["sys/panic-handler"] +eh-personality = ["sys/eh-personality"] +entry-point = ["sys/entry-point", "ctrl/entry-point", "system/entry-point"] error-ctx = [ "sys/error-ctx", "ctrl/error-ctx", @@ -226,6 +189,27 @@ bindings-documentation = [ ] +[[example]] +name = "hello-world" +crate-type = ["dylib", "staticlib"] +path = "examples/hello-world.rs" +required-features = ["entry-point"] + +# TODO: REMOVE ME! +[[example]] +name = "minimal" +crate-type = ["dylib", "staticlib"] +path = "examples/minimal.rs" +required-features = ["entry-point"] + +[package.metadata.playdate] +bundle-id = "rs.playdate.core" + +[package.metadata.playdate.dev-assets] +"examples/ferris.png" = true +"examples/" = "${PLAYDATE_SDK_PATH}/Examples/Level 1-1/Source/sfx/main_theme.wav" + + [package.metadata.docs.rs] all-features = false features = [ diff --git a/api/playdate/README.md b/api/playdate/README.md index d440e3a9..3e281263 100644 --- a/api/playdate/README.md +++ b/api/playdate/README.md @@ -2,10 +2,9 @@ Mostly high-level rusty API for the [Playdate handheld gaming system][playdate-website]. -Usage with [cargo-playdate][cargo-playdate-crate] is strongly recommended. +Usage with [cargo-playdate][cargo-playdate] is strongly recommended. -[cargo-playdate-crate]: https://crates.io/crates/cargo-playdate [playdate-website]: https://play.date/ @@ -31,14 +30,29 @@ Plus some extensions to make it all more rust-ish. - json - lua -Also __there is no default entry-point__ (read as event-handler) for your application. -Not yet, I'm working on it. - ## How to start -Currently there is no any beautiful HL examples ready yet. -It will be in v0.2.0 or little bit earlier as well as default entry-point that mentioned above. +Look at the [examples][gh-playdate-examples]. + + +## Examples + +[Here is available examples][gh-playdate-examples]. +You car run it with following command: + +```bash +# Simulator: +cargo playdate run -p=playdate --example=hello-world --features=entry-point +# Device: +cargo playdate run -p=playdate --example=hello-world --features=entry-point --device +``` + +More information how to use [cargo-playdate][] in help: `cargo playdate --help`. + + +[gh-playdate-examples]: https://github.com/boozook/playdate/tree/main/api/gfx/examples + ### Prerequisites @@ -50,15 +64,10 @@ Follow the instructions for: ### Hello World -> Note, this is incomplete and not-so production-ready crate. -> -> As minimum currently you'll need to implement missed entry-point for your program by yourself. -> But you can find some examples with ugly & primitive entry-point impl, -> so you'll need to add something like [this code][ugly-entry-point] to your program. -> -> Here is [list of missed parts that not ready yet](#not-yet-covered-parts). +There is multiple ways to learn and start. -[ugly-entry-point]: https://github.com/boozook/playdate/blob/main/api/sys/examples/hello-world.rs#L97-L138 +Following two is just a quick introduction. +In details it all will be explained soon in the wiki. #### Short Way @@ -83,8 +92,6 @@ Just run `cargo new ` and add do following: 1. Help this project somehow. -❤️‍🔥 - [sprite-examples]: https://github.com/boozook/playdate/tree/main/api/sprite/examples [cargo-playdate]: https://crates.io/crates/cargo-playdate @@ -93,4 +100,6 @@ Just run `cargo new ` and add do following: - - - +Made with ❤️‍🔥 by [my](https://a.koz.world). + This software is not sponsored or supported by Panic. diff --git a/api/playdate/examples/ferris.png b/api/playdate/examples/ferris.png new file mode 100644 index 00000000..55cbef35 Binary files /dev/null and b/api/playdate/examples/ferris.png differ diff --git a/api/playdate/examples/hello-world.rs b/api/playdate/examples/hello-world.rs new file mode 100644 index 00000000..8854b2e8 --- /dev/null +++ b/api/playdate/examples/hello-world.rs @@ -0,0 +1,215 @@ +#![no_std] +extern crate alloc; + +#[macro_use] +extern crate sys; +extern crate playdate as pd; + +use core::ffi::*; +use core::ptr::NonNull; + +use pd::sys::ffi::PlaydateAPI; +use display::Display; +use gfx::*; +use gfx::text::*; +use gfx::bitmap::Bitmap; +use gfx::bitmap::Color; +use fs::Path; + +use system::prelude::*; +use sound::prelude::*; +use player::*; +use sample::Sample; + + +const CENTER_X: u32 = Display::COLUMNS / 2; +const CENTER_Y: u32 = Display::ROWS / 2; + +const TEXT_HEIGHT: u32 = 20; +const FONT_PATH: &Path = "/System/Fonts/Asheville-Sans-14-Bold.pft"; + +const BOX_WIDTH: c_int = 100; +const BOX_HEIGHT: c_int = 100; + +const URL: &str = concat!(env!("CARGO_PKG_HOMEPAGE"), "\0"); +const ENC: StringEncoding = StringEncoding::ASCII; + +const IMG_PATH: &Path = "examples/ferris"; +const SOUND_PATH: &Path = "examples/main_theme.pda"; + + +/// 2D point +struct Point { + x: T, + y: T, +} + +impl Point { + const fn new(x: T, y: T) -> Point { Point { x, y } } +} + + +/// Game state +struct State { + location: Point, + velocity: Point, + rotation: c_float, + image: Bitmap, + #[allow(dead_code)] + player: SamplePlayer, +} + + +impl State { + fn new() -> Self { + // Create bitmap + let image = Bitmap::new(BOX_WIDTH, BOX_HEIGHT, Color::BLACK).unwrap(); + + // Push bitmap into context to draw onto it + gfx::push_context(&image); + + // Load ferris the crab and draw + let ferris = Bitmap::::load(IMG_PATH).expect("missed bitmap"); + gfx::set_draw_mode(BitmapDrawMode::Inverted); + let (ferris_width, ferris_height) = ferris.size().expect("impossible"); + ferris.draw(BOX_WIDTH / 2 - ferris_width / 2, 0, BitmapFlip::Unflipped); + + // Load system font + let font = text::load_font(FONT_PATH).expect("failed to load font"); + + // Draw text lines + let lines = [("Ferris", None), ("loves", None), ("Playdate!", Some(font))]; + let mut acc_text_height = 0; + for (line, font) in lines { + // Get width (screen-size) of the line + let text_width = text::get_text_width(line, font.as_ref(), 0).expect("get text width"); + + // Set font for future drawing + font.as_ref().map(text::set_font); + + // Draw line + text::draw_text( + line, + BOX_WIDTH / 2 - text_width / 2, + ferris_height + acc_text_height, + ).expect("invalid string"); + + acc_text_height += TEXT_HEIGHT as c_int; + } + + // Remove our bitmap from the context + gfx::pop_context(); + + + // Background music + + // Create player + let player = SamplePlayer::try_new().unwrap(); + + // load sound + let sample = Sample::new_from_file(SOUND_PATH); + player.try_set_sample(&sample).unwrap(); + + // start playback + player.try_play(Repeat::LoopsEndlessly, 1.0).unwrap(); + + // Finally store it all in the state + Self { location: Point::new(CENTER_X as _, CENTER_Y as _), + velocity: Point::new(1, 2), + rotation: 0., + image, + player } + } + + + /// System event handler + fn event(&'static mut self, event: SystemEvent) -> bool { + match event { + // Initial setup + SystemEvent::Init => { + // Set FPS to 30 + Display::Default().set_refresh_rate(30.0); + + // Register our update handler that defined below + self.set_update_handler(); + }, + _ => {}, + } + true + } +} + + +impl Update for State { + /// Updates the state + fn update(&mut self) -> bool { + gfx::clear(Color::WHITE); + + + self.location.x += self.velocity.x; + self.location.y += self.velocity.y; + + if self.location.x < BOX_WIDTH / 2 || self.location.x > Display::COLUMNS as i32 - BOX_WIDTH / 2 { + self.velocity.x = -self.velocity.x; + } + + if self.location.y < BOX_HEIGHT / 2 || self.location.y > Display::ROWS as i32 - BOX_HEIGHT / 2 { + self.velocity.y = -self.velocity.y; + } + + + let url = CStr::from_bytes_with_nul(URL.as_bytes()).expect("invalid string"); + + // Get width (screen-size) of text + let text_width = text::get_text_width_cstr(url, ENC, None, 0); + + // Draw bottom text + text::draw_text_cstr( + url, + ENC, + CENTER_X as c_int - text_width / 2, + (Display::ROWS - TEXT_HEIGHT).try_into().unwrap(), + ); + + // Draw bitmap + self.image.draw_rotated( + self.location.x as _, + self.location.y as _, + self.rotation, + 0.5, + 0.5, + 1.0, + 1.0, + ); + + self.rotation += 0.5 * self.velocity.x as c_float; + if self.rotation > 360.0 { + self.rotation = 0.0; + } + + + System::Default().draw_fps(0, 0); + + true + } +} + + +/// Entry point +#[no_mangle] +fn event_handler(_api: NonNull, event: SystemEvent, _sim_key_code: u32) -> bool { + // Unsafe static storage for our state. + // Usually it's safe because there's only one thread. + pub static mut STATE: Option = None; + if unsafe { STATE.is_none() } { + let state = State::new(); + unsafe { STATE = Some(state) } + } + + // Call state.event + unsafe { STATE.as_mut().expect("impossible") }.event(event) +} + + +// Needed for debug build, absolutely optional +ll_symbols!(); diff --git a/api/playdate/examples/minimal.rs b/api/playdate/examples/minimal.rs new file mode 100644 index 00000000..128d912f --- /dev/null +++ b/api/playdate/examples/minimal.rs @@ -0,0 +1,85 @@ +#![no_std] +extern crate alloc; + +#[macro_use] +extern crate playdate as pd; + +use core::ffi::*; +use core::ptr::NonNull; +use pd::sys::ffi::PlaydateAPI; + +use pd::display::Display; +use pd::graphics::*; +use pd::graphics::bitmap::*; +use pd::system::prelude::*; + + +/// Game state +struct State { + // TODO: Fill the state +} + + +impl State { + fn new() -> Self { + // TODO: Init the state + + Self {} + } + + + /// System event handler + fn event(&'static mut self, event: SystemEvent) -> bool { + match event { + // Initial setup + SystemEvent::Init => { + // Set FPS to 30 + Display::Default().set_refresh_rate(30.0); + + // Register our update handler that defined below + self.set_update_handler(); + + println!("Game init complete"); + }, + // TODO: React to other events + _ => {}, + } + true + } +} + + +impl Update for State { + /// Updates the state + fn update(&mut self) -> bool { + clear(Color::WHITE); + + + // TODO: update the state of game + + + System::Default().draw_fps(0, 0); + + true + } +} + + +/// Entry point +#[no_mangle] +fn event_handler(_api: NonNull, event: SystemEvent, _sim_key_code: u32) -> bool { + // Unsafe static storage for our state. + // Usually it's safe because there's only one thread. + pub static mut STATE: Option = None; + if unsafe { STATE.is_none() } { + let state = State::new(); + unsafe { STATE = Some(state) } + } + + // Call state.event + unsafe { STATE.as_mut().expect("impossible") }.event(event) +} + + +// Needed for debug build, absolutely optional +ll_symbols!(); diff --git a/api/playdate/src/lib.rs b/api/playdate/src/lib.rs index 8abbc148..0c2fbf70 100644 --- a/api/playdate/src/lib.rs +++ b/api/playdate/src/lib.rs @@ -1,13 +1,19 @@ #![cfg_attr(not(test), no_std)] - extern crate alloc; -extern crate sys; +#[allow(unused_imports)] +#[macro_use] +pub extern crate sys; pub extern crate menu; pub extern crate display; pub extern crate ctrl as controls; pub extern crate gfx as graphics; + +// macro re-export +pub use sys::{println, ll_symbols, api, api_opt, api_ok}; + + pub mod system { pub use system::*; pub use menu; diff --git a/api/sound/Cargo.toml b/api/sound/Cargo.toml index 9a831902..642e6c44 100644 --- a/api/sound/Cargo.toml +++ b/api/sound/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "playdate-sound" -version = "0.1.4" +version = "0.1.5" readme = "README.md" description = "High-level sound API built on-top of Playdate API" keywords = ["playdate", "sdk", "api", "gamedev"] diff --git a/api/sound/src/lib.rs b/api/sound/src/lib.rs index 0ed5d030..b62435c9 100644 --- a/api/sound/src/lib.rs +++ b/api/sound/src/lib.rs @@ -16,4 +16,5 @@ pub mod prelude { pub use crate::error::Error as SndError; pub use crate::player; + pub use crate::sample; } diff --git a/api/sys/Cargo.toml b/api/sys/Cargo.toml index faf9412a..e8047303 100644 --- a/api/sys/Cargo.toml +++ b/api/sys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "playdate-sys" -version = "0.1.10" +version = "0.1.11" build = "src/build.rs" readme = "README.md" description = "Low-level Playdate API bindings" @@ -9,7 +9,7 @@ categories = [ "external-ffi-bindings", "development-tools::ffi", "game-development", - "no-std" + "no-std", ] edition.workspace = true license.workspace = true @@ -23,18 +23,17 @@ default = [ "lang-items", "bindgen-runtime", "bindings-derive-debug", - # "bindings-documentation", # this should not be default ] -lang-items = ["allocator", "panic-handler", "eh-personality"] -# lang-items: -allocator = [] # global allocator -panic-handler = [] # global panic handler -eh-personality = [] # eh_personality for sim-targets, dummy empty no-op + +lang-items = ["allocator", "panic-handler", "eh-personality"] # lang-items +allocator = [] # global allocator +panic-handler = [] # global panic handler +eh-personality = [] # eh_personality for sim-targets, dummy empty no-op entry-point = [] # simple minimal proxy entry point error-ctx = [] # errors with context (incomplete feature) -# bindings build options: + bindgen-logging = ["bindgen/logging"] bindgen-pretty-please = ["bindgen/pretty-please"] bindgen-which-rustfmt = ["bindgen/which-rustfmt"] @@ -43,7 +42,6 @@ bindgen-static = ["bindgen/static"] # static linking to clang # parse docs from the SDK, gen doc-comments bindings-documentation = ["bindgen/documentation"] # generate docs for bindings -# derives: bindings-derive-default = [] # ask bindgen to derive `Default` bindings-derive-eq = [] # ask bindgen to derive `Eq` bindings-derive-copy = [] # ask bindgen to derive `Copy` @@ -100,5 +98,11 @@ name = "hello-world" crate-type = ["dylib", "staticlib"] path = "examples/hello-world.rs" +[[example]] +name = "handler" +crate-type = ["dylib", "staticlib"] +path = "examples/handler.rs" +required-features = ["entry-point"] + [package.metadata.playdate] bundle-id = "rs.playdate.sys" diff --git a/api/sys/examples/handler.rs b/api/sys/examples/handler.rs new file mode 100644 index 00000000..3ecce3e3 --- /dev/null +++ b/api/sys/examples/handler.rs @@ -0,0 +1,59 @@ +//! Entry point / event handler example. + +#![no_std] +extern crate alloc; +use core::ffi::*; +use core::ptr::null_mut; +use core::ptr::NonNull; +use alloc::boxed::Box; + +#[macro_use] +extern crate playdate_sys as pd; +use pd::ffi::*; + + +/// Entry point / event handler +#[no_mangle] +fn event_handler(api: NonNull, event: PDSystemEvent, arg: u32) -> bool { + println!("Init"); + + // Do something good with `api` here... + + + if event == PDSystemEvent::kEventInit { + // Registering update-callback with user-data, + // where user-data is just `u32` because not needed nothing complex for this example. + let state = Box::into_raw(Box::new(0_u32)); + unsafe { api!(system.setUpdateCallback)(Some(update_handler), state as *mut _) }; + } + + + // Continue event-loop: + true +} + + +/// Update handler. +/// +/// Just count to a hundred and stop the updates. +unsafe extern "C" fn update_handler(state: *mut c_void) -> c_int { + let ptr: *mut u32 = state.cast(); + let state = ptr.as_mut().expect("missed state"); + *state += 1; + + println!("Counting, {state}"); + + if *state == 100 { + println!("Stopping updates..."); + api!(system.setUpdateCallback)(None, null_mut()); + println!("See you."); + } + + + // Continue updates: + true.into() +} + + +// Needed for debug build, optional. +ll_symbols!(); diff --git a/api/system/Cargo.toml b/api/system/Cargo.toml index d903702d..394dd20c 100644 --- a/api/system/Cargo.toml +++ b/api/system/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "playdate-system" -version = "0.1.2" +version = "0.2.0" readme = "README.md" description = "High-level System API built on-top of Playdate API" keywords = ["playdate", "sdk", "api", "gamedev"] @@ -19,6 +19,7 @@ lang-items = ["sys/lang-items"] allocator = ["sys/allocator"] panic-handler = ["sys/panic-handler"] eh-personality = ["sys/eh-personality"] +entry-point = ["sys/entry-point"] error-ctx = ["sys/error-ctx"] bindgen-runtime = ["sys/bindgen-runtime"] bindgen-static = ["sys/bindgen-static"] @@ -58,3 +59,25 @@ cargo-args = [ "-Zrustdoc-scrape-examples", "-Zbuild-std=core,alloc", ] + + +[[example]] +name = "handler-boxed" +crate-type = ["dylib", "staticlib"] +path = "examples/handler-boxed.rs" +required-features = ["sys/entry-point"] + +[[example]] +name = "handler-static" +crate-type = ["dylib", "staticlib"] +path = "examples/handler-static.rs" +required-features = ["sys/entry-point"] + +[[example]] +name = "handler-pinned" +crate-type = ["dylib", "staticlib"] +path = "examples/handler-pinned.rs" +required-features = ["sys/entry-point"] + +[package.metadata.playdate] +bundle-id = "rs.playdate.system" diff --git a/api/system/examples/README.md b/api/system/examples/README.md new file mode 100644 index 00000000..55203ed1 --- /dev/null +++ b/api/system/examples/README.md @@ -0,0 +1,17 @@ +# Examples + + +# How to run + +```bash +# Simulator: +cargo playdate run -p=playdate-system --example=handler-static --features=entry-point +# Device: +cargo playdate run -p=playdate-system --example=handler-boxed --features=entry-point --device +``` + +More information how to use [cargo-playdate][] in help: `cargo playdate --help`. + + + +[cargo-playdate]: https://crates.io/crates/cargo-playdate diff --git a/api/system/examples/handler-boxed.rs b/api/system/examples/handler-boxed.rs new file mode 100644 index 00000000..469e306a --- /dev/null +++ b/api/system/examples/handler-boxed.rs @@ -0,0 +1,49 @@ +#![no_std] +extern crate alloc; + +#[macro_use] +extern crate sys; +extern crate playdate_system as system; + +use core::ptr::NonNull; + +use sys::ffi::*; +use system::System; + + +/// Entry point, event handler +#[no_mangle] +fn event_handler(api: NonNull, event: PDSystemEvent, arg: u32) -> bool { + println!("Init"); + + // Do something good with `api` here... + + + let system = System::Default(); + + // Registering update-callback with user-data. + // The user-data is just a number because not needed nothing complex for this example. + system.set_update_callback_boxed( + |v| { + *v += 1; + println!("{v} / 100"); + + if *v == 100 { + println!("Stopping updates..."); + system.set_update_callback_static(None, ()); + println!("See you."); + } + + // Continue updates: + true + }, + 42, + ); + + // Continue event-loop: + true +} + + +// Needed for debug build +ll_symbols!(); diff --git a/api/system/examples/handler-pinned.rs b/api/system/examples/handler-pinned.rs new file mode 100644 index 00000000..55d78780 --- /dev/null +++ b/api/system/examples/handler-pinned.rs @@ -0,0 +1,57 @@ +#![no_std] +extern crate alloc; + +#[macro_use] +extern crate sys; +extern crate playdate_system as system; + +use core::any::Any; +use core::ptr::NonNull; +use alloc::boxed::Box; + +use sys::ffi::*; +use system::System; + + +/// Entry point +#[no_mangle] +fn event_handler(api: NonNull, event: PDSystemEvent, arg: u32) -> bool { + println!("Init"); + + // Do something good with `api` here... + + + // Registering update-callback with user-data. + // The user-data is just a number because not needed nothing complex for this example. + + static mut HANDLE: Option> = None; + + if unsafe { HANDLE.is_none() } { + let system = System::Default(); + let handle = system.set_update_callback( + move |v| { + *v += 1; + println!("{v}"); + + if *v == 100 { + println!("Stopping updates..."); + system.set_update_callback_static(None, ()); + println!("See you."); + } + + // Continue updates: + true + }, + 42, + ); + unsafe { HANDLE = Some(Box::new(handle)) }; + } + + + // Continue event-loop: + true +} + + +// Needed for debug build +ll_symbols!(); diff --git a/api/system/examples/handler-static.rs b/api/system/examples/handler-static.rs new file mode 100644 index 00000000..1a58efe5 --- /dev/null +++ b/api/system/examples/handler-static.rs @@ -0,0 +1,49 @@ +#![no_std] +extern crate alloc; + +#[macro_use] +extern crate sys; +extern crate playdate_system as system; + +use core::ptr::NonNull; + +use sys::ffi::*; +use system::System; + + +/// Entry point, event handler +#[no_mangle] +fn event_handler(api: NonNull, event: PDSystemEvent, arg: u32) -> bool { + println!("Init"); + + // Do something good with `api` here... + + + // Registering update-callback with user-data. + // The user-data is just a number because not needed nothing complex for this example. + System::Default().set_update_callback_static(Some(on_update), 42); + + + // Continue event-loop: + true +} + + +/// Update handler +fn on_update(v: &mut i32) -> bool { + *v += 1; + println!("{v} / 100"); + + if *v == 100 { + println!("Stopping updates..."); + System::Default().set_update_callback_static(None, ()); + println!("See you."); + } + + // Continue updates: + true +} + + +// Needed for debug build +ll_symbols!(); diff --git a/api/system/src/event.rs b/api/system/src/event.rs new file mode 100644 index 00000000..5f4c0f33 --- /dev/null +++ b/api/system/src/event.rs @@ -0,0 +1,70 @@ +pub use sys::ffi::PDSystemEvent as SystemEvent; + + +pub trait SystemEventExt { + #![allow(non_upper_case_globals)] + + /// Program initialization. + /// + /// After loading pdex.bin into memory, the system calls your event handler with this event. + /// + /// Then you can supply your own run loop update function + /// by calling [`System::set_update_callback`](crate::System::set_update_callback) + /// or [`Update::set_update_handler`](crate::update::Update::set_update_handler) here. + /// + /// If you don’t provide an update callback, the system initializes a Lua context + /// and calls your event handler again with event [`InitLua`](SystemEventExt::InitLua). + #[doc(alias = "sys::ffi::PDSystemEvent::kEventInitage")] + const Init: SystemEvent = SystemEvent::kEventInit; + + /// Program initialization in __lua context__. + #[doc(alias = "sys::ffi::PDSystemEvent::kEventInitLua")] + const InitLua: SystemEvent = SystemEvent::kEventInitLua; + + /// System going to locked state. + #[doc(alias = "sys::ffi::PDSystemEvent::kEventLockage")] + const Lock: SystemEvent = SystemEvent::kEventLock; + + /// System has been unlocked by user. + #[doc(alias = "sys::ffi::PDSystemEvent::kEventUnlock")] + const Unlock: SystemEvent = SystemEvent::kEventUnlock; + + /// Program execution paused. + #[doc(alias = "sys::ffi::PDSystemEvent::kEventPausee")] + const Pause: SystemEvent = SystemEvent::kEventPause; + + /// Program execution resumed after [pause](SystemEventExt::Pause). + #[doc(alias = "sys::ffi::PDSystemEvent::kEventResume")] + const Resume: SystemEvent = SystemEvent::kEventResume; + + /// Program termination. + #[doc(alias = "sys::ffi::PDSystemEvent::kEventTerminate")] + const Terminate: SystemEvent = SystemEvent::kEventTerminate; + + /// Simulator key is pressed. + /// + /// When an arbitrary key is pressed __in the Simulator__ + /// your event handler is called with this event and the keycode of the key in the last argument. + /// + /// See also [`KeyReleased`](SystemEventExt::KeyReleased). + #[doc(alias = "sys::ffi::PDSystemEvent::kEventKeyPressed")] + const KeyPressed: SystemEvent = SystemEvent::kEventKeyPressed; + + /// Simulator key is released. + /// + /// When an arbitrary key is released __in the Simulator__ + /// your event handler is called with this event and the keycode of the key in the last argument. + /// + /// See also [`KeyPressed`](SystemEventExt::KeyPressed). + #[doc(alias = "sys::ffi::PDSystemEvent::kEventKeyReleased")] + const KeyReleased: SystemEvent = SystemEvent::kEventKeyReleased; + + /// Low power warning by system. + /// + /// At this point, it's a good idea to persistently save anything you need, such as a save-game. + #[doc(alias = "sys::ffi::PDSystemEvent::kEventLowPower")] + const LowPower: SystemEvent = SystemEvent::kEventLowPower; +} + + +impl SystemEventExt for SystemEvent {} diff --git a/api/system/src/lib.rs b/api/system/src/lib.rs index 726ad02b..17bc9fcb 100644 --- a/api/system/src/lib.rs +++ b/api/system/src/lib.rs @@ -5,22 +5,36 @@ extern crate alloc; use core::ffi::c_float; use core::ffi::c_int; use core::ffi::c_uint; -use core::ffi::c_void; -use core::marker::PhantomData; use core::time::Duration; -use core::pin::Pin; -use alloc::boxed::Box; pub mod time; pub mod lang; +pub mod update; +pub mod event; + +pub mod prelude { + pub use crate::System; + pub use crate::time::*; + pub use crate::lang::*; + pub use crate::update::*; + pub use crate::event::*; +} -pub use time::*; -pub use lang::*; +use time::*; +use lang::*; #[derive(Debug, Clone, Copy)] pub struct System(Api); +impl System { + /// Creates default [`System`] without type parameter requirement. + /// + /// Uses ZST [`api::Default`]. + #[allow(non_snake_case)] + pub fn Default() -> Self { Self(Default::default()) } +} + impl Default for System { fn default() -> Self { Self(Default::default()) } } @@ -34,80 +48,6 @@ impl System { } -pub struct CallbackHandler<'t, F, U>(Option>>, PhantomData<&'t ()>); - -impl<'t, F, U> Drop for CallbackHandler<'t, F, U> { - fn drop(&mut self) { - let get_fn = || sys::api_opt!(system.setUpdateCallback); - if self.0.is_some() { - if let Some(f) = get_fn() { - unsafe { - f(None, core::ptr::null_mut()); - } - } - } - } -} - - -impl System { - /// Takes an any function with `userdata` into the `Pin`, - /// registers callback in the system and returns this wrapped function with userdata. - /// - /// For register a fn-ptr you could better use [`set_update_callback_static`]. - /// - /// Wrapping [`sys::ffi::playdate_sys::setUpdateCallback`] - #[doc(alias = "sys::ffi::playdate_sys::setUpdateCallback")] - pub fn set_update_callback<'u, U, F>(&self, on_update: F, userdata: U) -> CallbackHandler<'u, F, U> - where U: 'u, - F: 'u + FnMut(&mut U) -> bool { - unsafe extern "C" fn proxy bool>(fn_ud: *mut c_void) -> c_int { - if let Some((callback, userdata)) = (fn_ud as *mut (Fn, UD)).as_mut() { - callback(userdata).into() - } else { - panic!("user callback missed"); - } - } - - let f = self.0.set_update_callback(); - - let mut userdata = Box::pin((on_update, userdata)); - let ptr = unsafe { userdata.as_mut().get_unchecked_mut() } as *mut _ as *mut c_void; - - unsafe { f(Some(proxy::), ptr) }; - - CallbackHandler(userdata.into(), PhantomData) - } - - - /// Takes `on_update` and `userdata` and wraps it into the `Box`, - /// then registers callback. - /// - /// Wrapping [`sys::ffi::playdate_sys::setUpdateCallback`] - #[doc(alias = "sys::ffi::playdate_sys::setUpdateCallback")] - pub fn set_update_callback_static(&self, - on_update: Option bool>, - userdata: U) { - unsafe extern "C" fn proxy(fn_ud: *mut c_void) -> c_int { - if let Some((callback, userdata)) = (fn_ud as *mut (fn(userdata: &mut UD) -> bool, UD)).as_mut() { - callback(userdata).into() - } else { - panic!("user callback missed"); - } - } - - let f = self.0.set_update_callback(); - - if let Some(callback) = on_update { - let ptr = Box::into_raw(Box::new((callback, userdata))); - unsafe { f(Some(proxy::), ptr as *mut _) }; - } else { - unsafe { f(None, core::ptr::null_mut()) }; - } - } -} - - impl System { /// Equivalent to [`sys::ffi::playdate_sys::getLanguage`] #[doc(alias = "sys::ffi::playdate_sys::getLanguage")] diff --git a/api/system/src/update.rs b/api/system/src/update.rs new file mode 100644 index 00000000..13f84cb1 --- /dev/null +++ b/api/system/src/update.rs @@ -0,0 +1,162 @@ +use core::ffi::c_void; +use core::ffi::c_int; +use core::marker::PhantomData; +use core::pin::Pin; +use alloc::boxed::Box; + +use crate::api; +use crate::System; + + +/// Pinned wrapper around a function and user data. +/// +/// On drop, automatically resets system registered update handler. +pub struct Handler<'t, F, U>(Option>>, PhantomData<&'t ()>); + +impl<'t, F, U> Drop for Handler<'t, F, U> { + fn drop(&mut self) { + let get_fn = || sys::api_opt!(system.setUpdateCallback); + if self.0.is_some() { + if let Some(f) = get_fn() { + unsafe { + f(None, core::ptr::null_mut()); + } + } + } + } +} + + +impl System { + /// Internal update callback proxy function. + unsafe extern "C" fn proxy bool>(fn_ud: *mut c_void) -> c_int { + if let Some((callback, userdata)) = (fn_ud as *mut (Fn, UD)).as_mut() { + callback(userdata).into() + } else { + panic!("user callback missed"); + } + } + + + /// Takes __any__ function and `userdata`, + /// registers callback in the system and + /// returns this function with userdata wrapped into the [`Handler`] with [`Pin`] inside. + /// + /// For register a fn-ptr you could better use [`set_update_callback_static`]. + /// + /// Safety is ensured by [`Handler`], + /// that resets the system registered update handler when drop. + /// + /// Wrapping [`sys::ffi::playdate_sys::setUpdateCallback`] + #[doc(alias = "sys::ffi::playdate_sys::setUpdateCallback")] + #[must_use = "Update handler will be unregistered when Handler dropped"] + pub fn set_update_callback<'u, U, F>(&self, on_update: F, userdata: U) -> Handler<'u, F, U> + where U: 'u, + F: 'u + FnMut(&mut U) -> bool { + let f = self.0.set_update_callback(); + let mut userdata = Box::pin((on_update, userdata)); + let ptr = unsafe { userdata.as_mut().get_unchecked_mut() } as *mut _ as *mut c_void; + unsafe { f(Some(Self::proxy::), ptr) }; + Handler(userdata.into(), PhantomData) + } + + /// Consumes and __leaks__ an __any__ function with `userdata` into the `Box`, + /// registers callback in the system. + /// + /// For register a fn-ptr you could better use [`set_update_callback_static`]. + /// + /// __Safety is guaranteed by the caller.__ + /// + /// See also [`System::set_update_callback`], it prevents leaks and more safe. + /// + /// Wrapping [`sys::ffi::playdate_sys::setUpdateCallback`] + #[doc(alias = "sys::ffi::playdate_sys::setUpdateCallback")] + pub fn set_update_callback_boxed<'u, U, F>(&self, on_update: F, userdata: U) + where U: 'u, + F: 'u + FnMut(&mut U) -> bool { + let f = self.0.set_update_callback(); + let ptr = Box::into_raw(Box::new((on_update, userdata))); + unsafe { f(Some(Self::proxy::), ptr as *mut _) }; + } + + + /// Consumes and __leaks__ function `on_update` and `userdata`, wraps it into the `Box`, + /// then registers callback. + /// + /// See also [`System::set_update_callback`], it prevents leaks and more safe. + /// + /// Wrapping [`sys::ffi::playdate_sys::setUpdateCallback`] + #[doc(alias = "sys::ffi::playdate_sys::setUpdateCallback")] + pub fn set_update_callback_static(&self, + on_update: Option bool>, + userdata: U) { + unsafe extern "C" fn proxy(fn_ud: *mut c_void) -> c_int { + if let Some((callback, userdata)) = (fn_ud as *mut (fn(userdata: &mut UD) -> bool, UD)).as_mut() { + callback(userdata).into() + } else { + panic!("user callback missed"); + } + } + + let f = self.0.set_update_callback(); + if let Some(callback) = on_update { + let ptr = Box::into_raw(Box::new((callback, userdata))); + unsafe { f(Some(proxy::), ptr as *mut _) }; + } else { + unsafe { f(None, core::ptr::null_mut()) }; + } + } + + /// Executes `handler`'s [`Update::set_update_handler_with`] with this inner api. + /// + /// Wrapping [`sys::ffi::playdate_sys::setUpdateCallback`] + #[doc(alias = "sys::ffi::playdate_sys::setUpdateCallback")] + #[inline(always)] + pub fn set_update_handler<'t, U: 'static + Update>(&'t self, handler: Option<&'static mut U>) + where &'t Api: api::Api { + if let Some(handler) = handler { + handler.set_update_handler_with(&self.0) + } else { + let f = self.0.set_update_callback(); + unsafe { f(None, core::ptr::null_mut()) }; + } + } +} + + +/// Implementable stateful update handler +/// with default implementation for adapter and register functions. +pub trait Update: Sized { + fn update(&mut self) -> bool; + + /// Register a callback function [`Self::update`] in the system, + /// using [`Default`](api::Default) `api`. + /// + /// See also [`Update::set_update_handler_with`] and [`System::set_update_handler`]. + /// + /// Equivalent to [`sys::ffi::playdate_sys::setUpdateCallback`] + #[doc(alias = "sys::ffi::playdate_sys::setUpdateCallback")] + #[inline(always)] + fn set_update_handler(&'static mut self) { self.set_update_handler_with(api::Default) } + + /// Register a callback function [`Self::update`] in the system, + /// using given `api`. + /// + /// Equivalent to [`sys::ffi::playdate_sys::setUpdateCallback`] + #[doc(alias = "sys::ffi::playdate_sys::setUpdateCallback")] + fn set_update_handler_with(&'static mut self, api: Api) { + let f = api.set_update_callback(); + unsafe { f(Some(Self::update_proxy), self as *mut Self as *mut _) }; + } + + + /// Overridable update callback adapter. + #[doc(hidden)] + unsafe extern "C" fn update_proxy(handler: *mut c_void) -> c_int { + if let Some(handler) = (handler as *mut Self).as_mut() { + Self::update(handler).into() + } else { + panic!("user callback missed"); + } + } +} diff --git a/cargo/Cargo.toml b/cargo/Cargo.toml index 06431820..3093badd 100644 --- a/cargo/Cargo.toml +++ b/cargo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-playdate" -version = "0.3.1" +version = "0.3.2" readme = "README.md" description = "Build tool for neat yellow console." keywords = ["playdate", "build", "cargo", "plugin", "cargo-subcommand"] diff --git a/cargo/src/init/bin-hl.rs b/cargo/src/init/bin-hl.rs new file mode 100644 index 00000000..195b4547 --- /dev/null +++ b/cargo/src/init/bin-hl.rs @@ -0,0 +1,17 @@ +#![no_std] +#![no_main] +#[allow(unused_imports)] +extern crate alloc; +extern crate playdate as pd; +extern crate {crate_name}; + +use core::ffi::c_int; +use core::ptr::NonNull; +use {crate_name}::event_handler; +use pd::sys::eventHandlerShim; +use pd::sys::ffi::{{PlaydateAPI, PDSystemEvent}}; + + +// Needed to tell rustc that it should be in the output after DCE, LTO and stripping. +#[used] pub static EVENT_HANDLER: extern "Rust" fn(NonNull, PDSystemEvent, u32) -> bool = event_handler; +#[used] pub static EVENT_HANDLER_SHIM: extern "C" fn(*const PlaydateAPI, PDSystemEvent, u32) -> c_int = eventHandlerShim; diff --git a/cargo/src/init/default-bin.rs b/cargo/src/init/bin-ll.rs similarity index 78% rename from cargo/src/init/default-bin.rs rename to cargo/src/init/bin-ll.rs index 21ead06d..9cc554c4 100644 --- a/cargo/src/init/default-bin.rs +++ b/cargo/src/init/bin-ll.rs @@ -4,8 +4,11 @@ extern crate alloc; extern crate playdate_sys as sys; extern crate {crate_name}; + +use core::ffi::c_int; use {crate_name}::eventHandlerShim; + #[used] /// TODO: describe that it's needed to tell rustc that it should be in the output after DCE, LTO and stripping. -pub static EVENT_HANDLER_SHIM: extern "C" fn(*const sys::ffi::PlaydateAPI, sys::ffi::PDSystemEvent, u32) -> i32 = eventHandlerShim; +pub static EVENT_HANDLER_SHIM: extern "C" fn(*const sys::ffi::PlaydateAPI, sys::ffi::PDSystemEvent, u32) -> c_int = eventHandlerShim; diff --git a/cargo/src/init/lib-hl.rs b/cargo/src/init/lib-hl.rs new file mode 100644 index 00000000..7bb7adb0 --- /dev/null +++ b/cargo/src/init/lib-hl.rs @@ -0,0 +1,90 @@ +#![no_std] +extern crate alloc; + +#[macro_use] +extern crate playdate as pd; + +use core::ffi::*; +use core::ptr::NonNull; +use pd::sys::ffi::PlaydateAPI; + +use pd::display::Display; +use pd::graphics::*; +use pd::graphics::text::*; +use pd::graphics::bitmap::*; +use pd::system::prelude::*; +use pd::sound::prelude::*; +use sample::Sample; +use player::*; +use pd::fs::Path; + + +/// Game state +struct State { + // TODO: Fill the state +} + + +impl State { + fn new() -> Self { + // TODO: Init the state + + Self {} + } + + + /// System event handler + fn event(&'static mut self, event: SystemEvent) -> bool { + match event { + // Initial setup + SystemEvent::Init => { + // Set FPS to 30 + Display::Default().set_refresh_rate(30.0); + + // Register our update handler that defined below + self.set_update_handler(); + + println!("Game init complete"); + }, + // TODO: React to other events + _ => {}, + } + true + } +} + + +impl Update for State { + /// Updates the state + fn update(&mut self) -> bool { + clear(Color::WHITE); + + + // TODO: update the state of game + + + System::Default().draw_fps(0, 0); + + true + } +} + + +/// Entry point +#[no_mangle] +pub fn event_handler(_api: NonNull, event: SystemEvent, _sim_key_code: u32) -> bool { + // Unsafe static storage for our state. + // Usually it's safe because there's only one thread. + pub static mut STATE: Option = None; + if unsafe { STATE.is_none() } { + let state = State::new(); + unsafe { STATE = Some(state) } + } + + // Call state.event + unsafe { STATE.as_mut().expect("impossible") }.event(event) +} + + +// Needed for debug build, absolutely optional +ll_symbols!(); diff --git a/cargo/src/init/default-lib.rs b/cargo/src/init/lib-ll.rs similarity index 100% rename from cargo/src/init/default-lib.rs rename to cargo/src/init/lib-ll.rs diff --git a/cargo/src/init/mod.rs b/cargo/src/init/mod.rs index cfbef746..a89ee382 100644 --- a/cargo/src/init/mod.rs +++ b/cargo/src/init/mod.rs @@ -66,14 +66,14 @@ pub fn new_or_init<'cfg>(config: &'cfg Config<'cfg>) -> CargoResult<()> { // TODO: add dependencies - let deps_to_add = add_dependencies(config, &mut manifest)?; + let (deps_to_add, hl) = add_dependencies(config, &mut manifest)?; // sources: - lib(config, &path, &mut manifest)?; + lib(config, &path, &mut manifest, hl)?; if is_bin { - bin(config, &path, &mut manifest)?; + bin(config, &path, &mut manifest, hl)?; } @@ -85,6 +85,7 @@ pub fn new_or_init<'cfg>(config: &'cfg Config<'cfg>) -> CargoResult<()> { // cargo config: cargo_config(config, path.join(".cargo").join("config.toml"))?; + // TODO: deps_to_add for dep in deps_to_add { // TODO call cargo add WITH PWD=path @@ -231,9 +232,46 @@ fn add_full_metadata(_config: &Config<'_>, manifest: &mut toml_edit::Document) - } +fn cargo_add<'s>(config: &Config<'_>, + pwd: &Path, + manifest: &Path, + name: &str, + git: bool, + rename: Option<&str>, + features: Option>) + -> CargoResult<()> { + let mut cargo = proc::cargo(config.workspace.config().into())?; + cargo.current_dir(pwd); + + cargo.arg("add"); + cargo.arg(name); + + if let Some(name) = rename { + cargo.arg("--rename"); + cargo.arg(name); + } + + if let Some(features) = features { + let features = features.into_iter().collect::>().join(","); + cargo.arg("--features"); + cargo.arg(features); + } + + // git => --git="URL" + + cargo.arg("manifest-path"); + cargo.arg(manifest); + + cargo.stderr(Stdio::inherit()); + cargo.stdout(Stdio::inherit()); + cargo.status()?.exit_ok()?; + Ok(()) +} + + fn add_dependencies<'cfg>(config: &'cfg Config<'_>, manifest: &mut toml_edit::Document) - -> CargoResult>> { + -> CargoResult<(Vec>, bool)> { use toml_edit::value; use crate::cli::deps::Dependency as Dep; use crate::cli::deps::DependencyName as Name; @@ -254,6 +292,8 @@ fn add_dependencies<'cfg>(config: &'cfg Config<'_>, Ok(()) } + let mut add_pd = false; + if config.create_deps_sys_only { let default = Dep { name: Name::Sys, source: Src::CratesIo }; @@ -262,12 +302,13 @@ fn add_dependencies<'cfg>(config: &'cfg Config<'_>, .find(|d| matches!(d.name, Name::Sys)) .unwrap_or(&default); add_dep(dep, manifest)?; - Ok(vec![]) + Ok((vec![], add_pd)) } else { let known = config.create_deps .iter() .filter(|d| !matches!(d.name, Name::Other(_))); for dep in known { + add_pd = matches!(dep.name, Name::Playdate) || add_pd; add_dep(dep, manifest)?; } @@ -278,7 +319,7 @@ fn add_dependencies<'cfg>(config: &'cfg Config<'_>, None } }); - Ok(others.collect()) + Ok((others.collect(), add_pd)) } } @@ -356,7 +397,7 @@ fn cargo_config>(config: &Config, path: P) -> CargoResult<()> { } -fn lib(_config: &Config, root: &Path, manifest: &mut toml_edit::Document) -> CargoResult<()> { +fn lib(config: &Config, root: &Path, manifest: &mut toml_edit::Document, hl: bool) -> CargoResult<()> { use toml_edit::Document; let toml = r#" @@ -371,16 +412,26 @@ crate-type = [ manifest["lib"] = toml.parse::().expect("invalid doc")["lib"].clone(); let path = root.join("src").join("lib.rs"); - std::fs::write(path, include_bytes!("default-lib.rs"))?; + let src = if config.create_deps_sys_only || !hl { + &include_bytes!("lib-ll.rs")[..] + } else { + &include_bytes!("lib-hl.rs")[..] + }; + std::fs::write(path, src)?; Ok(()) } -fn bin(_config: &Config, root: &Path, manifest: &mut toml_edit::Document) -> CargoResult<()> { +fn bin(config: &Config, root: &Path, manifest: &mut toml_edit::Document, hl: bool) -> CargoResult<()> { let path = root.join("src").join("main.rs"); let name = manifest["package"]["name"].as_str() .unwrap_or("hello-world") - .to_owned(); - let src = format!(include_str!("default-bin.rs"), crate_name = name); + .to_owned() + .replace("-", "_"); + let src = if config.create_deps_sys_only || !hl { + format!(include_str!("bin-ll.rs"), crate_name = name) + } else { + format!(include_str!("bin-hl.rs"), crate_name = name) + }; std::fs::write(path, src)?; Ok(()) } diff --git a/cargo/tests/init/init.rs b/cargo/tests/init/init.rs index da14d69d..6d8876d1 100644 --- a/cargo/tests/init/init.rs +++ b/cargo/tests/init/init.rs @@ -31,6 +31,8 @@ fn run(crate_name: &str, let output = Tool::init(&crate_path, args)?; assert!(output.status.success()); Ok((output, crate_path)) + + // TODO: Run `package` after `init`/`run` cmd }