├── .cargo └── config ├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── Crank.toml ├── LICENSE ├── README.md ├── crankstart-sys ├── Cargo.toml ├── src │ ├── bindings_aarch64.rs │ ├── bindings_playdate.rs │ ├── bindings_x86.rs │ └── lib.rs └── wrapper.h ├── examples ├── assets │ └── heart.png ├── hello_world.rs ├── life.rs ├── menu_items.rs └── sprite_game.rs ├── scripts ├── generate_bindings.sh └── vars.sh └── src ├── display.rs ├── file.rs ├── geometry.rs ├── graphics.rs ├── lib.rs ├── lua.rs ├── sound.rs ├── sound ├── fileplayer.rs └── sampleplayer.rs ├── sprite.rs └── system.rs /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | rustflags = ["-Ctarget-feature=+crt-static"] 3 | 4 | [target.i686-pc-windows-msvc] 5 | rustflags = ["-Ctarget-feature=+crt-static"] 6 | 7 | [target.i586-pc-windows-msvc] 8 | rustflags = ["-Ctarget-feature=+crt-static"] 9 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Check Rust Format 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | workflow_dispatch: 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | check: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Check Format 21 | run: cargo fmt --check 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | /sprite_game_images 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crankstart" 3 | version = "0.1.2" 4 | authors = ["Rob Tsuk "] 5 | edition = "2018" 6 | description = "A barely functional, wildly incomplete and basically undocumented Rust crate whose aim is to let you write games for the [Playdate handheld gaming system](https://play.date) in [Rust](https://www.rust-lang.org)." 7 | license = "MIT" 8 | repository = "https://github.com/pd-rs/crankstart" 9 | 10 | [package.metadata.cargo-xbuild] 11 | memcpy = false 12 | sysroot_path = "target/sysroot" 13 | panic_immediate_abort = false 14 | 15 | [profile.dev] 16 | panic = "abort" 17 | opt-level = 2 18 | lto = true 19 | 20 | [profile.release] 21 | panic = "abort" 22 | opt-level = 'z' 23 | lto = true 24 | 25 | [workspace] 26 | members = [ 27 | "crankstart-sys", 28 | ] 29 | 30 | [[example]] 31 | name = "hello_world" 32 | path = "examples/hello_world.rs" 33 | crate-type = ["staticlib", "cdylib"] 34 | 35 | [[example]] 36 | name = "menu_items" 37 | path = "examples/menu_items.rs" 38 | crate-type = ["staticlib", "cdylib"] 39 | 40 | [[example]] 41 | name = "life" 42 | path = "examples/life.rs" 43 | crate-type = ["staticlib", "cdylib"] 44 | 45 | [[example]] 46 | name = "sprite_game" 47 | path = "examples/sprite_game.rs" 48 | crate-type = ["staticlib", "cdylib"] 49 | 50 | [dependencies] 51 | anyhow = { version = "1.0.31", default-features = false } 52 | arrayvec = { version = "0.7.4", default-features = false } 53 | crankstart-sys = { version = "0.1.2", path = "crankstart-sys" } 54 | euclid = { version = "0.22.9", default-features = false, features = [ "libm" ] } 55 | hashbrown = "0.14.0" 56 | 57 | [dev-dependencies] 58 | randomize = "3.0.1" 59 | enum-iterator = "0.6.0" 60 | rand = { version = "0.8.4", default-features = false, features = [ "alloc" ] } 61 | rand_pcg = "0.3.1" 62 | 63 | [dependencies.cstr_core] 64 | version = "=0.1.2" 65 | default-features = false 66 | features = [ "alloc" ] 67 | 68 | -------------------------------------------------------------------------------- /Crank.toml: -------------------------------------------------------------------------------- 1 | [[target]] 2 | name = "hello_world" 3 | 4 | assets = [ 5 | "examples/assets/heart.png", 6 | ] 7 | 8 | [target.metadata] 9 | name = "Hello World" 10 | version = "0.1.0" 11 | 12 | [[target]] 13 | name = "sprite_game" 14 | assets = [ 15 | "sprite_game_images/background.png", 16 | "sprite_game_images/player.png", 17 | "sprite_game_images/doubleBullet.png", 18 | "sprite_game_images/plane1.png", 19 | "sprite_game_images/plane2.png", 20 | "sprite_game_images/explosion/1.png", 21 | "sprite_game_images/explosion/2.png", 22 | "sprite_game_images/explosion/3.png", 23 | "sprite_game_images/explosion/4.png", 24 | "sprite_game_images/explosion/5.png", 25 | "sprite_game_images/explosion/6.png", 26 | "sprite_game_images/explosion/7.png", 27 | "sprite_game_images/explosion/8.png", 28 | ] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rob Tsuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust for Playdate 2 | 3 | You've stumbled across a barely functional, wildly incomplete and basically undocumented Rust crate whose aim is to let you write games for the [Playdate handheld gaming system](https://play.date) in [Rust](https://www.rust-lang.org). 4 | 5 | This software is not sponsored or supported by Panic. 6 | 7 | ## Installation 8 | 9 | To use this crate, you'll also need to install the [crank command line tool](https://github.com/rtsuk/crank). 10 | 11 | From the crankstart directory where you found this README, 12 | 13 | crank run --release --example hello_world 14 | 15 | Should launch the simulator and load in the hello_world sample. 16 | 17 | If you have a device attached to your desktop, 18 | 19 | crank run --release --example hello_world --device 20 | 21 | Should launch the hello_world sample on the device. 22 | 23 | For the sprite_game example one needs to copy the images folder from `"PlaydateSDK/C_API/Examples/Sprite Game/Source/images"` to `"sprite_game_images"`. 24 | 25 | ## Your Own Project 26 | 27 | Using this system for your own project requires some setup: 28 | 29 | 1. Follow the setup for `crank` with Rust nightly's `no_std` support. 30 | 2. Start a new rust library project with `cargo new --lib project_name` 31 | 3. `git clone git@github.com:pd-rs/crankstart.git` at the same depth as your new project. 32 | 4. Go into the new project, and add the following to your `Cargo.toml`: 33 | 34 | ```toml 35 | [package.metadata.cargo-xbuild] 36 | memcpy = false 37 | sysroot_path = "target/sysroot" 38 | panic_immediate_abort = false 39 | 40 | [profile.dev] 41 | panic = "abort" 42 | opt-level = 'z' 43 | lto = true 44 | 45 | [profile.release] 46 | panic = "abort" 47 | opt-level = 'z' 48 | lto = true 49 | 50 | [lib] 51 | crate-type = ["staticlib", "cdylib"] 52 | 53 | [dependencies] 54 | crankstart = { path = "../crankstart" } 55 | crankstart-sys = { path = "../crankstart/crankstart-sys" } 56 | anyhow = { version = "1.0.31", default-features = false } 57 | euclid = { version = "0.22.9", default-features = false, features = [ "libm" ] } 58 | hashbrown = "0.14.0" 59 | 60 | [dependencies.cstr_core] 61 | version = "=0.1.2" 62 | default-features = false 63 | features = [ "alloc" ] 64 | ``` 65 | 66 | 5. Add a `Crank.toml` at the same level as your `Cargo.toml`, with this minimum: 67 | 68 | ```toml 69 | [[target]] 70 | name = "project_name" 71 | assets = [ 72 | ] 73 | ``` 74 | 75 | `assets` should be a list of paths to any/all assets you need copied into your project, such as sprites, music, etc. 76 | 77 | 6. Inside your `lib.rs`, you only need to implement the `crankstart::Game` trait to your game's core state struct, then call `crankstart::crankstart_game!` on that struct. See the `examples` folder for examples. 78 | 7. To run the project, from its root, you should now be able to `crank run` successfully! 79 | 80 | If you want an example of an independent project following these conventions, go check out [Nine Lives](https://github.com/bravely/nine_lives). 81 | 82 | ## Updating Bindings 83 | 84 | If there's a newer [Playdate SDK](https://play.date/dev/) available that updates the C API, the crankstart bindings should be updated to match. 85 | Here's a guide. 86 | 87 | 1. Install [the dependencies for bindgen](https://rust-lang.github.io/rust-bindgen/requirements.html). 88 | 2. Install [bindgen-cli](https://rust-lang.github.io/rust-bindgen/command-line-usage.html). 89 | 3. Install the gcc-arm-none-eabi toolchain, either [manually](https://developer.arm.com/Tools%20and%20Software/GNU%20Toolchain) or through a system package, which may also be named something like "cross-arm-none-eabi-gcc". 90 | 4. On Linux, install the 32-bit glibc development package, which will be called something like `glibc-devel-32bit`. 91 | 5. Install the new [Playdate SDK](https://play.date/dev/), and if it's not at the default MacOS path, set `PLAYDATE_SDK_PATH` to where you unzipped it. (This should be the directory that contains `C_API`, `CoreLibs`, etc.) 92 | 6. Run `./scripts/generate_bindings.sh` 93 | 7. Inspect the changes to `crankstart-sys/src/bindings_*` - they should reflect the updates to the Playdate C API. If nothing changed, double-check that the C API actually changed and not just the Lua API. 94 | 8. Submit a PR with the changes :) 95 | -------------------------------------------------------------------------------- /crankstart-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crankstart-sys" 3 | version = "0.1.2" 4 | authors = ["Rob Tsuk "] 5 | edition = "2018" 6 | description = "A barely functional, wildly incomplete and basically undocumented Rust crate whose aim is to let you write games for the [Playdate handheld gaming system](https://play.date) in [Rust](https://www.rust-lang.org)." 7 | license = "MIT" 8 | repository = "https://github.com/pd-rs/crankstart" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [features] 13 | default = ["euclid"] 14 | 15 | [dependencies] 16 | euclid = { version = "0.22.9", default-features = false, features = [ "libm" ], optional = true } 17 | -------------------------------------------------------------------------------- /crankstart-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(non_upper_case_globals)] 3 | #![allow(non_camel_case_types)] 4 | #![allow(non_snake_case)] 5 | 6 | #[cfg(all(target_os = "macos", any(target_arch = "x86", target_arch = "x86_64")))] 7 | pub mod ctypes { 8 | pub type c_ulong = u64; 9 | pub type c_int = i32; 10 | pub type c_char = i8; 11 | pub type c_uint = u32; 12 | pub type c_void = core::ffi::c_void; 13 | pub type realloc_size = u64; 14 | } 15 | 16 | #[cfg(all(target_os = "macos", any(target_arch = "aarch64", target_arch = "arm")))] 17 | pub mod ctypes { 18 | pub type c_ulong = u64; 19 | pub type c_int = i32; 20 | pub type c_char = u8; 21 | pub type c_uint = u32; 22 | pub type c_void = core::ffi::c_void; 23 | pub type realloc_size = u64; 24 | } 25 | 26 | #[cfg(all( 27 | not(target_os = "macos"), 28 | any(target_arch = "x86", target_arch = "x86_64") 29 | ))] 30 | pub mod ctypes { 31 | pub type c_ulong = u64; 32 | pub type c_int = i32; 33 | pub type c_char = i8; 34 | pub type c_uchar = u8; 35 | pub type c_uint = u32; 36 | pub type c_ushort = u16; 37 | pub type c_short = i16; 38 | pub type c_void = core::ffi::c_void; 39 | pub type realloc_size = u32; 40 | } 41 | 42 | #[cfg(all( 43 | not(target_os = "macos"), 44 | any(target_arch = "aarch64", target_arch = "arm") 45 | ))] 46 | pub mod ctypes { 47 | pub type c_ulong = u64; 48 | pub type c_int = i32; 49 | pub type c_char = u8; 50 | pub type c_uchar = u8; 51 | pub type c_uint = u32; 52 | pub type c_ushort = u16; 53 | pub type c_short = i16; 54 | pub type c_void = core::ffi::c_void; 55 | pub type realloc_size = u32; 56 | } 57 | 58 | #[cfg(all(target_os = "windows", target_feature = "crt-static"))] 59 | #[link(name = "libcmt")] 60 | extern "C" {} 61 | #[cfg(all(target_os = "windows", not(target_feature = "crt-static")))] 62 | #[link(name = "msvcrt")] 63 | extern "C" {} 64 | 65 | #[cfg(all( 66 | not(target_os = "none"), 67 | any(target_arch = "x86", target_arch = "x86_64") 68 | ))] 69 | include!("bindings_x86.rs"); 70 | #[cfg(all( 71 | not(target_os = "none"), 72 | any(target_arch = "aarch64", target_arch = "arm") 73 | ))] 74 | include!("bindings_aarch64.rs"); 75 | #[cfg(target_os = "none")] 76 | include!("bindings_playdate.rs"); 77 | 78 | #[cfg(feature = "euclid")] 79 | impl From> for LCDRect { 80 | fn from(r: euclid::default::Rect) -> Self { 81 | LCDRect { 82 | top: r.max_y(), 83 | bottom: r.min_y(), 84 | left: r.min_x(), 85 | right: r.max_x(), 86 | } 87 | } 88 | } 89 | 90 | #[cfg(feature = "euclid")] 91 | impl From for euclid::default::Rect { 92 | fn from(r: LCDRect) -> Self { 93 | euclid::rect(r.left, r.top, r.right - r.left, r.bottom - r.top) 94 | } 95 | } 96 | 97 | #[cfg(feature = "euclid")] 98 | impl From> for PDRect { 99 | fn from(r: euclid::default::Rect) -> Self { 100 | PDRect { 101 | x: r.origin.x, 102 | y: r.origin.y, 103 | width: r.size.width, 104 | height: r.size.height, 105 | } 106 | } 107 | } 108 | 109 | #[cfg(feature = "euclid")] 110 | impl From for euclid::default::Rect { 111 | fn from(r: PDRect) -> Self { 112 | euclid::rect(r.x, r.y, r.width, r.height) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /crankstart-sys/wrapper.h: -------------------------------------------------------------------------------- 1 | #include "pd_api.h" 2 | -------------------------------------------------------------------------------- /examples/assets/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pd-rs/crankstart/2d2e99c89326d16b3ee6b465bdfea39c1a25d8ce/examples/assets/heart.png -------------------------------------------------------------------------------- /examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | extern crate alloc; 4 | 5 | use crankstart::log_to_console; 6 | use crankstart::sprite::{Sprite, SpriteManager}; 7 | use crankstart_sys::{LCDBitmapFlip, PDButtons}; 8 | use { 9 | alloc::boxed::Box, 10 | anyhow::Error, 11 | crankstart::{ 12 | crankstart_game, 13 | geometry::{ScreenPoint, ScreenVector}, 14 | graphics::{Graphics, LCDColor, LCDSolidColor}, 15 | system::System, 16 | Game, Playdate, 17 | }, 18 | crankstart_sys::{LCD_COLUMNS, LCD_ROWS}, 19 | euclid::{point2, vec2}, 20 | }; 21 | 22 | struct State { 23 | location: ScreenPoint, 24 | velocity: ScreenVector, 25 | sprite: Sprite, 26 | } 27 | 28 | fn load_sprite() -> Result { 29 | let sprite_manager = SpriteManager::get_mut(); 30 | let mut sprite = sprite_manager.new_sprite()?; 31 | let image = Graphics::get().load_bitmap("examples/assets/heart")?; 32 | sprite.set_image(image, LCDBitmapFlip::kBitmapUnflipped)?; 33 | sprite.move_to(200.0, 120.0)?; 34 | sprite.set_z_index(10)?; 35 | sprite.set_opaque(false)?; 36 | sprite_manager.add_sprite(&sprite)?; 37 | Ok(sprite) 38 | } 39 | 40 | impl State { 41 | pub fn new(_playdate: &Playdate) -> Result, Error> { 42 | crankstart::display::Display::get().set_refresh_rate(20.0)?; 43 | let sprite = load_sprite()?; 44 | Ok(Box::new(Self { 45 | location: point2(INITIAL_X, INITIAL_Y), 46 | velocity: vec2(1, 2), 47 | sprite, 48 | })) 49 | } 50 | } 51 | 52 | impl Game for State { 53 | fn update(&mut self, _playdate: &mut Playdate) -> Result<(), Error> { 54 | let graphics = Graphics::get(); 55 | graphics.clear_context()?; 56 | graphics.clear(LCDColor::Solid(LCDSolidColor::kColorWhite))?; 57 | graphics.draw_text("Hello World Rust", self.location)?; 58 | 59 | self.location += self.velocity; 60 | 61 | if self.location.x < 0 || self.location.x > LCD_COLUMNS as i32 - TEXT_WIDTH { 62 | self.velocity.x = -self.velocity.x; 63 | } 64 | 65 | if self.location.y < 0 || self.location.y > LCD_ROWS as i32 - TEXT_HEIGHT { 66 | self.velocity.y = -self.velocity.y; 67 | } 68 | 69 | let (_, pushed, _) = System::get().get_button_state()?; 70 | if (pushed & PDButtons::kButtonA).0 != 0 { 71 | log_to_console!("Button A pushed"); 72 | self.sprite 73 | .set_visible(!self.sprite.is_visible().unwrap_or(false)) 74 | .unwrap(); 75 | } 76 | 77 | System::get().draw_fps(0, 0)?; 78 | 79 | Ok(()) 80 | } 81 | 82 | fn update_sprite( 83 | &mut self, 84 | sprite: &mut Sprite, 85 | _playdate: &mut Playdate, 86 | ) -> Result<(), Error> { 87 | sprite.mark_dirty()?; 88 | Ok(()) 89 | } 90 | } 91 | 92 | const INITIAL_X: i32 = (400 - TEXT_WIDTH) / 2; 93 | const INITIAL_Y: i32 = (240 - TEXT_HEIGHT) / 2; 94 | 95 | const TEXT_WIDTH: i32 = 86; 96 | const TEXT_HEIGHT: i32 = 16; 97 | 98 | crankstart_game!(State); 99 | -------------------------------------------------------------------------------- /examples/life.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | extern crate alloc; 4 | 5 | use { 6 | alloc::boxed::Box, 7 | anyhow::Error, 8 | crankstart::{ 9 | crankstart_game, 10 | graphics::{Graphics, LCD_COLUMNS, LCD_ROWS, LCD_ROWSIZE}, 11 | system::{PDButtons, System}, 12 | Game, Playdate, 13 | }, 14 | randomize::PCG32, 15 | }; 16 | 17 | const LIMIT: usize = (LCD_COLUMNS - 1) as usize; 18 | 19 | fn ison(row: &'static [u8], x: usize) -> bool { 20 | (row[x / 8] & (0x80 >> (x % 8))) == 0 21 | } 22 | 23 | fn val(row: &'static [u8], x: usize) -> u8 { 24 | 1 - ((row[x / 8] >> (7 - (x % 8))) & 1) 25 | } 26 | 27 | fn rowsum(row: &'static [u8], x: usize) -> u8 { 28 | if x == 0 { 29 | val(row, LIMIT) + val(row, x) + val(row, x + 1) 30 | } else if x < LIMIT { 31 | return val(row, x - 1) + val(row, x) + val(row, x + 1); 32 | } else { 33 | val(row, x - 1) + val(row, x) + val(row, 0) 34 | } 35 | } 36 | 37 | fn middlerowsum(row: &'static [u8], x: usize) -> u8 { 38 | if x == 0 { 39 | val(row, LIMIT) + val(row, x + 1) 40 | } else if x < LIMIT { 41 | val(row, x - 1) + val(row, x + 1) 42 | } else { 43 | val(row, x - 1) + val(row, 0) 44 | } 45 | } 46 | 47 | fn do_row<'a>( 48 | lastrow: &'static [u8], 49 | row: &'static [u8], 50 | nextrow: &'static [u8], 51 | outrow: &'a mut [u8], 52 | ) { 53 | let mut b = 0; 54 | let mut bitpos = 0x80; 55 | 56 | for x in 0..(LCD_COLUMNS as usize) { 57 | // If total is 3 cell is alive 58 | // If total is 4, no change 59 | // Else, cell is dead 60 | 61 | let sum = rowsum(lastrow, x) + middlerowsum(row, x) + rowsum(nextrow, x); 62 | 63 | if sum == 3 || (ison(row, x) && sum == 2) { 64 | b |= bitpos; 65 | } 66 | 67 | bitpos >>= 1; 68 | 69 | if bitpos == 0 { 70 | outrow[x / 8] = !b; 71 | b = 0; 72 | bitpos = 0x80; 73 | } 74 | } 75 | } 76 | 77 | fn randomize(graphics: &Graphics, rng: &mut PCG32) -> Result<(), Error> { 78 | let frame = graphics.get_display_frame()?; 79 | let start = 0; 80 | for element in &mut frame[start..] { 81 | *element = rng.next_u32() as u8; 82 | } 83 | Ok(()) 84 | } 85 | 86 | struct Life { 87 | rng: PCG32, 88 | started: bool, 89 | } 90 | 91 | const LAST_ROW_INDEX: usize = ((LCD_ROWS - 1) * LCD_ROWSIZE) as usize; 92 | const LAST_ROW_LIMIT: usize = LAST_ROW_INDEX + LCD_ROWSIZE as usize; 93 | 94 | impl Life { 95 | pub fn new(_playdate: &Playdate) -> Result, Error> { 96 | let rng0 = PCG32::seed(1, 1); 97 | Ok(Box::new(Self { 98 | rng: rng0, 99 | started: false, 100 | })) 101 | } 102 | } 103 | 104 | impl Game for Life { 105 | fn update(&mut self, _playdate: &mut Playdate) -> Result<(), Error> { 106 | let graphics = Graphics::get(); 107 | if !self.started { 108 | randomize(&graphics, &mut self.rng)?; 109 | self.started = true; 110 | } 111 | 112 | let (_, pushed, _) = System::get().get_button_state()?; 113 | 114 | if (pushed & PDButtons::kButtonA) == PDButtons::kButtonA { 115 | randomize(&graphics, &mut self.rng)?; 116 | } 117 | 118 | let frame = graphics.get_frame()?; 119 | 120 | let last_frame = graphics.get_display_frame()?; 121 | let mut last_row = &last_frame[LAST_ROW_INDEX..LAST_ROW_LIMIT]; 122 | let mut row = &last_frame[0..LCD_ROWSIZE as usize]; 123 | let mut next_row = &last_frame[LCD_ROWSIZE as usize..(LCD_ROWSIZE * 2) as usize]; 124 | for y in 0..LCD_ROWS { 125 | let index = (y * LCD_ROWSIZE) as usize; 126 | let limit = index + LCD_ROWSIZE as usize; 127 | do_row(last_row, row, next_row, &mut frame[index..limit]); 128 | 129 | last_row = row; 130 | row = next_row; 131 | let next_row_index = (y + 2) % LCD_ROWS; 132 | 133 | let index = (next_row_index * LCD_ROWSIZE) as usize; 134 | let limit = index + LCD_ROWSIZE as usize; 135 | next_row = &last_frame[index..limit]; 136 | } 137 | 138 | graphics.mark_updated_rows(0..=(LCD_ROWS as i32) - 1)?; 139 | 140 | Ok(()) 141 | } 142 | } 143 | 144 | crankstart_game!(Life); 145 | -------------------------------------------------------------------------------- /examples/menu_items.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | extern crate alloc; 4 | 5 | use alloc::rc::Rc; 6 | use alloc::string::String; 7 | use alloc::vec; 8 | use alloc::vec::Vec; 9 | use core::cell::RefCell; 10 | 11 | use hashbrown::HashMap; 12 | 13 | use crankstart::system::MenuItemKind; 14 | use { 15 | alloc::boxed::Box, 16 | anyhow::Error, 17 | crankstart::{ 18 | crankstart_game, 19 | geometry::ScreenPoint, 20 | graphics::{Graphics, LCDColor, LCDSolidColor}, 21 | log_to_console, 22 | system::{MenuItem, System}, 23 | Game, Playdate, 24 | }, 25 | euclid::point2, 26 | }; 27 | 28 | struct State { 29 | _menu_items: Rc>>, 30 | text_location: ScreenPoint, 31 | } 32 | 33 | impl State { 34 | pub fn new(_playdate: &Playdate) -> Result, Error> { 35 | crankstart::display::Display::get().set_refresh_rate(20.0)?; 36 | let menu_items = Rc::new(RefCell::new(HashMap::new())); 37 | let system = System::get(); 38 | let normal_item = { 39 | system.add_menu_item( 40 | "Select Me", 41 | Box::new(|| { 42 | log_to_console!("Normal option picked"); 43 | }), 44 | )? 45 | }; 46 | let checkmark_item = { 47 | let ref_menu_items = menu_items.clone(); 48 | system.add_checkmark_menu_item( 49 | "Toggle Me", 50 | false, 51 | Box::new(move || { 52 | let value_of_item = { 53 | let menu_items = ref_menu_items.borrow(); 54 | let this_menu_item = menu_items.get("checkmark").unwrap(); 55 | System::get().get_menu_item_value(this_menu_item).unwrap() != 0 56 | }; 57 | log_to_console!("Checked option picked: Value is now: {}", value_of_item); 58 | }), 59 | )? 60 | }; 61 | let options_item = { 62 | let ref_menu_items = menu_items.clone(); 63 | let options: Vec = vec!["Small".into(), "Medium".into(), "Large".into()]; 64 | system.add_options_menu_item( 65 | "Size", 66 | options, 67 | Box::new(move || { 68 | let value_of_item = { 69 | let menu_items = ref_menu_items.borrow(); 70 | let this_menu_item = menu_items.get("options").unwrap(); 71 | let idx = System::get().get_menu_item_value(this_menu_item).unwrap(); 72 | match &this_menu_item.kind { 73 | MenuItemKind::Options(opts) => opts.get(idx).map(|s| s.clone()), 74 | _ => None, 75 | } 76 | }; 77 | log_to_console!("Checked option picked: Value is now {:?}", value_of_item); 78 | }), 79 | )? 80 | }; 81 | { 82 | let mut menu_items = menu_items.borrow_mut(); 83 | menu_items.insert("normal", normal_item); 84 | menu_items.insert("checkmark", checkmark_item); 85 | menu_items.insert("options", options_item); 86 | } 87 | Ok(Box::new(Self { 88 | _menu_items: menu_items, 89 | text_location: point2(100, 100), 90 | })) 91 | } 92 | } 93 | 94 | impl Game for State { 95 | fn update(&mut self, _playdate: &mut Playdate) -> Result<(), Error> { 96 | let graphics = Graphics::get(); 97 | graphics.clear(LCDColor::Solid(LCDSolidColor::kColorWhite))?; 98 | graphics 99 | .draw_text("Menu Items", self.text_location) 100 | .unwrap(); 101 | 102 | System::get().draw_fps(0, 0)?; 103 | 104 | Ok(()) 105 | } 106 | } 107 | 108 | crankstart_game!(State); 109 | -------------------------------------------------------------------------------- /examples/sprite_game.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | extern crate alloc; 4 | 5 | use { 6 | alloc::{boxed::Box, format, vec::Vec}, 7 | anyhow::Error, 8 | crankstart::{ 9 | crankstart_game, 10 | graphics::{rect_make, Bitmap, BitmapData, Graphics, LCDBitmapFlip, PDRect}, 11 | log_to_console, 12 | sprite::{Sprite, SpriteCollider, SpriteManager}, 13 | system::{PDButtons, System}, 14 | Game, Playdate, 15 | }, 16 | crankstart_sys::SpriteCollisionResponseType, 17 | euclid::point2, 18 | randomize::PCG32, 19 | }; 20 | 21 | const MAX_MAX_ENEMIES: usize = 119; 22 | 23 | struct BackgroundHandler { 24 | background_image: Bitmap, 25 | y: i32, 26 | height: i32, 27 | } 28 | 29 | #[derive(Debug)] 30 | struct OverlapCollider; 31 | 32 | impl SpriteCollider for OverlapCollider { 33 | fn response_type(&self, _: Sprite, _: Sprite) -> SpriteCollisionResponseType { 34 | SpriteCollisionResponseType::kCollisionTypeOverlap 35 | } 36 | } 37 | 38 | fn remove_sprite_from_list(sprites: &mut Vec, target: &Sprite) { 39 | if let Some(pos) = sprites.iter().position(|s| *s == *target) { 40 | sprites.remove(pos); 41 | } else { 42 | log_to_console!("can't find sprite to remove"); 43 | } 44 | } 45 | 46 | fn create_explosion( 47 | x: f32, 48 | y: f32, 49 | explosions: &mut Vec, 50 | explosion_bitmaps: &Vec, 51 | ) -> Result<(), Error> { 52 | let sprite_manager = SpriteManager::get_mut(); 53 | let mut explosion = sprite_manager.new_sprite()?; 54 | explosion.set_image( 55 | explosion_bitmaps[0].clone(), 56 | LCDBitmapFlip::kBitmapUnflipped, 57 | )?; 58 | explosion.move_to(x, y)?; 59 | explosion.set_tag(SpriteType::ExplosionBase as u8)?; 60 | explosion.set_z_index(2000)?; 61 | sprite_manager.add_sprite(&explosion)?; 62 | explosions.push(explosion); 63 | Ok(()) 64 | } 65 | 66 | fn destroy_enemy_plane( 67 | enemies: &mut Vec, 68 | target: &Sprite, 69 | explosions: &mut Vec, 70 | explosion_bitmaps: &Vec, 71 | ) -> Result<(), Error> { 72 | let (x, y) = target.get_position()?; 73 | create_explosion(x, y, explosions, explosion_bitmaps)?; 74 | remove_sprite_from_list(enemies, target); 75 | Ok(()) 76 | } 77 | 78 | impl BackgroundHandler { 79 | fn update(&mut self, sprite: &mut Sprite) -> Result<(), Error> { 80 | self.y += 1; 81 | if self.y > self.height { 82 | self.y = 0; 83 | } 84 | sprite.mark_dirty()?; 85 | Ok(()) 86 | } 87 | 88 | fn draw(&self) -> Result<(), Error> { 89 | self.background_image 90 | .draw(point2(0, self.y), LCDBitmapFlip::kBitmapUnflipped)?; 91 | self.background_image.draw( 92 | point2(0, self.y - self.height), 93 | LCDBitmapFlip::kBitmapUnflipped, 94 | )?; 95 | Ok(()) 96 | } 97 | } 98 | 99 | struct PlayerHandler; 100 | 101 | impl PlayerHandler { 102 | fn update( 103 | &mut self, 104 | sprite: &mut Sprite, 105 | enemies: &mut Vec, 106 | explosions: &mut Vec, 107 | explosion_bitmaps: &Vec, 108 | _playdate: &Playdate, 109 | ) -> Result<(), Error> { 110 | let (current, _, _) = System::get().get_button_state()?; 111 | 112 | let mut dx = 0.0; 113 | let mut dy = 0.0; 114 | 115 | if (current & PDButtons::kButtonUp) == PDButtons::kButtonUp { 116 | dy = -4.0; 117 | } else if (current & PDButtons::kButtonDown) == PDButtons::kButtonDown { 118 | dy = 4.0; 119 | } 120 | if (current & PDButtons::kButtonLeft) == PDButtons::kButtonLeft { 121 | dx = -4.0; 122 | } else if (current & PDButtons::kButtonRight) == PDButtons::kButtonRight { 123 | dx = 4.0; 124 | } 125 | 126 | let (mut x, mut y) = sprite.get_position()?; 127 | 128 | x += dx; 129 | y += dy; 130 | 131 | let (_, _, collisions) = sprite.move_with_collisions(x, y)?; 132 | 133 | for collision in collisions.iter() { 134 | let tag = collision.other.get_tag()?; 135 | if tag == SpriteType::EnemyPlane as u8 { 136 | destroy_enemy_plane(enemies, &collision.other, explosions, explosion_bitmaps)?; 137 | } 138 | } 139 | 140 | Ok(()) 141 | } 142 | } 143 | 144 | struct BulletHandler { 145 | bullet_image_data: BitmapData, 146 | } 147 | 148 | impl BulletHandler { 149 | fn update( 150 | &mut self, 151 | bullets: &mut Vec, 152 | enemies: &mut Vec, 153 | explosions: &mut Vec, 154 | explosion_bitmaps: &Vec, 155 | sprite: &mut Sprite, 156 | ) -> Result<(), Error> { 157 | fn remove_bullet(bullets: &mut Vec, sprite: &mut Sprite) { 158 | if let Some(pos) = bullets.iter().position(|bullet| *bullet == *sprite) { 159 | bullets.remove(pos); 160 | } else { 161 | log_to_console!("can't find bullet to remove"); 162 | } 163 | } 164 | 165 | let (x, y) = sprite.get_position()?; 166 | let new_y = y - 20.0; 167 | if new_y < -self.bullet_image_data.height as f32 { 168 | remove_bullet(bullets, sprite); 169 | } else { 170 | let (_, _, collisions) = sprite.move_with_collisions(x, new_y)?; 171 | for collision in collisions.iter() { 172 | let tag = collision.other.get_tag()?; 173 | if tag == SpriteType::EnemyPlane as u8 { 174 | remove_bullet(bullets, sprite); 175 | destroy_enemy_plane(enemies, &collision.other, explosions, explosion_bitmaps)?; 176 | } 177 | } 178 | } 179 | Ok(()) 180 | } 181 | } 182 | 183 | struct EnemyPlaneHandler { 184 | enemy_image_data: BitmapData, 185 | } 186 | 187 | impl EnemyPlaneHandler { 188 | fn update(&mut self, enemies: &mut Vec, sprite: &mut Sprite) -> Result<(), Error> { 189 | let (x, y) = sprite.get_position()?; 190 | let new_y = y + 4.0; 191 | if new_y > 400.0 + self.enemy_image_data.height as f32 { 192 | if let Some(pos) = enemies.iter().position(|enemy| *enemy == *sprite) { 193 | enemies.remove(pos); 194 | } else { 195 | log_to_console!("can't find enemy to remove"); 196 | } 197 | } else { 198 | sprite.move_to(x, new_y)?; 199 | } 200 | Ok(()) 201 | } 202 | } 203 | 204 | struct BackgroundPlaneHandler { 205 | background_plane_image_data: BitmapData, 206 | } 207 | 208 | impl BackgroundPlaneHandler { 209 | fn update( 210 | &mut self, 211 | background_planes: &mut Vec, 212 | sprite: &mut Sprite, 213 | ) -> Result<(), Error> { 214 | let (x, y) = sprite.get_position()?; 215 | let new_y = y + 2.0; 216 | if new_y > 400.0 + self.background_plane_image_data.height as f32 { 217 | if let Some(pos) = background_planes.iter().position(|p| *p == *sprite) { 218 | background_planes.remove(pos); 219 | } else { 220 | log_to_console!("can't find enemy to remove"); 221 | } 222 | } else { 223 | sprite.move_to(x, new_y)?; 224 | } 225 | Ok(()) 226 | } 227 | } 228 | 229 | struct ExplosionHandler {} 230 | 231 | impl ExplosionHandler { 232 | fn update( 233 | &mut self, 234 | explosion_bitmaps: &Vec, 235 | explosions: &mut Vec, 236 | sprite: &mut Sprite, 237 | ) -> Result<(), Error> { 238 | let frame_number = (sprite.get_tag()? - SpriteType::ExplosionBase as u8 + 1) as usize; 239 | if frame_number >= explosion_bitmaps.len() { 240 | remove_sprite_from_list(explosions, sprite); 241 | } else { 242 | sprite.set_image( 243 | explosion_bitmaps[frame_number].clone(), 244 | LCDBitmapFlip::kBitmapUnflipped, 245 | )?; 246 | sprite.set_tag(SpriteType::ExplosionBase as u8 + frame_number as u8)?; 247 | } 248 | Ok(()) 249 | } 250 | } 251 | 252 | #[repr(u8)] 253 | enum SpriteType { 254 | Player = 0, 255 | PlayerBullet = 1, 256 | EnemyPlane = 2, 257 | Background = 3, 258 | BackgroundPlane = 4, 259 | ExplosionBase = 5, 260 | } 261 | 262 | impl From for SpriteType { 263 | fn from(tag: u8) -> Self { 264 | let sprite_type = match tag { 265 | 0 => SpriteType::Player, 266 | 1 => SpriteType::PlayerBullet, 267 | 2 => SpriteType::EnemyPlane, 268 | 3 => SpriteType::Background, 269 | 4 => SpriteType::BackgroundPlane, 270 | _ => SpriteType::ExplosionBase, 271 | }; 272 | sprite_type 273 | } 274 | } 275 | 276 | struct SpriteGame { 277 | rng: PCG32, 278 | #[allow(unused)] 279 | background: Sprite, 280 | background_handler: BackgroundHandler, 281 | player: Sprite, 282 | player_handler: PlayerHandler, 283 | bullet_image: Bitmap, 284 | bullet_handler: BulletHandler, 285 | bullets: Vec, 286 | enemy_plane_image: Bitmap, 287 | enemy_plane_handler: EnemyPlaneHandler, 288 | enemies: Vec, 289 | background_plane_image: Bitmap, 290 | background_plane_handler: BackgroundPlaneHandler, 291 | background_planes: Vec, 292 | explosion_handler: ExplosionHandler, 293 | explosions: Vec, 294 | explosion_bitmaps: Vec, 295 | max_enemies: usize, 296 | max_background_planes: usize, 297 | } 298 | 299 | impl SpriteGame { 300 | fn new(_playdate: &mut Playdate) -> Result, Error> { 301 | let graphics = Graphics::get(); 302 | crankstart::display::Display::get().set_refresh_rate(20.0)?; 303 | // setup background 304 | let sprite_manager = SpriteManager::get_mut(); 305 | let mut background = sprite_manager.new_sprite()?; 306 | let background_image = graphics.load_bitmap("sprite_game_images/background")?; 307 | let background_image_data = background_image.get_data()?; 308 | let bounds = rect_make(0.0, 0.0, 400.0, 240.0); 309 | background.set_bounds(&bounds)?; 310 | background.set_z_index(0)?; 311 | background.set_tag(SpriteType::Background as u8)?; 312 | background.set_use_custom_draw()?; 313 | sprite_manager.add_sprite(&background)?; 314 | let background_handler = BackgroundHandler { 315 | background_image, 316 | height: background_image_data.height, 317 | y: 0, 318 | }; 319 | 320 | // setup player 321 | let mut player = sprite_manager.new_sprite()?; 322 | let player_image = graphics.load_bitmap("sprite_game_images/player")?; 323 | let player_image_data = player_image.get_data()?; 324 | player.set_image(player_image, LCDBitmapFlip::kBitmapUnflipped)?; 325 | let center_x: f32 = 200.0 - player_image_data.width as f32 / 2.0; 326 | let center_y: f32 = 180.0 - player_image_data.height as f32 / 2.0; 327 | let cr = rect_make( 328 | 5.0, 329 | 5.0, 330 | player_image_data.width as f32 - 10.0, 331 | player_image_data.height as f32 - 10.0, 332 | ); 333 | 334 | player.set_collide_rect(&cr)?; 335 | player.set_collision_response_type(Some(Box::new(OverlapCollider {})))?; 336 | player.set_tag(SpriteType::Player as u8)?; 337 | 338 | player.move_to(center_x, center_y)?; 339 | 340 | let bullet_image = graphics.load_bitmap("sprite_game_images/doubleBullet")?; 341 | let bullet_image_data = bullet_image.get_data()?; 342 | 343 | let enemy_plane_image = graphics.load_bitmap("sprite_game_images/plane1")?; 344 | let enemy_image_data = enemy_plane_image.get_data()?; 345 | let enemy_plane_handler = EnemyPlaneHandler { enemy_image_data }; 346 | 347 | let background_plane_image = graphics.load_bitmap("sprite_game_images/plane2")?; 348 | let background_plane_image_data = background_plane_image.get_data()?; 349 | let background_plane_handler = BackgroundPlaneHandler { 350 | background_plane_image_data, 351 | }; 352 | 353 | let explosion_handler = ExplosionHandler {}; 354 | let explosion_bitmaps: Vec = (1..12) 355 | .flat_map(|index| { 356 | graphics.load_bitmap(&format!("sprite_game_images/explosion/{}", index)) 357 | }) 358 | .collect(); 359 | 360 | let rng = PCG32::seed(1, 1); 361 | let mut sprite_game = Self { 362 | rng, 363 | background, 364 | background_handler, 365 | player, 366 | player_handler: PlayerHandler {}, 367 | bullet_handler: BulletHandler { bullet_image_data }, 368 | bullet_image, 369 | bullets: Vec::with_capacity(32), 370 | enemy_plane_image, 371 | enemy_plane_handler, 372 | enemies: Vec::with_capacity(32), 373 | background_plane_image, 374 | background_plane_handler, 375 | background_planes: Vec::with_capacity(32), 376 | explosion_handler, 377 | explosions: Vec::with_capacity(32), 378 | explosion_bitmaps, 379 | max_enemies: 10, 380 | max_background_planes: 10, 381 | }; 382 | sprite_game.setup()?; 383 | Ok(Box::new(sprite_game)) 384 | } 385 | 386 | fn setup(&mut self) -> Result<(), Error> { 387 | SpriteManager::get_mut().add_sprite(&self.player)?; 388 | self.player.set_z_index(1000)?; 389 | Ok(()) 390 | } 391 | 392 | fn player_fire(&mut self) -> Result<(), Error> { 393 | let sprite_manager = SpriteManager::get_mut(); 394 | let player_bounds = self.player.get_bounds()?; 395 | let bullet_image_data = self.bullet_image.get_data()?; 396 | let x = player_bounds.x + player_bounds.width / 2.0 - bullet_image_data.width as f32 / 2.0; 397 | let y = player_bounds.y; 398 | 399 | let mut bullet = sprite_manager.new_sprite()?; 400 | bullet.set_image(self.bullet_image.clone(), LCDBitmapFlip::kBitmapUnflipped)?; 401 | let cr = rect_make( 402 | 0.0, 403 | 0.0, 404 | bullet_image_data.width as f32, 405 | bullet_image_data.height as f32, 406 | ); 407 | bullet.set_collide_rect(&cr)?; 408 | bullet.set_collision_response_type(Some(Box::new(OverlapCollider {})))?; 409 | bullet.move_to(x, y)?; 410 | bullet.set_z_index(999)?; 411 | sprite_manager.add_sprite(&bullet)?; 412 | bullet.set_tag(SpriteType::PlayerBullet as u8)?; 413 | self.bullets.push(bullet); 414 | Ok(()) 415 | } 416 | 417 | fn check_buttons(&mut self, _playdate: &mut Playdate) -> Result<(), Error> { 418 | let (_, pushed, _) = System::get().get_button_state()?; 419 | if (pushed & PDButtons::kButtonA) == PDButtons::kButtonA 420 | || (pushed & PDButtons::kButtonB) == PDButtons::kButtonB 421 | { 422 | self.player_fire()?; 423 | } 424 | Ok(()) 425 | } 426 | 427 | fn check_crank(&mut self, _playdate: &mut Playdate) -> Result<(), Error> { 428 | let change = System::get().get_crank_change()? as i32; 429 | 430 | if change > 1 { 431 | self.max_enemies += 1; 432 | if self.max_enemies > MAX_MAX_ENEMIES { 433 | self.max_enemies = MAX_MAX_ENEMIES; 434 | } 435 | log_to_console!("Maximum number of enemy planes: {}", self.max_enemies); 436 | } else if change < -1 { 437 | self.max_enemies = self.max_enemies.saturating_sub(1); 438 | log_to_console!("Maximum number of enemy planes: {}", self.max_enemies); 439 | } 440 | Ok(()) 441 | } 442 | 443 | fn create_enemy_plane(&mut self) -> Result<(), Error> { 444 | let sprite_manager = SpriteManager::get_mut(); 445 | let mut plane = sprite_manager.new_sprite()?; 446 | plane.set_collision_response_type(Some(Box::new(OverlapCollider {})))?; 447 | let plane_image_data = self.enemy_plane_image.get_data()?; 448 | plane.set_image( 449 | self.enemy_plane_image.clone(), 450 | LCDBitmapFlip::kBitmapUnflipped, 451 | )?; 452 | let cr = rect_make( 453 | 0.0, 454 | 0.0, 455 | plane_image_data.width as f32, 456 | plane_image_data.height as f32, 457 | ); 458 | plane.set_collide_rect(&cr)?; 459 | let x = (self.rng.next_u32() % 400) as f32 - plane_image_data.width as f32 / 2.0; 460 | let y = -((self.rng.next_u32() % 30) as f32) - plane_image_data.height as f32; 461 | 462 | plane.move_to(x, y)?; 463 | plane.set_z_index(500)?; 464 | plane.set_tag(SpriteType::EnemyPlane as u8)?; 465 | sprite_manager.add_sprite(&plane)?; 466 | self.enemies.push(plane); 467 | Ok(()) 468 | } 469 | 470 | fn spawn_enemy_if_needed(&mut self) -> Result<(), Error> { 471 | if self.enemies.len() < self.max_enemies { 472 | let rand_v = self.rng.next_u32() as usize; 473 | if rand_v % (120 / self.max_enemies) == 0 { 474 | self.create_enemy_plane()?; 475 | } 476 | } 477 | Ok(()) 478 | } 479 | 480 | fn create_background_plane(&mut self) -> Result<(), Error> { 481 | let sprite_manager = SpriteManager::get_mut(); 482 | let mut plane = sprite_manager.new_sprite()?; 483 | let plane_image_data = self.background_plane_image.get_data()?; 484 | plane.set_image( 485 | self.background_plane_image.clone(), 486 | LCDBitmapFlip::kBitmapUnflipped, 487 | )?; 488 | let x = (self.rng.next_u32() % 400) as f32 - plane_image_data.width as f32 / 2.0; 489 | let y = -plane_image_data.height as f32; 490 | plane.move_to(x, y)?; 491 | plane.set_tag(SpriteType::BackgroundPlane as u8)?; 492 | plane.set_z_index(100)?; 493 | sprite_manager.add_sprite(&plane)?; 494 | self.background_planes.push(plane); 495 | Ok(()) 496 | } 497 | 498 | fn spawn_background_plane_if_needed(&mut self) -> Result<(), Error> { 499 | if self.background_planes.len() < self.max_background_planes { 500 | let rand_v = self.rng.next_u32() as usize; 501 | if rand_v % (120 / self.max_background_planes) == 0 { 502 | self.create_background_plane()?; 503 | } 504 | } 505 | Ok(()) 506 | } 507 | } 508 | 509 | impl Game for SpriteGame { 510 | fn update_sprite(&mut self, sprite: &mut Sprite, playdate: &mut Playdate) -> Result<(), Error> { 511 | let tag = sprite.get_tag()?.into(); 512 | match tag { 513 | SpriteType::Background => self.background_handler.update(sprite)?, 514 | SpriteType::Player => self.player_handler.update( 515 | sprite, 516 | &mut self.enemies, 517 | &mut self.explosions, 518 | &self.explosion_bitmaps, 519 | playdate, 520 | )?, 521 | SpriteType::PlayerBullet => self.bullet_handler.update( 522 | &mut self.bullets, 523 | &mut self.enemies, 524 | &mut self.explosions, 525 | &self.explosion_bitmaps, 526 | sprite, 527 | )?, 528 | SpriteType::EnemyPlane => self.enemy_plane_handler.update(&mut self.enemies, sprite)?, 529 | SpriteType::BackgroundPlane => self 530 | .background_plane_handler 531 | .update(&mut self.background_planes, sprite)?, 532 | _ => { 533 | self.explosion_handler.update( 534 | &self.explosion_bitmaps, 535 | &mut self.explosions, 536 | sprite, 537 | )?; 538 | } 539 | } 540 | Ok(()) 541 | } 542 | 543 | fn draw_sprite( 544 | &self, 545 | sprite: &Sprite, 546 | _bounds: &PDRect, 547 | _draw_rect: &PDRect, 548 | _playdate: &Playdate, 549 | ) -> Result<(), Error> { 550 | let tag = sprite.get_tag()?.into(); 551 | match tag { 552 | SpriteType::Background => self.background_handler.draw()?, 553 | _ => (), 554 | } 555 | Ok(()) 556 | } 557 | 558 | fn update(&mut self, playdate: &mut Playdate) -> Result<(), Error> { 559 | self.check_buttons(playdate)?; 560 | self.check_crank(playdate)?; 561 | self.spawn_enemy_if_needed()?; 562 | self.spawn_background_plane_if_needed()?; 563 | Ok(()) 564 | } 565 | 566 | fn draw_fps(&self) -> bool { 567 | true 568 | } 569 | } 570 | 571 | crankstart_game!(SpriteGame); 572 | -------------------------------------------------------------------------------- /scripts/generate_bindings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -x 4 | 5 | crankstart_crate_dir="$(cd "$(dirname "$0")/.." >/dev/null 2>&1 && pwd)" 6 | . "$crankstart_crate_dir/scripts/vars.sh" 7 | # shellcheck disable=SC2181 # can't check exit code of . in same line with POSIX sh 8 | if [ "$?" -ne 0 ]; then 9 | exit 2 10 | fi 11 | 12 | # POSIX sh "array" used to store common parameters to all bindgen calls 13 | set -- "$crankstart_crate_dir/crankstart-sys/wrapper.h" \ 14 | "--use-core" \ 15 | "--ctypes-prefix" "ctypes" \ 16 | "--with-derive-default" \ 17 | "--with-derive-eq" \ 18 | "--default-enum-style" "rust" \ 19 | "--allowlist-type" "PlaydateAPI" \ 20 | "--allowlist-type" "PDSystemEvent" \ 21 | "--allowlist-type" "LCDSolidColor" \ 22 | "--allowlist-type" "LCDColor" \ 23 | "--allowlist-type" "LCDPattern" \ 24 | "--allowlist-type" "PDEventHandler" \ 25 | "--allowlist-var" "LCD_COLUMNS" \ 26 | "--allowlist-var" "LCD_ROWS" \ 27 | "--allowlist-var" "LCD_ROWSIZE" \ 28 | "--allowlist-var" "SEEK_SET" \ 29 | "--allowlist-var" "SEEK_CUR" \ 30 | "--allowlist-var" "SEEK_END" \ 31 | "--bitfield-enum" "FileOptions" \ 32 | "--bitfield-enum" "PDButtons" 33 | 34 | bindgen "$@" \ 35 | -- \ 36 | -target x86_64 \ 37 | -I"$PLAYDATE_C_API" \ 38 | -I"$(arm-none-eabi-gcc -print-sysroot)/include" \ 39 | -DTARGET_EXTENSION > "${crankstart_crate_dir}/crankstart-sys/src/bindings_x86.rs" 40 | 41 | bindgen "$@" \ 42 | -- \ 43 | -target aarch64 \ 44 | -I"$PLAYDATE_C_API" \ 45 | -I"$(arm-none-eabi-gcc -print-sysroot)/include" \ 46 | -DTARGET_EXTENSION > "${crankstart_crate_dir}/crankstart-sys/src/bindings_aarch64.rs" 47 | 48 | bindgen "$@" \ 49 | -- \ 50 | -I"$PLAYDATE_C_API" \ 51 | -I"$(arm-none-eabi-gcc -print-sysroot)/include" \ 52 | -target thumbv7em-none-eabihf \ 53 | -fshort-enums \ 54 | -DTARGET_EXTENSION > "${crankstart_crate_dir}/crankstart-sys/src/bindings_playdate.rs" 55 | -------------------------------------------------------------------------------- /scripts/vars.sh: -------------------------------------------------------------------------------- 1 | : "${LLVM_CONFIG_PATH:=/usr/local/opt/llvm/bin/llvm-config}" 2 | export LLVM_CONFIG_PATH 3 | : "${PLAYDATE_SDK_PATH:=$HOME/Developer/PlaydateSDK}" 4 | PLAYDATE_C_API=$PLAYDATE_SDK_PATH/C_API 5 | -------------------------------------------------------------------------------- /src/display.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | geometry::{ScreenPoint, ScreenSize}, 3 | pd_func_caller, 4 | }; 5 | use anyhow::Error; 6 | use core::ptr; 7 | use euclid::{default::Vector2D, size2}; 8 | 9 | #[derive(Clone, Debug)] 10 | pub struct Display(*const crankstart_sys::playdate_display); 11 | 12 | impl Display { 13 | pub(crate) fn new(display: *const crankstart_sys::playdate_display) { 14 | unsafe { 15 | DISPLAY = Self(display); 16 | } 17 | } 18 | 19 | pub fn get() -> Self { 20 | unsafe { DISPLAY.clone() } 21 | } 22 | 23 | pub fn get_size(&self) -> Result { 24 | Ok(size2( 25 | pd_func_caller!((*self.0).getWidth)?, 26 | pd_func_caller!((*self.0).getHeight)?, 27 | )) 28 | } 29 | 30 | pub fn set_inverted(&self, inverted: bool) -> Result<(), Error> { 31 | pd_func_caller!((*self.0).setInverted, inverted as i32) 32 | } 33 | 34 | pub fn set_scale(&self, scale_factor: u32) -> Result<(), Error> { 35 | pd_func_caller!((*self.0).setScale, scale_factor) 36 | } 37 | 38 | pub fn set_mosaic(&self, amount: Vector2D) -> Result<(), Error> { 39 | pd_func_caller!((*self.0).setMosaic, amount.x, amount.y) 40 | } 41 | 42 | pub fn set_offset(&self, offset: ScreenPoint) -> Result<(), Error> { 43 | pd_func_caller!((*self.0).setOffset, offset.x, offset.y) 44 | } 45 | 46 | pub fn set_refresh_rate(&self, rate: f32) -> Result<(), Error> { 47 | pd_func_caller!((*self.0).setRefreshRate, rate) 48 | } 49 | 50 | pub fn set_flipped(&self, flipped: Vector2D) -> Result<(), Error> { 51 | pd_func_caller!((*self.0).setFlipped, flipped.x as i32, flipped.y as i32) 52 | } 53 | } 54 | 55 | static mut DISPLAY: Display = Display(ptr::null_mut()); 56 | -------------------------------------------------------------------------------- /src/file.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{log_to_console, pd_func_caller, pd_func_caller_log}, 3 | alloc::{boxed::Box, format, string::String, vec::Vec}, 4 | anyhow::{ensure, Error}, 5 | core::ptr, 6 | crankstart_sys::{ctypes::c_void, FileOptions, PDButtons, SDFile}, 7 | cstr_core::CStr, 8 | cstr_core::CString, 9 | }; 10 | 11 | pub use crankstart_sys::FileStat; 12 | 13 | fn ensure_filesystem_success(result: i32, function_name: &str) -> Result<(), Error> { 14 | if result < 0 { 15 | let file_sys = FileSystem::get(); 16 | let err_result = pd_func_caller!((*file_sys.0).geterr)?; 17 | let err_string = unsafe { CStr::from_ptr(err_result) }; 18 | 19 | Err(Error::msg(format!( 20 | "Error {} from {}: {:?}", 21 | result, function_name, err_string 22 | ))) 23 | } else { 24 | Ok(()) 25 | } 26 | } 27 | 28 | #[derive(Clone, Debug)] 29 | pub struct FileSystem(*const crankstart_sys::playdate_file); 30 | 31 | extern "C" fn list_files_callback( 32 | filename: *const crankstart_sys::ctypes::c_char, 33 | userdata: *mut core::ffi::c_void, 34 | ) { 35 | unsafe { 36 | let path = CStr::from_ptr(filename).to_string_lossy().into_owned(); 37 | let files_ptr: *mut Vec = userdata as *mut Vec; 38 | (*files_ptr).push(path); 39 | } 40 | } 41 | 42 | impl FileSystem { 43 | pub(crate) fn new(file: *const crankstart_sys::playdate_file) { 44 | unsafe { 45 | FILE_SYSTEM = FileSystem(file); 46 | } 47 | } 48 | 49 | pub fn get() -> Self { 50 | unsafe { FILE_SYSTEM.clone() } 51 | } 52 | 53 | pub fn listfiles(&self, path: &str, show_invisible: bool) -> Result, Error> { 54 | let mut files: Box> = Box::default(); 55 | let files_ptr: *mut Vec = &mut *files; 56 | let c_path = CString::new(path).map_err(Error::msg)?; 57 | let result = pd_func_caller!( 58 | (*self.0).listfiles, 59 | c_path.as_ptr(), 60 | Some(list_files_callback), 61 | files_ptr as *mut core::ffi::c_void, 62 | if show_invisible { 1 } else { 0 } 63 | )?; 64 | ensure_filesystem_success(result, "listfiles")?; 65 | Ok(*files) 66 | } 67 | 68 | pub fn stat(&self, path: &str) -> Result { 69 | let c_path = CString::new(path).map_err(Error::msg)?; 70 | let mut file_stat = FileStat::default(); 71 | let result = pd_func_caller!((*self.0).stat, c_path.as_ptr(), &mut file_stat)?; 72 | ensure_filesystem_success(result, "stat")?; 73 | Ok(file_stat) 74 | } 75 | 76 | pub fn mkdir(&self, path: &str) -> Result<(), Error> { 77 | let c_path = CString::new(path).map_err(Error::msg)?; 78 | let result = pd_func_caller!((*self.0).mkdir, c_path.as_ptr())?; 79 | ensure_filesystem_success(result, "mkdir")?; 80 | Ok(()) 81 | } 82 | 83 | pub fn unlink(&self, path: &str, recursive: bool) -> Result<(), Error> { 84 | let c_path = CString::new(path).map_err(Error::msg)?; 85 | let result = pd_func_caller!((*self.0).unlink, c_path.as_ptr(), recursive as i32)?; 86 | ensure_filesystem_success(result, "unlink")?; 87 | Ok(()) 88 | } 89 | 90 | pub fn rename(&self, from_path: &str, to_path: &str) -> Result<(), Error> { 91 | let c_from_path = CString::new(from_path).map_err(Error::msg)?; 92 | let c_to_path = CString::new(to_path).map_err(Error::msg)?; 93 | let result = pd_func_caller!((*self.0).rename, c_from_path.as_ptr(), c_to_path.as_ptr())?; 94 | ensure_filesystem_success(result, "rename")?; 95 | Ok(()) 96 | } 97 | 98 | pub fn open(&self, path: &str, options: FileOptions) -> Result { 99 | let c_path = CString::new(path).map_err(Error::msg)?; 100 | let raw_file = pd_func_caller!((*self.0).open, c_path.as_ptr(), options)?; 101 | ensure!( 102 | !raw_file.is_null(), 103 | "Failed to open file at {} with options {:?}", 104 | path, 105 | options 106 | ); 107 | Ok(File(raw_file)) 108 | } 109 | 110 | pub fn read_file_as_string(&self, path: &str) -> Result { 111 | let stat = self.stat(path)?; 112 | let mut buffer = alloc::vec![0; stat.size as usize]; 113 | let sd_file = self.open(path, FileOptions::kFileRead | FileOptions::kFileReadData)?; 114 | sd_file.read(&mut buffer)?; 115 | String::from_utf8(buffer).map_err(Error::msg) 116 | } 117 | } 118 | 119 | static mut FILE_SYSTEM: FileSystem = FileSystem(ptr::null_mut()); 120 | 121 | #[repr(i32)] 122 | #[derive(Debug, Clone, Copy)] 123 | pub enum Whence { 124 | Set = crankstart_sys::SEEK_SET as i32, 125 | Cur = crankstart_sys::SEEK_CUR as i32, 126 | End = crankstart_sys::SEEK_END as i32, 127 | } 128 | 129 | #[derive(Debug)] 130 | pub struct File(*mut SDFile); 131 | 132 | impl File { 133 | pub fn read(&self, buf: &mut [u8]) -> Result { 134 | let file_sys = FileSystem::get(); 135 | let sd_file = self.0; 136 | let result = pd_func_caller!( 137 | (*file_sys.0).read, 138 | sd_file, 139 | buf.as_mut_ptr() as *mut core::ffi::c_void, 140 | buf.len() as u32 141 | )?; 142 | ensure_filesystem_success(result, "read")?; 143 | Ok(result as usize) 144 | } 145 | 146 | pub fn write(&self, buf: &[u8]) -> Result { 147 | let file_sys = FileSystem::get(); 148 | let sd_file = self.0; 149 | let result = pd_func_caller!( 150 | (*file_sys.0).write, 151 | sd_file, 152 | buf.as_ptr() as *mut core::ffi::c_void, 153 | buf.len() as u32 154 | )?; 155 | ensure_filesystem_success(result, "write")?; 156 | Ok(result as usize) 157 | } 158 | 159 | pub fn flush(&self) -> Result<(), Error> { 160 | let file_sys = FileSystem::get(); 161 | let sd_file = self.0; 162 | let result = pd_func_caller!((*file_sys.0).flush, sd_file)?; 163 | ensure_filesystem_success(result, "flush")?; 164 | Ok(()) 165 | } 166 | 167 | pub fn tell(&self) -> Result { 168 | let file_sys = FileSystem::get(); 169 | let sd_file = self.0; 170 | let result = pd_func_caller!((*file_sys.0).tell, sd_file)?; 171 | ensure_filesystem_success(result, "tell")?; 172 | Ok(result) 173 | } 174 | 175 | pub fn seek(&self, pos: i32, whence: Whence) -> Result<(), Error> { 176 | let file_sys = FileSystem::get(); 177 | let sd_file = self.0; 178 | let result = pd_func_caller!((*file_sys.0).seek, sd_file, pos, whence as i32)?; 179 | ensure_filesystem_success(result, "seek")?; 180 | Ok(()) 181 | } 182 | } 183 | 184 | impl Drop for File { 185 | fn drop(&mut self) { 186 | let file_sys = FileSystem::get(); 187 | let sd_file = self.0; 188 | pd_func_caller_log!((*file_sys.0).close, sd_file); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/geometry.rs: -------------------------------------------------------------------------------- 1 | pub type ScreenCoord = i32; 2 | pub type ScreenPoint = euclid::default::Point2D; 3 | pub type ScreenVector = euclid::default::Vector2D; 4 | pub type ScreenRect = euclid::default::Rect; 5 | pub type ScreenSize = euclid::default::Size2D; 6 | 7 | pub type GrCoord = f32; 8 | pub type GrPoint = euclid::default::Point2D; 9 | pub type GrVector = euclid::default::Vector2D; 10 | pub type GrRect = euclid::default::Rect; 11 | pub type GrSize = euclid::default::Size2D; 12 | -------------------------------------------------------------------------------- /src/graphics.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | geometry::{ScreenPoint, ScreenRect, ScreenSize, ScreenVector}, 4 | log_to_console, pd_func_caller, pd_func_caller_log, 5 | system::System, 6 | }, 7 | alloc::{format, rc::Rc, vec::Vec}, 8 | anyhow::{anyhow, ensure, Error}, 9 | core::{cell::RefCell, ops::RangeInclusive, ptr, slice}, 10 | crankstart_sys::{ctypes::c_int, LCDBitmapTable, LCDPattern}, 11 | cstr_core::{CStr, CString}, 12 | euclid::default::{Point2D, Vector2D}, 13 | hashbrown::HashMap, 14 | }; 15 | 16 | pub use crankstart_sys::{ 17 | LCDBitmapDrawMode, LCDBitmapFlip, LCDLineCapStyle, LCDPolygonFillRule, LCDRect, LCDSolidColor, 18 | PDRect, PDStringEncoding, LCD_COLUMNS, LCD_ROWS, LCD_ROWSIZE, 19 | }; 20 | 21 | pub fn rect_make(x: f32, y: f32, width: f32, height: f32) -> PDRect { 22 | PDRect { 23 | x, 24 | y, 25 | width, 26 | height, 27 | } 28 | } 29 | 30 | #[derive(Clone, Debug)] 31 | pub enum LCDColor { 32 | Solid(LCDSolidColor), 33 | Pattern(LCDPattern), 34 | } 35 | 36 | impl From for usize { 37 | fn from(color: LCDColor) -> Self { 38 | match color { 39 | LCDColor::Solid(solid_color) => solid_color as usize, 40 | LCDColor::Pattern(pattern) => { 41 | let pattern_ptr = &pattern as *const u8; 42 | pattern_ptr as usize 43 | } 44 | } 45 | } 46 | } 47 | 48 | #[derive(Debug)] 49 | pub struct BitmapData { 50 | pub width: c_int, 51 | pub height: c_int, 52 | pub rowbytes: c_int, 53 | pub hasmask: bool, 54 | } 55 | 56 | #[derive(Debug)] 57 | pub struct BitmapInner { 58 | pub(crate) raw_bitmap: *mut crankstart_sys::LCDBitmap, 59 | owned: bool, 60 | } 61 | 62 | impl BitmapInner { 63 | pub fn get_data(&self) -> Result { 64 | let mut width = 0; 65 | let mut height = 0; 66 | let mut rowbytes = 0; 67 | let mut mask_ptr = ptr::null_mut(); 68 | pd_func_caller!( 69 | (*Graphics::get_ptr()).getBitmapData, 70 | self.raw_bitmap, 71 | &mut width, 72 | &mut height, 73 | &mut rowbytes, 74 | &mut mask_ptr, 75 | ptr::null_mut(), 76 | )?; 77 | Ok(BitmapData { 78 | width, 79 | height, 80 | rowbytes, 81 | hasmask: !mask_ptr.is_null(), 82 | }) 83 | } 84 | 85 | pub fn draw(&self, location: ScreenPoint, flip: LCDBitmapFlip) -> Result<(), Error> { 86 | pd_func_caller!( 87 | (*Graphics::get_ptr()).drawBitmap, 88 | self.raw_bitmap, 89 | location.x, 90 | location.y, 91 | flip, 92 | )?; 93 | Ok(()) 94 | } 95 | 96 | pub fn draw_scaled(&self, location: ScreenPoint, scale: Vector2D) -> Result<(), Error> { 97 | pd_func_caller!( 98 | (*Graphics::get_ptr()).drawScaledBitmap, 99 | self.raw_bitmap, 100 | location.x, 101 | location.y, 102 | scale.x, 103 | scale.y, 104 | ) 105 | } 106 | 107 | pub fn draw_rotated( 108 | &self, 109 | location: ScreenPoint, 110 | degrees: f32, 111 | center: Vector2D, 112 | scale: Vector2D, 113 | ) -> Result<(), Error> { 114 | pd_func_caller!( 115 | (*Graphics::get_ptr()).drawRotatedBitmap, 116 | self.raw_bitmap, 117 | location.x, 118 | location.y, 119 | degrees, 120 | center.x, 121 | center.y, 122 | scale.x, 123 | scale.y, 124 | ) 125 | } 126 | 127 | pub fn rotated(&self, degrees: f32, scale: Vector2D) -> Result { 128 | let raw_bitmap = pd_func_caller!( 129 | (*Graphics::get_ptr()).rotatedBitmap, 130 | self.raw_bitmap, 131 | degrees, 132 | scale.x, 133 | scale.y, 134 | // No documentation on this anywhere, but null works in testing. 135 | ptr::null_mut(), // allocedSize 136 | )?; 137 | Ok(Self { 138 | raw_bitmap, 139 | owned: true, 140 | }) 141 | } 142 | 143 | pub fn tile( 144 | &self, 145 | location: ScreenPoint, 146 | size: ScreenSize, 147 | flip: LCDBitmapFlip, 148 | ) -> Result<(), Error> { 149 | pd_func_caller!( 150 | (*Graphics::get_ptr()).tileBitmap, 151 | self.raw_bitmap, 152 | location.x, 153 | location.y, 154 | size.width, 155 | size.height, 156 | flip, 157 | )?; 158 | Ok(()) 159 | } 160 | 161 | pub fn clear(&self, color: LCDColor) -> Result<(), Error> { 162 | pd_func_caller!( 163 | (*Graphics::get_ptr()).clearBitmap, 164 | self.raw_bitmap, 165 | color.into() 166 | ) 167 | } 168 | 169 | pub fn duplicate(&self) -> Result { 170 | let raw_bitmap = pd_func_caller!((*Graphics::get_ptr()).copyBitmap, self.raw_bitmap)?; 171 | 172 | Ok(Self { 173 | raw_bitmap, 174 | owned: self.owned, 175 | }) 176 | } 177 | 178 | pub fn transform(&self, rotation: f32, scale: Vector2D) -> Result { 179 | // let raw_bitmap = pd_func_caller!( 180 | // (*Graphics::get_ptr()).transformedBitmap, 181 | // self.raw_bitmap, 182 | // rotation, 183 | // scale.x, 184 | // scale.y, 185 | // core::ptr::null_mut(), 186 | // )?; 187 | // Ok(Self { raw_bitmap }) 188 | todo!(); 189 | } 190 | 191 | pub fn into_color(&self, bitmap: Bitmap, top_left: Point2D) -> Result { 192 | let mut pattern = LCDPattern::default(); 193 | let pattern_ptr = pattern.as_mut_ptr(); 194 | let mut pattern_val = pattern_ptr as usize; 195 | let graphics = Graphics::get(); 196 | pd_func_caller!( 197 | (*graphics.0).setColorToPattern, 198 | &mut pattern_val, 199 | self.raw_bitmap, 200 | top_left.x, 201 | top_left.y 202 | )?; 203 | Ok(LCDColor::Pattern(pattern)) 204 | } 205 | 206 | pub fn load(&self, path: &str) -> Result<(), Error> { 207 | let c_path = CString::new(path).map_err(Error::msg)?; 208 | let mut out_err: *const crankstart_sys::ctypes::c_char = ptr::null_mut(); 209 | let graphics = Graphics::get(); 210 | pd_func_caller!( 211 | (*graphics.0).loadIntoBitmap, 212 | c_path.as_ptr(), 213 | self.raw_bitmap, 214 | &mut out_err 215 | )?; 216 | if !out_err.is_null() { 217 | let err_msg = unsafe { CStr::from_ptr(out_err).to_string_lossy().into_owned() }; 218 | Err(anyhow!(err_msg)) 219 | } else { 220 | Ok(()) 221 | } 222 | } 223 | 224 | pub fn check_mask_collision( 225 | &self, 226 | my_location: ScreenPoint, 227 | my_flip: LCDBitmapFlip, 228 | other: Bitmap, 229 | other_location: ScreenPoint, 230 | other_flip: LCDBitmapFlip, 231 | rect: ScreenRect, 232 | ) -> Result { 233 | let graphics = Graphics::get(); 234 | let other_raw = other.inner.borrow().raw_bitmap; 235 | let lcd_rect: LCDRect = rect.to_untyped().into(); 236 | let pixels_covered = pd_func_caller!( 237 | (*graphics.0).checkMaskCollision, 238 | self.raw_bitmap, 239 | my_location.x, 240 | my_location.y, 241 | my_flip, 242 | other_raw, 243 | other_location.x, 244 | other_location.y, 245 | other_flip, 246 | lcd_rect, 247 | )?; 248 | Ok(pixels_covered != 0) 249 | } 250 | } 251 | 252 | impl Drop for BitmapInner { 253 | fn drop(&mut self) { 254 | if self.owned { 255 | pd_func_caller_log!((*Graphics::get_ptr()).freeBitmap, self.raw_bitmap); 256 | } 257 | } 258 | } 259 | 260 | pub type BitmapInnerPtr = Rc>; 261 | 262 | #[derive(Clone, Debug)] 263 | pub struct Bitmap { 264 | pub(crate) inner: BitmapInnerPtr, 265 | } 266 | 267 | impl Bitmap { 268 | fn new(raw_bitmap: *mut crankstart_sys::LCDBitmap, owned: bool) -> Self { 269 | Bitmap { 270 | inner: Rc::new(RefCell::new(BitmapInner { raw_bitmap, owned })), 271 | } 272 | } 273 | 274 | pub fn get_data(&self) -> Result { 275 | self.inner.borrow().get_data() 276 | } 277 | 278 | pub fn draw(&self, location: ScreenPoint, flip: LCDBitmapFlip) -> Result<(), Error> { 279 | self.inner.borrow().draw(location, flip) 280 | } 281 | 282 | pub fn draw_scaled(&self, location: ScreenPoint, scale: Vector2D) -> Result<(), Error> { 283 | self.inner.borrow().draw_scaled(location, scale) 284 | } 285 | 286 | /// Draw the `Bitmap` to the given `location`, rotated `degrees` about the `center` point, 287 | /// scaled up or down in size by `scale`. `center` is given by two numbers between 0.0 and 288 | /// 1.0, where (0, 0) is the top left and (0.5, 0.5) is the center point. 289 | pub fn draw_rotated( 290 | &self, 291 | location: ScreenPoint, 292 | degrees: f32, 293 | center: Vector2D, 294 | scale: Vector2D, 295 | ) -> Result<(), Error> { 296 | self.inner 297 | .borrow() 298 | .draw_rotated(location, degrees, center, scale) 299 | } 300 | 301 | /// Return a copy of self, rotated by `degrees` and scaled up or down in size by `scale`. 302 | pub fn rotated(&self, degrees: f32, scale: Vector2D) -> Result { 303 | let raw_bitmap = self.inner.borrow().rotated(degrees, scale)?; 304 | Ok(Self { 305 | inner: Rc::new(RefCell::new(raw_bitmap)), 306 | }) 307 | } 308 | 309 | pub fn tile( 310 | &self, 311 | location: ScreenPoint, 312 | size: ScreenSize, 313 | flip: LCDBitmapFlip, 314 | ) -> Result<(), Error> { 315 | self.inner.borrow().tile(location, size, flip) 316 | } 317 | 318 | pub fn clear(&self, color: LCDColor) -> Result<(), Error> { 319 | self.inner.borrow().clear(color) 320 | } 321 | 322 | pub fn transform(&self, rotation: f32, scale: Vector2D) -> Result { 323 | let inner = self.inner.borrow().transform(rotation, scale)?; 324 | Ok(Self { 325 | inner: Rc::new(RefCell::new(inner)), 326 | }) 327 | } 328 | 329 | pub fn into_color(&self, bitmap: Bitmap, top_left: Point2D) -> Result { 330 | self.inner.borrow().into_color(bitmap, top_left) 331 | } 332 | 333 | pub fn load(&self, path: &str) -> Result<(), Error> { 334 | self.inner.borrow().load(path) 335 | } 336 | 337 | pub fn check_mask_collision( 338 | &self, 339 | my_location: ScreenPoint, 340 | my_flip: LCDBitmapFlip, 341 | other: Bitmap, 342 | other_location: ScreenPoint, 343 | other_flip: LCDBitmapFlip, 344 | rect: ScreenRect, 345 | ) -> Result { 346 | self.inner.borrow().check_mask_collision( 347 | my_location, 348 | my_flip, 349 | other, 350 | other_location, 351 | other_flip, 352 | rect, 353 | ) 354 | } 355 | } 356 | 357 | type OptionalBitmap<'a> = Option<&'a mut Bitmap>; 358 | 359 | fn raw_bitmap(bitmap: OptionalBitmap<'_>) -> *mut crankstart_sys::LCDBitmap { 360 | if let Some(bitmap) = bitmap { 361 | bitmap.inner.borrow().raw_bitmap 362 | } else { 363 | ptr::null_mut() 364 | } 365 | } 366 | 367 | pub struct Font(*mut crankstart_sys::LCDFont); 368 | 369 | impl Font { 370 | pub fn new(font: *mut crankstart_sys::LCDFont) -> Result { 371 | anyhow::ensure!(!font.is_null(), "Null pointer passed to Font::new"); 372 | Ok(Self(font)) 373 | } 374 | } 375 | 376 | impl Drop for Font { 377 | fn drop(&mut self) { 378 | log_to_console!("Leaking a font"); 379 | } 380 | } 381 | 382 | #[derive(Debug)] 383 | struct BitmapTableInner { 384 | raw_bitmap_table: *mut LCDBitmapTable, 385 | bitmaps: HashMap, 386 | } 387 | 388 | impl BitmapTableInner { 389 | fn get_bitmap(&mut self, index: usize) -> Result { 390 | if let Some(bitmap) = self.bitmaps.get(&index) { 391 | Ok(bitmap.clone()) 392 | } else { 393 | let raw_bitmap = pd_func_caller!( 394 | (*Graphics::get_ptr()).getTableBitmap, 395 | self.raw_bitmap_table, 396 | index as c_int 397 | )?; 398 | ensure!( 399 | !raw_bitmap.is_null(), 400 | "Failed to load bitmap {} from table {:?}", 401 | index, 402 | self.raw_bitmap_table 403 | ); 404 | let bitmap = Bitmap::new(raw_bitmap, true); 405 | self.bitmaps.insert(index, bitmap.clone()); 406 | Ok(bitmap) 407 | } 408 | } 409 | 410 | fn load(&mut self, path: &str) -> Result<(), Error> { 411 | let c_path = CString::new(path).map_err(Error::msg)?; 412 | let mut out_err: *const crankstart_sys::ctypes::c_char = ptr::null_mut(); 413 | let graphics = Graphics::get(); 414 | pd_func_caller!( 415 | (*graphics.0).loadIntoBitmapTable, 416 | c_path.as_ptr(), 417 | self.raw_bitmap_table, 418 | &mut out_err 419 | )?; 420 | if !out_err.is_null() { 421 | let err_msg = unsafe { CStr::from_ptr(out_err).to_string_lossy().into_owned() }; 422 | Err(anyhow!(err_msg)) 423 | } else { 424 | Ok(()) 425 | } 426 | } 427 | } 428 | 429 | impl Drop for BitmapTableInner { 430 | fn drop(&mut self) { 431 | pd_func_caller_log!( 432 | (*Graphics::get_ptr()).freeBitmapTable, 433 | self.raw_bitmap_table 434 | ); 435 | } 436 | } 437 | 438 | type BitmapTableInnerPtr = Rc>; 439 | 440 | #[derive(Clone, Debug)] 441 | pub struct BitmapTable { 442 | inner: BitmapTableInnerPtr, 443 | } 444 | 445 | impl BitmapTable { 446 | pub fn new(raw_bitmap_table: *mut LCDBitmapTable) -> Self { 447 | Self { 448 | inner: Rc::new(RefCell::new(BitmapTableInner { 449 | raw_bitmap_table, 450 | bitmaps: HashMap::new(), 451 | })), 452 | } 453 | } 454 | 455 | pub fn load(&self, path: &str) -> Result<(), Error> { 456 | self.inner.borrow_mut().load(path) 457 | } 458 | 459 | pub fn get_bitmap(&self, index: usize) -> Result { 460 | self.inner.borrow_mut().get_bitmap(index) 461 | } 462 | } 463 | 464 | static mut GRAPHICS: Graphics = Graphics(ptr::null_mut()); 465 | 466 | #[derive(Clone, Debug)] 467 | pub struct Graphics(*const crankstart_sys::playdate_graphics); 468 | 469 | impl Graphics { 470 | pub(crate) fn new(graphics: *const crankstart_sys::playdate_graphics) { 471 | unsafe { 472 | GRAPHICS = Self(graphics); 473 | } 474 | } 475 | 476 | pub fn get() -> Self { 477 | unsafe { GRAPHICS.clone() } 478 | } 479 | 480 | pub fn get_ptr() -> *const crankstart_sys::playdate_graphics { 481 | Self::get().0 482 | } 483 | 484 | /// Allows drawing directly into an image rather than the framebuffer, for example for 485 | /// drawing text into a sprite's image. 486 | pub fn with_context(&self, bitmap: &Bitmap, f: F) -> Result 487 | where 488 | F: FnOnce() -> Result, 489 | { 490 | // Any calls in this context are directly modifying the bitmap, so borrow mutably 491 | // for safety. 492 | self.push_context(bitmap.inner.borrow_mut().raw_bitmap)?; 493 | let res = f(); 494 | self.pop_context()?; 495 | res 496 | } 497 | 498 | /// Internal function; use `with_context`. 499 | fn push_context(&self, raw_bitmap: *mut crankstart_sys::LCDBitmap) -> Result<(), Error> { 500 | pd_func_caller!((*self.0).pushContext, raw_bitmap) 501 | } 502 | 503 | /// Clear the context stack for graphics to make all drawing go to the display framebuffer. 504 | pub fn clear_context(&self) -> Result<(), Error> { 505 | pd_func_caller!((*self.0).pushContext, core::ptr::null_mut()) 506 | } 507 | 508 | /// Internal function; use `with_context`. 509 | fn pop_context(&self) -> Result<(), Error> { 510 | pd_func_caller!((*self.0).popContext) 511 | } 512 | 513 | pub fn get_frame(&self) -> Result<&'static mut [u8], Error> { 514 | let ptr = pd_func_caller!((*self.0).getFrame)?; 515 | anyhow::ensure!(!ptr.is_null(), "Null pointer returned from getFrame"); 516 | let frame = unsafe { slice::from_raw_parts_mut(ptr, (LCD_ROWSIZE * LCD_ROWS) as usize) }; 517 | Ok(frame) 518 | } 519 | 520 | pub fn get_display_frame(&self) -> Result<&'static mut [u8], Error> { 521 | let ptr = pd_func_caller!((*self.0).getDisplayFrame)?; 522 | anyhow::ensure!(!ptr.is_null(), "Null pointer returned from getDisplayFrame"); 523 | let frame = unsafe { slice::from_raw_parts_mut(ptr, (LCD_ROWSIZE * LCD_ROWS) as usize) }; 524 | Ok(frame) 525 | } 526 | 527 | pub fn get_debug_bitmap(&self) -> Result { 528 | let raw_bitmap = pd_func_caller!((*self.0).getDebugBitmap)?; 529 | anyhow::ensure!( 530 | !raw_bitmap.is_null(), 531 | "Null pointer returned from getDebugImage" 532 | ); 533 | Ok(Bitmap::new(raw_bitmap, false)) 534 | } 535 | 536 | pub fn get_framebuffer_bitmap(&self) -> Result { 537 | let raw_bitmap = pd_func_caller!((*self.0).copyFrameBufferBitmap)?; 538 | anyhow::ensure!( 539 | !raw_bitmap.is_null(), 540 | "Null pointer returned from getFrameBufferBitmap" 541 | ); 542 | Ok(Bitmap::new(raw_bitmap, true)) 543 | } 544 | 545 | pub fn set_background_color(&self, color: LCDSolidColor) -> Result<(), Error> { 546 | pd_func_caller!((*self.0).setBackgroundColor, color) 547 | } 548 | 549 | pub fn set_draw_mode(&self, mode: LCDBitmapDrawMode) -> Result { 550 | pd_func_caller!((*self.0).setDrawMode, mode) 551 | } 552 | 553 | pub fn mark_updated_rows(&self, range: RangeInclusive) -> Result<(), Error> { 554 | let (start, end) = range.into_inner(); 555 | pd_func_caller!((*self.0).markUpdatedRows, start, end) 556 | } 557 | 558 | pub fn display(&self) -> Result<(), Error> { 559 | pd_func_caller!((*self.0).display) 560 | } 561 | 562 | pub fn set_draw_offset(&self, offset: ScreenVector) -> Result<(), Error> { 563 | pd_func_caller!((*self.0).setDrawOffset, offset.x, offset.y) 564 | } 565 | 566 | pub fn new_bitmap(&self, size: ScreenSize, bg_color: LCDColor) -> Result { 567 | let raw_bitmap = pd_func_caller!( 568 | (*self.0).newBitmap, 569 | size.width, 570 | size.height, 571 | bg_color.into() 572 | )?; 573 | anyhow::ensure!( 574 | !raw_bitmap.is_null(), 575 | "Null pointer returned from new_bitmap" 576 | ); 577 | Ok(Bitmap::new(raw_bitmap, true)) 578 | } 579 | 580 | pub fn load_bitmap(&self, path: &str) -> Result { 581 | let c_path = CString::new(path).map_err(Error::msg)?; 582 | let mut out_err: *const crankstart_sys::ctypes::c_char = ptr::null_mut(); 583 | let raw_bitmap = pd_func_caller!((*self.0).loadBitmap, c_path.as_ptr(), &mut out_err)?; 584 | if raw_bitmap.is_null() { 585 | if !out_err.is_null() { 586 | let err_msg = unsafe { CStr::from_ptr(out_err).to_string_lossy().into_owned() }; 587 | Err(anyhow!(err_msg)) 588 | } else { 589 | Err(anyhow!( 590 | "load_bitmap failed without providing an error message" 591 | )) 592 | } 593 | } else { 594 | Ok(Bitmap::new(raw_bitmap, true)) 595 | } 596 | } 597 | 598 | pub fn new_bitmap_table(&self, count: usize, size: ScreenSize) -> Result { 599 | let raw_bitmap_table = pd_func_caller!( 600 | (*self.0).newBitmapTable, 601 | count as i32, 602 | size.width, 603 | size.height 604 | )?; 605 | 606 | Ok(BitmapTable::new(raw_bitmap_table)) 607 | } 608 | 609 | pub fn load_bitmap_table(&self, path: &str) -> Result { 610 | let c_path = CString::new(path).map_err(Error::msg)?; 611 | let mut out_err: *const crankstart_sys::ctypes::c_char = ptr::null_mut(); 612 | let raw_bitmap_table = 613 | pd_func_caller!((*self.0).loadBitmapTable, c_path.as_ptr(), &mut out_err)?; 614 | if raw_bitmap_table.is_null() { 615 | if !out_err.is_null() { 616 | let err_msg = unsafe { CStr::from_ptr(out_err).to_string_lossy().into_owned() }; 617 | Err(anyhow!(err_msg)) 618 | } else { 619 | Err(anyhow!( 620 | "load_bitmap_table failed without providing an error message" 621 | )) 622 | } 623 | } else { 624 | Ok(BitmapTable::new(raw_bitmap_table)) 625 | } 626 | } 627 | 628 | pub fn clear(&self, color: LCDColor) -> Result<(), Error> { 629 | pd_func_caller!((*self.0).clear, color.into()) 630 | } 631 | 632 | pub fn draw_line( 633 | &self, 634 | p1: ScreenPoint, 635 | p2: ScreenPoint, 636 | width: i32, 637 | color: LCDColor, 638 | ) -> Result<(), Error> { 639 | pd_func_caller!( 640 | (*self.0).drawLine, 641 | p1.x, 642 | p1.y, 643 | p2.x, 644 | p2.y, 645 | width, 646 | color.into(), 647 | ) 648 | } 649 | 650 | pub fn fill_polygon( 651 | &self, 652 | coords: &[ScreenPoint], 653 | color: LCDColor, 654 | fillrule: LCDPolygonFillRule, 655 | ) -> Result<(), Error> { 656 | let n_pts = coords.len(); 657 | let mut coords_seq = coords 658 | .iter() 659 | .flat_map(|pt| [pt.x, pt.y]) 660 | .collect::>(); 661 | 662 | pd_func_caller!( 663 | (*self.0).fillPolygon, 664 | n_pts as i32, 665 | coords_seq.as_mut_ptr(), 666 | color.into(), 667 | fillrule 668 | )?; 669 | 670 | Ok(()) 671 | } 672 | 673 | pub fn fill_triangle( 674 | &self, 675 | p1: ScreenPoint, 676 | p2: ScreenPoint, 677 | p3: ScreenPoint, 678 | color: LCDColor, 679 | ) -> Result<(), Error> { 680 | pd_func_caller!( 681 | (*self.0).fillTriangle, 682 | p1.x, 683 | p1.y, 684 | p2.x, 685 | p2.y, 686 | p3.x, 687 | p3.y, 688 | color.into(), 689 | ) 690 | } 691 | 692 | pub fn draw_rect(&self, rect: ScreenRect, color: LCDColor) -> Result<(), Error> { 693 | pd_func_caller!( 694 | (*self.0).drawRect, 695 | rect.origin.x, 696 | rect.origin.y, 697 | rect.size.width, 698 | rect.size.height, 699 | color.into(), 700 | ) 701 | } 702 | 703 | pub fn fill_rect(&self, rect: ScreenRect, color: LCDColor) -> Result<(), Error> { 704 | pd_func_caller!( 705 | (*self.0).fillRect, 706 | rect.origin.x, 707 | rect.origin.y, 708 | rect.size.width, 709 | rect.size.height, 710 | color.into(), 711 | ) 712 | } 713 | 714 | pub fn draw_ellipse( 715 | &self, 716 | origin: ScreenPoint, 717 | size: ScreenSize, 718 | line_width: i32, 719 | start_angle: f32, 720 | end_angle: f32, 721 | color: LCDColor, 722 | ) -> Result<(), Error> { 723 | pd_func_caller!( 724 | (*self.0).drawEllipse, 725 | origin.x, 726 | origin.y, 727 | size.width, 728 | size.height, 729 | line_width, 730 | start_angle, 731 | end_angle, 732 | color.into(), 733 | ) 734 | } 735 | 736 | pub fn fill_ellipse( 737 | &self, 738 | target: OptionalBitmap, 739 | stencil: OptionalBitmap, 740 | origin: ScreenPoint, 741 | size: ScreenSize, 742 | line_width: i32, 743 | start_angle: f32, 744 | end_angle: f32, 745 | color: LCDColor, 746 | clip: LCDRect, 747 | ) -> Result<(), Error> { 748 | pd_func_caller!( 749 | (*self.0).fillEllipse, 750 | origin.x, 751 | origin.y, 752 | size.width, 753 | size.height, 754 | start_angle, 755 | end_angle, 756 | color.into(), 757 | ) 758 | } 759 | 760 | pub fn load_font(&self, path: &str) -> Result { 761 | let c_path = CString::new(path).map_err(Error::msg)?; 762 | let mut out_err: *const crankstart_sys::ctypes::c_char = ptr::null_mut(); 763 | let font = pd_func_caller!((*self.0).loadFont, c_path.as_ptr(), &mut out_err)?; 764 | if font.is_null() { 765 | if !out_err.is_null() { 766 | let err_msg = unsafe { CStr::from_ptr(out_err).to_string_lossy().into_owned() }; 767 | Err(anyhow!(err_msg)) 768 | } else { 769 | Err(anyhow!( 770 | "load_font failed without providing an error message" 771 | )) 772 | } 773 | } else { 774 | Font::new(font) 775 | } 776 | } 777 | 778 | pub fn set_font(&self, font: &Font) -> Result<(), Error> { 779 | pd_func_caller_log!((*self.0).setFont, font.0); 780 | Ok(()) 781 | } 782 | 783 | pub fn draw_text(&self, text: &str, position: ScreenPoint) -> Result { 784 | let c_text = CString::new(text).map_err(Error::msg)?; 785 | pd_func_caller!( 786 | (*self.0).drawText, 787 | c_text.as_ptr() as *const core::ffi::c_void, 788 | text.len(), 789 | PDStringEncoding::kUTF8Encoding, 790 | position.x, 791 | position.y, 792 | ) 793 | } 794 | 795 | pub fn get_text_width(&self, font: &Font, text: &str, tracking: i32) -> Result { 796 | let c_text = CString::new(text).map_err(Error::msg)?; 797 | pd_func_caller!( 798 | (*self.0).getTextWidth, 799 | font.0, 800 | c_text.as_ptr() as *const core::ffi::c_void, 801 | text.len(), 802 | PDStringEncoding::kUTF8Encoding, 803 | tracking, 804 | ) 805 | } 806 | 807 | pub fn get_font_height(&self, font: &Font) -> Result { 808 | pd_func_caller!((*self.0).getFontHeight, font.0) 809 | } 810 | 811 | pub fn get_system_text_width(&self, text: &str, tracking: i32) -> Result { 812 | let c_text = CString::new(text).map_err(Error::msg)?; 813 | pd_func_caller!( 814 | (*self.0).getTextWidth, 815 | ptr::null_mut(), 816 | c_text.as_ptr() as *const core::ffi::c_void, 817 | text.len(), 818 | PDStringEncoding::kUTF8Encoding, 819 | tracking, 820 | ) 821 | } 822 | } 823 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(internal_features)] 3 | #![feature(lang_items, alloc_error_handler, core_intrinsics)] 4 | #![allow(unused_variables, dead_code, unused_imports)] 5 | 6 | extern crate alloc; 7 | 8 | pub mod display; 9 | pub mod file; 10 | pub mod geometry; 11 | pub mod graphics; 12 | pub mod lua; 13 | pub mod sound; 14 | pub mod sprite; 15 | pub mod system; 16 | 17 | use { 18 | crate::{ 19 | display::Display, 20 | file::FileSystem, 21 | graphics::{Graphics, PDRect}, 22 | lua::Lua, 23 | sound::Sound, 24 | sprite::{ 25 | Sprite, SpriteCollideFunction, SpriteDrawFunction, SpriteManager, SpriteUpdateFunction, 26 | }, 27 | system::System, 28 | }, 29 | alloc::boxed::Box, 30 | anyhow::Error, 31 | core::{fmt, panic::PanicInfo}, 32 | crankstart_sys::{playdate_sprite, LCDRect, LCDSprite, SpriteCollisionResponseType}, 33 | }; 34 | 35 | pub struct Playdate { 36 | playdate: *const crankstart_sys::PlaydateAPI, 37 | } 38 | 39 | impl Playdate { 40 | pub fn new( 41 | playdate: *const crankstart_sys::PlaydateAPI, 42 | sprite_update: SpriteUpdateFunction, 43 | sprite_draw: SpriteDrawFunction, 44 | ) -> Result { 45 | let playdate_api = unsafe { *playdate }; 46 | let system = playdate_api.system; 47 | System::new(system); 48 | let playdate_sprite = playdate_api.sprite; 49 | SpriteManager::new(playdate_sprite, sprite_update, sprite_draw); 50 | let file = playdate_api.file; 51 | FileSystem::new(file); 52 | let graphics = playdate_api.graphics; 53 | Graphics::new(graphics); 54 | let lua = playdate_api.lua; 55 | Lua::new(lua); 56 | let sound = playdate_api.sound; 57 | Sound::new(sound)?; 58 | let display = playdate_api.display; 59 | Display::new(display); 60 | Ok(Self { playdate }) 61 | } 62 | } 63 | 64 | #[macro_export] 65 | macro_rules! log_to_console { 66 | ($($arg:tt)*) => ($crate::system::System::log_to_console(&alloc::format!($($arg)*))); 67 | } 68 | 69 | #[macro_export] 70 | macro_rules! pd_func_caller { 71 | ($raw_fn_opt:expr, $($arg:tt)*) => { 72 | unsafe { 73 | use alloc::format; 74 | let raw_fn = $raw_fn_opt 75 | .ok_or_else(|| anyhow::anyhow!("{} did not contain a function pointer", stringify!($raw_fn_opt)))?; 76 | Ok::<_, Error>(raw_fn($($arg)*)) 77 | } 78 | }; 79 | ($raw_fn_opt:expr) => { 80 | unsafe { 81 | use alloc::format; 82 | let raw_fn = $raw_fn_opt 83 | .ok_or_else(|| anyhow::anyhow!("{} did not contain a function pointer", stringify!($raw_fn_opt)))?; 84 | Ok::<_, Error>(raw_fn()) 85 | } 86 | }; 87 | } 88 | 89 | #[macro_export] 90 | macro_rules! pd_func_caller_log { 91 | ($raw_fn_opt:expr, $($arg:tt)*) => { 92 | unsafe { 93 | if let Some(raw_fn) = $raw_fn_opt { 94 | raw_fn($($arg)*); 95 | } else { 96 | $crate::log_to_console!("{} did not contain a function pointer", stringify!($raw_fn_opt)); 97 | } 98 | } 99 | }; 100 | } 101 | 102 | pub trait Game { 103 | fn update_sprite(&mut self, sprite: &mut Sprite, playdate: &mut Playdate) -> Result<(), Error> { 104 | use alloc::format; 105 | Err(anyhow::anyhow!("Error: sprite {:?} needs update but this game hasn't implemented the update_sprite trait method", sprite)) 106 | } 107 | 108 | fn draw_sprite( 109 | &self, 110 | sprite: &Sprite, 111 | bounds: &PDRect, 112 | draw_rect: &PDRect, 113 | playdate: &Playdate, 114 | ) -> Result<(), Error> { 115 | use alloc::format; 116 | Err(anyhow::anyhow!("Error: sprite {:?} needs to draw but this game hasn't implemented the draw_sprite trait method", sprite)) 117 | } 118 | 119 | fn update(&mut self, playdate: &mut Playdate) -> Result<(), Error>; 120 | 121 | fn draw_fps(&self) -> bool { 122 | false 123 | } 124 | 125 | fn draw_and_update_sprites(&self) -> bool { 126 | true 127 | } 128 | } 129 | 130 | pub type GamePtr = Box; 131 | 132 | pub struct GameRunner { 133 | game: Option>, 134 | init_failed: bool, 135 | playdate: Playdate, 136 | } 137 | 138 | impl GameRunner { 139 | pub fn new(game: Option>, playdate: Playdate) -> Self { 140 | Self { 141 | init_failed: false, 142 | game, 143 | playdate, 144 | } 145 | } 146 | 147 | pub fn update(&mut self) { 148 | if self.init_failed { 149 | return; 150 | } 151 | 152 | if let Some(game) = self.game.as_mut() { 153 | if let Err(err) = game.update(&mut self.playdate) { 154 | log_to_console!("Error in update: {err:#}") 155 | } 156 | if game.draw_and_update_sprites() { 157 | if let Err(err) = SpriteManager::get_mut().update_and_draw_sprites() { 158 | log_to_console!("Error from sprite_manager.update_and_draw_sprites: {err:#}") 159 | } 160 | } 161 | if game.draw_fps() { 162 | if let Err(err) = System::get().draw_fps(0, 0) { 163 | log_to_console!("Error from system().draw_fps: {err:#}") 164 | } 165 | } 166 | } else { 167 | log_to_console!("can't get game to update"); 168 | self.init_failed = true; 169 | } 170 | } 171 | 172 | pub fn update_sprite(&mut self, sprite: *mut LCDSprite) { 173 | if let Some(game) = self.game.as_mut() { 174 | if let Some(mut sprite) = SpriteManager::get_mut().get_sprite(sprite) { 175 | if let Err(err) = game.update_sprite(&mut sprite, &mut self.playdate) { 176 | log_to_console!("Error in update_sprite: {err:#}") 177 | } 178 | } else { 179 | log_to_console!("Can't find sprite {sprite:?} to update"); 180 | } 181 | } else { 182 | log_to_console!("can't get game to update_sprite"); 183 | } 184 | } 185 | 186 | pub fn draw_sprite(&mut self, sprite: *mut LCDSprite, bounds: PDRect, draw_rect: PDRect) { 187 | if let Some(game) = self.game.as_ref() { 188 | if let Some(sprite) = SpriteManager::get_mut().get_sprite(sprite) { 189 | if let Err(err) = game.draw_sprite(&sprite, &bounds, &draw_rect, &self.playdate) { 190 | log_to_console!("Error in draw_sprite: {err:#}") 191 | } 192 | } else { 193 | log_to_console!("Can't find sprite {sprite:?} to draw"); 194 | } 195 | } else { 196 | log_to_console!("can't get game to draw_sprite"); 197 | } 198 | } 199 | 200 | pub fn playdate_sprite(&self) -> *const playdate_sprite { 201 | SpriteManager::get_mut().playdate_sprite 202 | } 203 | } 204 | 205 | #[macro_export] 206 | macro_rules! crankstart_game { 207 | ($game_struct:ty) => { 208 | crankstart_game!($game_struct, PDSystemEvent::kEventInit); 209 | }; 210 | ($game_struct:ty, $pd_system_event:expr) => { 211 | pub mod game_setup { 212 | extern crate alloc; 213 | use super::*; 214 | use { 215 | alloc::{boxed::Box, format}, 216 | crankstart::{ 217 | graphics::PDRect, log_to_console, sprite::SpriteManager, system::System, 218 | GameRunner, Playdate, 219 | }, 220 | crankstart_sys::{ 221 | LCDRect, LCDSprite, PDSystemEvent, PlaydateAPI, SpriteCollisionResponseType, 222 | }, 223 | }; 224 | 225 | static mut GAME_RUNNER: Option> = None; 226 | 227 | extern "C" fn sprite_update(sprite: *mut LCDSprite) { 228 | let game_runner = unsafe { GAME_RUNNER.as_mut().expect("GAME_RUNNER") }; 229 | game_runner.update_sprite(sprite); 230 | } 231 | 232 | extern "C" fn sprite_draw(sprite: *mut LCDSprite, bounds: PDRect, drawrect: PDRect) { 233 | let game_runner = unsafe { GAME_RUNNER.as_mut().expect("GAME_RUNNER") }; 234 | game_runner.draw_sprite(sprite, bounds, drawrect); 235 | } 236 | 237 | extern "C" fn update(_user_data: *mut core::ffi::c_void) -> i32 { 238 | let game_runner = unsafe { GAME_RUNNER.as_mut().expect("GAME_RUNNER") }; 239 | 240 | game_runner.update(); 241 | 242 | 1 243 | } 244 | 245 | #[no_mangle] 246 | extern "C" fn eventHandler( 247 | playdate: *mut PlaydateAPI, 248 | event: PDSystemEvent, 249 | _arg: u32, 250 | ) -> crankstart_sys::ctypes::c_int { 251 | if event == $pd_system_event { 252 | // This would only fail if PlaydateAPI has null pointers, which shouldn't happen. 253 | let mut playdate = match Playdate::new(playdate, sprite_update, sprite_draw) { 254 | Ok(playdate) => playdate, 255 | Err(e) => { 256 | log_to_console!("Failed to construct Playdate system: {e:#}"); 257 | return 1; 258 | } 259 | }; 260 | System::get() 261 | .set_update_callback(Some(update)) 262 | .unwrap_or_else(|err| { 263 | log_to_console!("Got error while setting update callback: {err:#}"); 264 | }); 265 | let game = match <$game_struct>::new(&mut playdate) { 266 | Ok(game) => Some(game), 267 | Err(err) => { 268 | log_to_console!("Got error while creating game: {err:#}"); 269 | None 270 | } 271 | }; 272 | 273 | unsafe { 274 | GAME_RUNNER = Some(GameRunner::new(game, playdate)); 275 | } 276 | } 277 | 0 278 | } 279 | } 280 | }; 281 | } 282 | 283 | fn abort_with_addr(addr: usize) -> ! { 284 | let p = addr as *mut i32; 285 | unsafe { 286 | *p = 0; 287 | } 288 | core::intrinsics::abort() 289 | } 290 | 291 | #[panic_handler] 292 | fn panic(#[allow(unused)] panic_info: &PanicInfo) -> ! { 293 | use arrayvec::ArrayString; 294 | use core::fmt::Write; 295 | if let Some(location) = panic_info.location() { 296 | let mut output = ArrayString::<1024>::new(); 297 | let payload = if let Some(payload) = panic_info.payload().downcast_ref::<&str>() { 298 | payload 299 | } else { 300 | "no payload" 301 | }; 302 | write!( 303 | output, 304 | "panic: {} @ {}:{}\0", 305 | payload, 306 | location.file(), 307 | location.line() 308 | ) 309 | .expect("write"); 310 | System::log_to_console(output.as_str()); 311 | } else { 312 | System::log_to_console("panic\0"); 313 | } 314 | #[cfg(target_os = "macos")] 315 | { 316 | unsafe { 317 | core::intrinsics::breakpoint(); 318 | } 319 | abort_with_addr(0xdeadbeef); 320 | } 321 | #[cfg(not(target_os = "macos"))] 322 | { 323 | abort_with_addr(0xdeadbeef); 324 | } 325 | } 326 | 327 | use core::alloc::{GlobalAlloc, Layout}; 328 | 329 | pub(crate) struct PlaydateAllocator; 330 | 331 | unsafe impl Sync for PlaydateAllocator {} 332 | 333 | unsafe impl GlobalAlloc for PlaydateAllocator { 334 | unsafe fn alloc(&self, layout: Layout) -> *mut u8 { 335 | let system = System::get(); 336 | system.realloc(core::ptr::null_mut(), layout.size()) as *mut u8 337 | } 338 | 339 | unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) { 340 | let system = System::get(); 341 | system.realloc(ptr as *mut core::ffi::c_void, 0); 342 | } 343 | 344 | unsafe fn realloc(&self, ptr: *mut u8, _layout: Layout, new_size: usize) -> *mut u8 { 345 | System::get().realloc(ptr as *mut core::ffi::c_void, new_size) as *mut u8 346 | } 347 | } 348 | 349 | #[global_allocator] 350 | pub(crate) static mut A: PlaydateAllocator = PlaydateAllocator; 351 | 352 | // define what happens in an Out Of Memory (OOM) condition 353 | #[alloc_error_handler] 354 | fn alloc_error(_layout: Layout) -> ! { 355 | System::log_to_console("Out of Memory\0"); 356 | abort_with_addr(0xDEADFA11); 357 | } 358 | 359 | #[cfg(target_os = "macos")] 360 | #[no_mangle] 361 | pub unsafe extern "C" fn memcpy(dest: *mut u8, src: *const u8, n: usize) -> *mut u8 { 362 | let mut i = 0; 363 | while i < n { 364 | *dest.add(i) = *src.add(i); 365 | i += 1; 366 | } 367 | dest 368 | } 369 | 370 | #[cfg(target_os = "macos")] 371 | #[no_mangle] 372 | pub unsafe extern "C" fn memmove(dest: *mut u8, src: *const u8, n: usize) -> *mut u8 { 373 | if src < dest as *const u8 { 374 | // copy from end 375 | let mut i = n; 376 | while i != 0 { 377 | i -= 1; 378 | *dest.add(i) = *src.add(i); 379 | } 380 | } else { 381 | // copy from beginning 382 | let mut i = 0; 383 | while i < n { 384 | *dest.add(i) = *src.add(i); 385 | i += 1; 386 | } 387 | } 388 | dest 389 | } 390 | 391 | #[cfg(target_os = "macos")] 392 | #[no_mangle] 393 | pub unsafe extern "C" fn memcmp(s1: *const u8, s2: *const u8, n: usize) -> i32 { 394 | let mut i = 0; 395 | while i < n { 396 | let a = *s1.add(i); 397 | let b = *s2.add(i); 398 | if a != b { 399 | return a as i32 - b as i32; 400 | } 401 | i += 1; 402 | } 403 | 0 404 | } 405 | 406 | #[cfg(target_os = "macos")] 407 | #[no_mangle] 408 | pub unsafe extern "C" fn bcmp(s1: *const u8, s2: *const u8, n: usize) -> i32 { 409 | memcmp(s1, s2, n) 410 | } 411 | 412 | #[cfg(target_os = "macos")] 413 | pub unsafe fn memset_internal(s: *mut u8, c: crankstart_sys::ctypes::c_int, n: usize) -> *mut u8 { 414 | let mut i = 0; 415 | while i < n { 416 | *s.add(i) = c as u8; 417 | i += 1; 418 | } 419 | s 420 | } 421 | 422 | #[cfg(target_os = "macos")] 423 | #[no_mangle] 424 | pub unsafe extern "C" fn memset(s: *mut u8, c: crankstart_sys::ctypes::c_int, n: usize) -> *mut u8 { 425 | memset_internal(s, c, n) 426 | } 427 | 428 | #[cfg(target_os = "macos")] 429 | #[no_mangle] 430 | pub unsafe extern "C" fn __bzero(s: *mut u8, n: usize) { 431 | memset_internal(s, 0, n); 432 | } 433 | 434 | #[no_mangle] 435 | pub extern "C" fn _sbrk() {} 436 | 437 | #[cfg(not(target_os = "windows"))] 438 | #[no_mangle] 439 | pub extern "C" fn _write() {} 440 | 441 | #[cfg(not(target_os = "windows"))] 442 | #[no_mangle] 443 | pub extern "C" fn _close() {} 444 | 445 | #[cfg(not(target_os = "windows"))] 446 | #[no_mangle] 447 | pub extern "C" fn _lseek() {} 448 | 449 | #[cfg(not(target_os = "windows"))] 450 | #[no_mangle] 451 | pub extern "C" fn _read() {} 452 | 453 | #[no_mangle] 454 | pub extern "C" fn _fstat() {} 455 | 456 | #[no_mangle] 457 | pub extern "C" fn _isatty() {} 458 | 459 | #[cfg(not(target_os = "windows"))] 460 | #[no_mangle] 461 | pub extern "C" fn _exit() {} 462 | 463 | #[no_mangle] 464 | pub extern "C" fn _open() {} 465 | 466 | #[no_mangle] 467 | pub extern "C" fn _kill() {} 468 | 469 | #[no_mangle] 470 | pub extern "C" fn _getpid() {} 471 | 472 | #[no_mangle] 473 | pub extern "C" fn rust_eh_personality() { 474 | unimplemented!(); 475 | } 476 | 477 | #[cfg(target_os = "macos")] 478 | #[no_mangle] 479 | extern "C" fn _Unwind_Resume() { 480 | unimplemented!(); 481 | } 482 | 483 | #[no_mangle] 484 | extern "C" fn __exidx_start() { 485 | unimplemented!(); 486 | } 487 | 488 | #[no_mangle] 489 | extern "C" fn __exidx_end() { 490 | unimplemented!(); 491 | } 492 | 493 | #[cfg(target_os = "macos")] 494 | #[link(name = "System")] 495 | extern "C" {} 496 | -------------------------------------------------------------------------------- /src/lua.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::pd_func_caller, 3 | alloc::string::String, 4 | anyhow::{anyhow, Error}, 5 | core::ptr, 6 | crankstart_sys::{ctypes, lua_CFunction}, 7 | cstr_core::{CStr, CString}, 8 | }; 9 | 10 | static mut LUA: Lua = Lua(ptr::null_mut()); 11 | 12 | #[derive(Clone, Debug)] 13 | pub struct Lua(*const crankstart_sys::playdate_lua); 14 | 15 | impl Lua { 16 | pub(crate) fn new(file: *const crankstart_sys::playdate_lua) { 17 | unsafe { 18 | LUA = Lua(file); 19 | } 20 | } 21 | 22 | pub fn get() -> Self { 23 | unsafe { LUA.clone() } 24 | } 25 | 26 | pub fn add_function(&self, f: lua_CFunction, name: &str) -> Result<(), Error> { 27 | let c_name = CString::new(name).map_err(Error::msg)?; 28 | let mut out_err: *const crankstart_sys::ctypes::c_char = ptr::null_mut(); 29 | pd_func_caller!((*self.0).addFunction, f, c_name.as_ptr(), &mut out_err)?; 30 | if !out_err.is_null() { 31 | let err_msg = unsafe { CStr::from_ptr(out_err).to_string_lossy().into_owned() }; 32 | Err(anyhow!(err_msg)) 33 | } else { 34 | Ok(()) 35 | } 36 | } 37 | 38 | pub fn call_function(&self, name: &str, nargs: i32) -> Result<(), Error> { 39 | let c_name = CString::new(name).map_err(Error::msg)?; 40 | let mut out_err: *const crankstart_sys::ctypes::c_char = ptr::null_mut(); 41 | pd_func_caller!( 42 | (*self.0).callFunction, 43 | c_name.as_ptr(), 44 | nargs as ctypes::c_int, 45 | &mut out_err 46 | )?; 47 | if !out_err.is_null() { 48 | let err_msg = unsafe { CStr::from_ptr(out_err).to_string_lossy().into_owned() }; 49 | Err(anyhow!(err_msg)) 50 | } else { 51 | Ok(()) 52 | } 53 | } 54 | 55 | pub fn get_arg_string(&self, pos: i32) -> Result { 56 | let c_arg_string = pd_func_caller!((*self.0).getArgString, pos as ctypes::c_int)?; 57 | unsafe { 58 | let arg_string = CStr::from_ptr(c_arg_string).to_string_lossy().into_owned(); 59 | Ok(arg_string) 60 | } 61 | } 62 | 63 | pub fn push_function(&self, f: lua_CFunction) -> Result<(), Error> { 64 | pd_func_caller!((*self.0).pushFunction, f) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/sound.rs: -------------------------------------------------------------------------------- 1 | //! `Sound` is the parent structure for the Playdate audio API, and you can get access specific 2 | //! subsystems through its 'get' methods. 3 | //! 4 | //! For example, to play an audio sample (sound effect): 5 | //! 6 | //! ```rust 7 | //! let sound = Sound::get(); 8 | //! let player = sound.get_sample_player()?; 9 | //! let mut sample = sound.load_audio_sample("test.wav")?; 10 | //! player.set_sample(&mut sample)?; 11 | //! player.play(1, 1.0)?; 12 | //! ``` 13 | //! 14 | //! To play a music file: 15 | //! ```rust 16 | //! let music = Sound::get().get_file_player()?; 17 | //! music.load_into_player("music.pda")?; 18 | //! music.play(0)?; 19 | //! ``` 20 | 21 | use crate::{pd_func_caller, pd_func_caller_log}; 22 | use crankstart_sys::ctypes; 23 | 24 | use anyhow::{anyhow, ensure, Error, Result}; 25 | use core::ptr; 26 | use cstr_core::CString; 27 | 28 | pub mod sampleplayer; 29 | pub use sampleplayer::{AudioSample, SamplePlayer}; 30 | pub mod fileplayer; 31 | pub use fileplayer::FilePlayer; 32 | 33 | // When the Playdate system struct is created, it passes the given playdate_sound to Sound::new, 34 | // which then replaces this. 35 | static mut SOUND: Sound = Sound::null(); 36 | 37 | /// `Sound` is the main interface to the Playdate audio subsystems. 38 | #[derive(Clone, Debug)] 39 | pub struct Sound { 40 | raw_sound: *const crankstart_sys::playdate_sound, 41 | 42 | // Each audio API subsystem has a struct with all of the relevant functions for that subsystem. 43 | // These functions are used repeatedly, so pointers to them are stored here for convenience. 44 | raw_file_player: *const crankstart_sys::playdate_sound_fileplayer, 45 | raw_sample: *const crankstart_sys::playdate_sound_sample, 46 | raw_sample_player: *const crankstart_sys::playdate_sound_sampleplayer, 47 | } 48 | 49 | // Not implemented: addSource, removeSource, setMicCallback, and getHeadphoneState (waiting on 50 | // crankstart callback strategy), getDefaultChannel, addChannel, removeChannel. 51 | impl Sound { 52 | const fn null() -> Self { 53 | Self { 54 | raw_sound: ptr::null(), 55 | raw_file_player: ptr::null(), 56 | raw_sample: ptr::null(), 57 | raw_sample_player: ptr::null(), 58 | } 59 | } 60 | 61 | /// Internal: builds the `Sound` struct from the pointers given in the Playdate SDK after it's started. 62 | #[allow(clippy::new_ret_no_self)] 63 | pub(crate) fn new(raw_sound: *const crankstart_sys::playdate_sound) -> Result<()> { 64 | ensure!(!raw_sound.is_null(), "Null pointer passed to Sound::new"); 65 | 66 | // Get supported subsystem pointers. 67 | let raw_file_player = unsafe { (*raw_sound).fileplayer }; 68 | ensure!(!raw_file_player.is_null(), "Null sound.fileplayer"); 69 | let raw_sample = unsafe { (*raw_sound).sample }; 70 | ensure!(!raw_sample.is_null(), "Null sound.sample"); 71 | let raw_sample_player = unsafe { (*raw_sound).sampleplayer }; 72 | ensure!(!raw_sample_player.is_null(), "Null sound.sampleplayer"); 73 | 74 | let sound = Self { 75 | raw_sound, 76 | raw_file_player, 77 | raw_sample, 78 | raw_sample_player, 79 | }; 80 | unsafe { SOUND = sound }; 81 | Ok(()) 82 | } 83 | 84 | /// Gets a handle to the Sound system. This is the primary entry point for users. 85 | pub fn get() -> Self { 86 | unsafe { SOUND.clone() } 87 | } 88 | 89 | /// Get a `FilePlayer` that can be used to stream audio from disk, e.g. for music. 90 | pub fn get_file_player(&self) -> Result { 91 | let raw_player = pd_func_caller!((*self.raw_file_player).newPlayer)?; 92 | ensure!( 93 | !raw_player.is_null(), 94 | "Null returned from fileplayer.newPlayer" 95 | ); 96 | FilePlayer::new(self.raw_file_player, raw_player) 97 | } 98 | 99 | /// Get a `SamplePlayer` that can be used to play sound effects. 100 | pub fn get_sample_player(&self) -> Result { 101 | let raw_player = pd_func_caller!((*self.raw_sample_player).newPlayer)?; 102 | ensure!( 103 | !raw_player.is_null(), 104 | "Null returned from sampleplayer.newPlayer" 105 | ); 106 | SamplePlayer::new(self.raw_sample_player, raw_player) 107 | } 108 | 109 | /// Loads an `AudioSample` sound effect. Assign it to a `SamplePlayer` with 110 | /// `SamplePlayer.set_sample`. 111 | pub fn load_audio_sample(&self, sample_path: &str) -> Result { 112 | let sample_path_c = CString::new(sample_path).map_err(Error::msg)?; 113 | let arg_ptr = sample_path_c.as_ptr() as *const ctypes::c_char; 114 | let raw_audio_sample = pd_func_caller!((*self.raw_sample).load, arg_ptr)?; 115 | ensure!( 116 | !raw_audio_sample.is_null(), 117 | "Null returned from sample.load" 118 | ); 119 | AudioSample::new(self.raw_sample, raw_audio_sample) 120 | } 121 | 122 | /// Returns the sound engine's current time, in frames, 44.1k per second. 123 | pub fn get_current_time(&self) -> Result { 124 | pd_func_caller!((*self.raw_sound).getCurrentTime) 125 | } 126 | 127 | /// Sets which audio outputs should be active. Note: if you disable headphones and enable 128 | /// speaker, sound will be played through the speaker even if headphones are plugged in. 129 | pub fn set_outputs_active(&self, headphone: bool, speaker: bool) -> Result<()> { 130 | pd_func_caller!( 131 | (*self.raw_sound).setOutputsActive, 132 | headphone as ctypes::c_int, 133 | speaker as ctypes::c_int 134 | ) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/sound/fileplayer.rs: -------------------------------------------------------------------------------- 1 | use crate::{pd_func_caller, pd_func_caller_log}; 2 | use crankstart_sys::ctypes; 3 | 4 | use anyhow::{anyhow, ensure, Error, Result}; 5 | use cstr_core::CString; 6 | 7 | /// Note: Make sure you hold on to a FilePlayer until the file has played as much as you want, 8 | /// because dropping it will stop playback. 9 | #[derive(Debug)] 10 | pub struct FilePlayer { 11 | raw_subsystem: *const crankstart_sys::playdate_sound_fileplayer, 12 | raw_player: *mut crankstart_sys::FilePlayer, 13 | } 14 | 15 | impl Drop for FilePlayer { 16 | fn drop(&mut self) { 17 | // Use _log to leak rather than fail 18 | pd_func_caller_log!((*self.raw_subsystem).freePlayer, self.raw_player); 19 | } 20 | } 21 | 22 | // Not implemented: newPlayer (use Sound::get_file_player), setFinishCallback and fadeVolume 23 | // (waiting on crankstart callback strategy), and setLoopRange (does not seem to do anything). 24 | impl FilePlayer { 25 | pub(crate) fn new( 26 | raw_subsystem: *const crankstart_sys::playdate_sound_fileplayer, 27 | raw_player: *mut crankstart_sys::FilePlayer, 28 | ) -> Result { 29 | ensure!( 30 | !raw_subsystem.is_null(), 31 | "Null pointer given as subsystem to FilePlayer::new" 32 | ); 33 | ensure!( 34 | !raw_player.is_null(), 35 | "Null pointer given as player to FilePlayer::new" 36 | ); 37 | Ok(Self { 38 | raw_subsystem, 39 | raw_player, 40 | }) 41 | } 42 | 43 | /// Loads the given file into the player. Unlike with SamplePlayer, you must give the 44 | /// compiled audio filename here, e.g. "file.pda" instead of "file.wav". MP3 files are 45 | /// not compiled, so they keep their original .mp3 extension. 46 | pub fn load_into_player(&self, file_path: &str) -> Result<()> { 47 | let file_path_c = CString::new(file_path).map_err(Error::msg)?; 48 | let arg_ptr = file_path_c.as_ptr() as *const ctypes::c_char; 49 | let result = pd_func_caller!( 50 | (*self.raw_subsystem).loadIntoPlayer, 51 | self.raw_player, 52 | arg_ptr 53 | )?; 54 | if result == 1 { 55 | Ok(()) 56 | } else { 57 | Err(anyhow!( 58 | "load_into_player given nonexistent file '{}'", 59 | file_path 60 | )) 61 | } 62 | } 63 | 64 | /// Play the file 'repeat_count' times; if 0, play until `stop` is called. See set_loop_range 65 | /// for the portion of the file that will repeat. 66 | pub fn play(&self, repeat_count: ctypes::c_int) -> Result<()> { 67 | let result = pd_func_caller!((*self.raw_subsystem).play, self.raw_player, repeat_count,)?; 68 | if result == 1 { 69 | Ok(()) 70 | } else { 71 | Err(anyhow!( 72 | "fileplayer.play should return 1; returned {}", 73 | result 74 | )) 75 | } 76 | } 77 | 78 | /// Can be used to stop a played file early, or stop one that's repeating endlessly because 79 | /// 'repeat' was set to 0. 80 | pub fn stop(&self) -> Result<()> { 81 | pd_func_caller!((*self.raw_subsystem).stop, self.raw_player) 82 | } 83 | 84 | /// Pause playback. To resume playback at the same point, use play(). 85 | pub fn pause(&self) -> Result<()> { 86 | pd_func_caller!((*self.raw_subsystem).pause, self.raw_player) 87 | } 88 | 89 | /// Returns whether the player is currently playing the file. 90 | pub fn is_playing(&self) -> Result { 91 | let result = pd_func_caller!((*self.raw_subsystem).isPlaying, self.raw_player)?; 92 | Ok(result == 1) 93 | } 94 | 95 | /// How much audio to buffer, in seconds. Larger buffers use more memory but help avoid 96 | /// underruns, which can cause stuttering (see set_stop_on_underrun). 97 | pub fn set_buffer_length(&self, length: f32) -> Result<()> { 98 | pd_func_caller!( 99 | (*self.raw_subsystem).setBufferLength, 100 | self.raw_player, 101 | length 102 | ) 103 | } 104 | 105 | /// If set to true, and the buffer runs out of data (known as an underrun), the player 106 | /// will stop playing. If false (the default), the player will continue playback as soon 107 | /// as more data is available; this will come across as audio stuttering, particularly 108 | /// with small buffer sizes. (Note that Inside Playdate with C says the reverse, but 109 | /// seems wrong.) 110 | pub fn set_stop_on_underrun(&self, stop: bool) -> Result<()> { 111 | pd_func_caller!( 112 | (*self.raw_subsystem).setStopOnUnderrun, 113 | self.raw_player, 114 | stop as ctypes::c_int 115 | ) 116 | } 117 | 118 | /// Returns whether the buffer has underrun. 119 | pub fn did_underrun(&self) -> Result { 120 | let result = pd_func_caller!((*self.raw_subsystem).didUnderrun, self.raw_player)?; 121 | Ok(result == 1) 122 | } 123 | 124 | /// Returns the current offset into the file, in seconds, increasing as it plays. 125 | pub fn get_offset(&self) -> Result { 126 | pd_func_caller!((*self.raw_subsystem).getOffset, self.raw_player) 127 | } 128 | 129 | /// Set how far into the file to start playing, in seconds. 130 | pub fn set_offset(&self, offset: f32) -> Result<()> { 131 | pd_func_caller!((*self.raw_subsystem).setOffset, self.raw_player, offset) 132 | } 133 | 134 | /// Gets the current volume of the left and right audio channels, out of 1. 135 | pub fn get_volume(&self) -> Result<(f32, f32)> { 136 | let mut left = 0.0; 137 | let mut right = 0.0; 138 | pd_func_caller!( 139 | (*self.raw_subsystem).getVolume, 140 | self.raw_player, 141 | &mut left, 142 | &mut right, 143 | )?; 144 | Ok((left, right)) 145 | } 146 | 147 | /// Sets the volume of the left and right audio channels, out of 1. 148 | pub fn set_volume(&self, left: f32, right: f32) -> Result<()> { 149 | pd_func_caller!( 150 | (*self.raw_subsystem).setVolume, 151 | self.raw_player, 152 | left, 153 | right 154 | ) 155 | } 156 | 157 | /// Gets the current playback speed. 158 | pub fn get_rate(&self) -> Result { 159 | pd_func_caller!((*self.raw_subsystem).getRate, self.raw_player) 160 | } 161 | 162 | /// Sets the playback speed of the player; 1.0 is normal speed, 0.5 is down an octave, 163 | /// 2.0 is up one, etc. 164 | pub fn set_rate(&self, playback_speed: f32) -> Result<()> { 165 | ensure!( 166 | playback_speed >= 0.0, 167 | "FilePlayer cannot play in reverse (playback_speed < 0)" 168 | ); 169 | pd_func_caller!( 170 | (*self.raw_subsystem).setRate, 171 | self.raw_player, 172 | playback_speed 173 | ) 174 | } 175 | 176 | /// Returns the length of the loaded file, in seconds. 177 | pub fn get_length(&self) -> Result { 178 | pd_func_caller!((*self.raw_subsystem).getLength, self.raw_player) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/sound/sampleplayer.rs: -------------------------------------------------------------------------------- 1 | use crate::{log_to_console, pd_func_caller, pd_func_caller_log}; 2 | use crankstart_sys::ctypes; 3 | 4 | use alloc::rc::Rc; 5 | use anyhow::{anyhow, ensure, Error, Result}; 6 | 7 | /// Note: Make sure you hold on to a SamplePlayer until the sample has played as much as you want, 8 | /// because dropping it will stop playback. 9 | #[derive(Debug)] 10 | pub struct SamplePlayer { 11 | raw_subsystem: *const crankstart_sys::playdate_sound_sampleplayer, 12 | raw_player: *mut crankstart_sys::SamplePlayer, 13 | 14 | // We store an Rc clone of the audio sample so that it's not freed before the player is 15 | // finished using it, or until another sample is set. 16 | sample: Option, 17 | } 18 | 19 | impl Drop for SamplePlayer { 20 | fn drop(&mut self) { 21 | // Use _log to leak rather than fail 22 | pd_func_caller_log!((*self.raw_subsystem).freePlayer, self.raw_player); 23 | } 24 | } 25 | 26 | // Not implemented: newPlayer (use Sound::get_sample_player), and setFinishCallback and setLoopCallback 27 | // (waiting on crankstart callback strategy). 28 | impl SamplePlayer { 29 | pub(crate) fn new( 30 | raw_subsystem: *const crankstart_sys::playdate_sound_sampleplayer, 31 | raw_player: *mut crankstart_sys::SamplePlayer, 32 | ) -> Result { 33 | ensure!( 34 | !raw_subsystem.is_null(), 35 | "Null pointer given as subsystem to SamplePlayer::new" 36 | ); 37 | ensure!( 38 | !raw_player.is_null(), 39 | "Null pointer given as player to SamplePlayer::new" 40 | ); 41 | Ok(Self { 42 | raw_subsystem, 43 | raw_player, 44 | sample: None, 45 | }) 46 | } 47 | 48 | /// Sets the sound effect to be played by this player. 49 | pub fn set_sample(&mut self, audio_sample: &AudioSample) -> Result<()> { 50 | // We store an Rc clone of the audio sample so that it's not freed before the player is 51 | // finished using it, or until another sample is set. 52 | self.sample = Some(audio_sample.clone()); 53 | 54 | pd_func_caller!( 55 | (*self.raw_subsystem).setSample, 56 | self.raw_player, 57 | audio_sample.inner.raw_audio_sample 58 | ) 59 | } 60 | 61 | /// Play the sample 'repeat_count' times; if 0, play until `stop` is called; if -1, play 62 | /// forward, backward, forward, etc. (See set_play_range to change which part is looped.) 63 | /// 'playback_speed' is how fast the sample plays; 1.0 is normal speed, 0.5 is down an octave, 64 | /// 2.0 is up one, etc. A negative rate plays the sample in reverse. 65 | pub fn play(&self, repeat_count: ctypes::c_int, playback_speed: f32) -> Result<()> { 66 | let result = pd_func_caller!( 67 | (*self.raw_subsystem).play, 68 | self.raw_player, 69 | repeat_count, 70 | playback_speed 71 | )?; 72 | if result == 1 { 73 | Ok(()) 74 | } else { 75 | Err(anyhow!( 76 | "sampleplayer.play should return 1; returned {}", 77 | result 78 | )) 79 | } 80 | } 81 | 82 | /// Can be used to stop a sample early, or stop one that's repeating endlessly because 'repeat' 83 | /// was set to 0. 84 | pub fn stop(&self) -> Result<()> { 85 | pd_func_caller!((*self.raw_subsystem).stop, self.raw_player) 86 | } 87 | 88 | /// Pause or resume playback. 89 | pub fn set_paused(&self, paused: bool) -> Result<()> { 90 | pd_func_caller!( 91 | (*self.raw_subsystem).setPaused, 92 | self.raw_player, 93 | paused as ctypes::c_int 94 | ) 95 | } 96 | 97 | /// Returns whether the player is currently playing the sample. 98 | pub fn is_playing(&self) -> Result { 99 | let result = pd_func_caller!((*self.raw_subsystem).isPlaying, self.raw_player)?; 100 | Ok(result == 1) 101 | } 102 | 103 | /// Sets the start and end position, in frames, when looping a sample with repeat_count -1. 104 | pub fn set_play_range(&self, start: ctypes::c_int, end: ctypes::c_int) -> Result<()> { 105 | pd_func_caller!( 106 | (*self.raw_subsystem).setPlayRange, 107 | self.raw_player, 108 | start, 109 | end 110 | ) 111 | } 112 | 113 | /// Returns the current offset into the sample, in seconds, increasing as it plays. This is not 114 | /// adjusted for rate. 115 | pub fn get_offset(&self) -> Result { 116 | pd_func_caller!((*self.raw_subsystem).getOffset, self.raw_player) 117 | } 118 | 119 | /// Set how far into the sample to start playing, in seconds. This is not adjusted for rate. 120 | pub fn set_offset(&self, offset: f32) -> Result<()> { 121 | pd_func_caller!((*self.raw_subsystem).setOffset, self.raw_player, offset) 122 | } 123 | 124 | /// Gets the current volume of the left and right audio channels, out of 1. 125 | pub fn get_volume(&self) -> Result<(f32, f32)> { 126 | let mut left = 0.0; 127 | let mut right = 0.0; 128 | pd_func_caller!( 129 | (*self.raw_subsystem).getVolume, 130 | self.raw_player, 131 | &mut left, 132 | &mut right, 133 | )?; 134 | Ok((left, right)) 135 | } 136 | 137 | /// Sets the volume of the left and right audio channels, out of 1. 138 | pub fn set_volume(&self, left: f32, right: f32) -> Result<()> { 139 | pd_func_caller!( 140 | (*self.raw_subsystem).setVolume, 141 | self.raw_player, 142 | left, 143 | right 144 | ) 145 | } 146 | 147 | /// Gets the current playback speed. Returns 1 unless the value was changed by `set_rate` - it 148 | /// still returns 1 if the rate is changed with the argument to `play`. 149 | pub fn get_rate(&self) -> Result { 150 | pd_func_caller!((*self.raw_subsystem).getRate, self.raw_player) 151 | } 152 | 153 | /// Sets the playback speed of the player after a sample has started playing. 1.0 is normal, 154 | /// 0.5 is down an octave, 2.0 is up one, etc. A negative rate plays the sample in reverse. 155 | pub fn set_rate(&self, playback_speed: f32) -> Result<()> { 156 | pd_func_caller!( 157 | (*self.raw_subsystem).setRate, 158 | self.raw_player, 159 | playback_speed 160 | ) 161 | } 162 | 163 | /// Returns the length of the assigned sample, in seconds. 164 | pub fn get_length(&self) -> Result { 165 | pd_func_caller!((*self.raw_subsystem).getLength, self.raw_player) 166 | } 167 | } 168 | 169 | /// A loaded sound effect. 170 | // Really a wrapper around an Rc clone of the internal structure; derive Clone so it's easy to get 171 | // another Rc reference. We use Rc so we don't free the sample before we're done using it. 172 | #[derive(Clone, Debug)] 173 | pub struct AudioSample { 174 | inner: Rc, 175 | } 176 | 177 | #[derive(Debug)] 178 | struct AudioSampleInner { 179 | raw_subsystem: *const crankstart_sys::playdate_sound_sample, 180 | raw_audio_sample: *mut crankstart_sys::AudioSample, 181 | } 182 | 183 | impl Drop for AudioSampleInner { 184 | fn drop(&mut self) { 185 | // Use _log to leak rather than fail 186 | pd_func_caller_log!((*self.raw_subsystem).freeSample, self.raw_audio_sample); 187 | } 188 | } 189 | 190 | // Not implemented: getData, newSampleBuffer, loadIntoSample, newSampleFromData - 191 | // only Sound::load_audio_sample for now. 192 | impl AudioSample { 193 | pub(crate) fn new( 194 | raw_subsystem: *const crankstart_sys::playdate_sound_sample, 195 | raw_audio_sample: *mut crankstart_sys::AudioSample, 196 | ) -> Result { 197 | ensure!( 198 | !raw_subsystem.is_null(), 199 | "Null pointer given as subsystem to AudioSample::new" 200 | ); 201 | ensure!( 202 | !raw_audio_sample.is_null(), 203 | "Null pointer given as sample to AudioSample::new" 204 | ); 205 | Ok(Self { 206 | inner: Rc::new(AudioSampleInner { 207 | raw_subsystem, 208 | raw_audio_sample, 209 | }), 210 | }) 211 | } 212 | 213 | /// Returns the length of the sample, in seconds. 214 | pub fn get_length(&self) -> Result { 215 | pd_func_caller!( 216 | (*self.inner.raw_subsystem).getLength, 217 | self.inner.raw_audio_sample 218 | ) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/sprite.rs: -------------------------------------------------------------------------------- 1 | extern crate alloc; 2 | 3 | use { 4 | crate::{ 5 | graphics::{Bitmap, Graphics, LCDBitmapDrawMode, LCDBitmapFlip, LCDColor, PDRect}, 6 | log_to_console, pd_func_caller, pd_func_caller_log, 7 | system::System, 8 | Playdate, 9 | }, 10 | alloc::{ 11 | boxed::Box, 12 | collections::BTreeMap, 13 | rc::{Rc, Weak}, 14 | }, 15 | anyhow::{anyhow, Error, Result}, 16 | core::{ 17 | cell::{Ref, RefCell}, 18 | fmt::Debug, 19 | hash::{Hash, Hasher}, 20 | slice, 21 | }, 22 | crankstart_sys::{ 23 | playdate_sprite, LCDRect, LCDSprite, LCDSpriteCollisionFilterProc, SpriteCollisionInfo, 24 | }, 25 | euclid::default::Vector2D, 26 | euclid::{point2, size2, vec2}, 27 | hashbrown::HashMap, 28 | }; 29 | 30 | pub use crankstart_sys::SpriteCollisionResponseType; 31 | 32 | // Currently no font:getHeight in C API. 33 | const SYSTEM_FONT_HEIGHT: i32 = 18; 34 | 35 | pub type SpriteUpdateFunction = unsafe extern "C" fn(sprite: *mut crankstart_sys::LCDSprite); 36 | pub type SpriteDrawFunction = 37 | unsafe extern "C" fn(sprite: *mut crankstart_sys::LCDSprite, bounds: PDRect, drawrect: PDRect); 38 | pub type SpriteCollideFunction = unsafe extern "C" fn( 39 | sprite: *const crankstart_sys::LCDSprite, 40 | other: *const crankstart_sys::LCDSprite, 41 | ) -> SpriteCollisionResponseType; 42 | 43 | static mut SPRITE_UPDATE: Option = None; 44 | static mut SPRITE_DRAW: Option = None; 45 | 46 | pub trait SpriteCollider: Debug + 'static { 47 | fn response_type(&self, sprite: Sprite, other: Sprite) -> SpriteCollisionResponseType; 48 | } 49 | 50 | pub type SpriteCollisionResponses = 51 | HashMap<*const crankstart_sys::LCDSprite, Box>; 52 | 53 | static mut SPRITE_COLLISION_RESPONSES: Option = None; 54 | static mut SPRITE_MANAGER: Option = None; 55 | 56 | pub struct Collisions(*mut SpriteCollisionInfo, crankstart_sys::ctypes::c_int); 57 | 58 | impl Collisions { 59 | pub fn iter(&self) -> CollisionInfoIter<'_> { 60 | CollisionInfoIter { 61 | collisions: self, 62 | index: 0, 63 | } 64 | } 65 | } 66 | 67 | #[derive(Debug)] 68 | pub struct CollisionInfo<'a> { 69 | pub sprite: Sprite, 70 | pub other: Sprite, 71 | pub info: &'a SpriteCollisionInfo, 72 | } 73 | 74 | pub struct CollisionInfoIter<'a> { 75 | collisions: &'a Collisions, 76 | index: usize, 77 | } 78 | 79 | impl<'a> Iterator for CollisionInfoIter<'a> { 80 | type Item = CollisionInfo<'a>; 81 | 82 | fn next(&mut self) -> Option> { 83 | if self.index >= self.collisions.1 as usize { 84 | None 85 | } else { 86 | let index = self.index; 87 | self.index += 1; 88 | let collision_slice = 89 | unsafe { slice::from_raw_parts(self.collisions.0, self.collisions.1 as usize) }; 90 | 91 | let sprite_manager = SpriteManager::get_mut(); 92 | let sprite = sprite_manager.get_sprite(collision_slice[index].sprite); 93 | let other = sprite_manager.get_sprite(collision_slice[index].other); 94 | if sprite.is_none() || other.is_none() { 95 | return None; 96 | } 97 | let sprite = sprite.unwrap(); 98 | let other = other.unwrap(); 99 | let collision_info = CollisionInfo { 100 | sprite, 101 | other, 102 | info: &collision_slice[index], 103 | }; 104 | Some(collision_info) 105 | } 106 | } 107 | } 108 | 109 | impl Drop for Collisions { 110 | fn drop(&mut self) { 111 | System::get().realloc(self.0 as *mut core::ffi::c_void, 0); 112 | } 113 | } 114 | 115 | pub struct SpriteInner { 116 | pub raw_sprite: *mut crankstart_sys::LCDSprite, 117 | playdate_sprite: *const playdate_sprite, 118 | image: Option, 119 | userdata: Option>, 120 | } 121 | 122 | pub type SpritePtr = Rc>; 123 | pub type SpriteWeakPtr = Weak>; 124 | 125 | extern "C" fn get_sprite_collision_response( 126 | sprite: *mut crankstart_sys::LCDSprite, 127 | other: *mut crankstart_sys::LCDSprite, 128 | ) -> SpriteCollisionResponseType { 129 | if let Some(collision_responses) = unsafe { SPRITE_COLLISION_RESPONSES.as_ref() } { 130 | let collider = collision_responses.get(&(sprite as *const crankstart_sys::LCDSprite)); 131 | if let Some(collider) = collider { 132 | if let Some(sprite) = SpriteManager::get_sprite_static(sprite) { 133 | if let Some(other) = SpriteManager::get_sprite_static(other) { 134 | return collider.response_type(sprite, other); 135 | } 136 | } 137 | } 138 | } 139 | 140 | SpriteCollisionResponseType::kCollisionTypeOverlap 141 | } 142 | 143 | impl SpriteInner { 144 | pub fn set_use_custom_draw(&mut self) -> Result<(), Error> { 145 | self.set_draw_function(unsafe { SPRITE_DRAW.expect("SPRITE_DRAW") }) 146 | } 147 | 148 | pub fn set_collision_response_type( 149 | &mut self, 150 | response_type: Option>, 151 | ) -> Result<(), Error> { 152 | if let Some(response_type) = response_type { 153 | unsafe { 154 | if let Some(collision_responses) = SPRITE_COLLISION_RESPONSES.as_mut() { 155 | collision_responses.insert(self.raw_sprite, response_type); 156 | } else { 157 | log_to_console!("Can't access SPRITE_COLLISION_RESPONSES"); 158 | } 159 | } 160 | self.set_collision_response_function(Some(get_sprite_collision_response))?; 161 | } else { 162 | self.set_collision_response_function(None)?; 163 | unsafe { 164 | if let Some(collision_responses) = SPRITE_COLLISION_RESPONSES.as_mut() { 165 | collision_responses 166 | .remove(&(self.raw_sprite as *const crankstart_sys::LCDSprite)); 167 | } else { 168 | log_to_console!("Can't access SPRITE_COLLISION_RESPONSES"); 169 | } 170 | } 171 | } 172 | Ok(()) 173 | } 174 | 175 | fn set_update_function(&self, f: SpriteUpdateFunction) -> Result<(), Error> { 176 | pd_func_caller!( 177 | (*self.playdate_sprite).setUpdateFunction, 178 | self.raw_sprite, 179 | Some(f) 180 | ) 181 | } 182 | 183 | fn set_draw_function(&self, f: SpriteDrawFunction) -> Result<(), Error> { 184 | pd_func_caller!( 185 | (*self.playdate_sprite).setDrawFunction, 186 | self.raw_sprite, 187 | Some(f) 188 | ) 189 | } 190 | 191 | fn set_collision_response_function( 192 | &self, 193 | f: LCDSpriteCollisionFilterProc, 194 | ) -> Result<(), Error> { 195 | pd_func_caller!( 196 | (*self.playdate_sprite).setCollisionResponseFunction, 197 | self.raw_sprite, 198 | f 199 | ) 200 | } 201 | 202 | pub fn get_bounds(&self) -> Result { 203 | pd_func_caller!((*self.playdate_sprite).getBounds, self.raw_sprite) 204 | } 205 | 206 | pub fn set_bounds(&self, bounds: &PDRect) -> Result<(), Error> { 207 | pd_func_caller!((*self.playdate_sprite).setBounds, self.raw_sprite, *bounds) 208 | } 209 | 210 | pub fn get_z_index(&self) -> Result { 211 | pd_func_caller!((*self.playdate_sprite).getZIndex, self.raw_sprite) 212 | } 213 | 214 | pub fn set_z_index(&self, z_index: i16) -> Result<(), Error> { 215 | pd_func_caller!((*self.playdate_sprite).setZIndex, self.raw_sprite, z_index) 216 | } 217 | 218 | /// Returns a reference to the bitmap assigned to the sprite, if any. 219 | pub fn get_image(&self) -> Option<&Bitmap> { 220 | self.image.as_ref() 221 | } 222 | 223 | pub fn set_image(&mut self, bitmap: Bitmap, flip: LCDBitmapFlip) -> Result<(), Error> { 224 | pd_func_caller!( 225 | (*self.playdate_sprite).setImage, 226 | self.raw_sprite, 227 | bitmap.inner.borrow().raw_bitmap, 228 | flip, 229 | )?; 230 | self.image = Some(bitmap); 231 | Ok(()) 232 | } 233 | 234 | pub fn set_tag(&mut self, tag: u8) -> Result<(), Error> { 235 | pd_func_caller!((*self.playdate_sprite).setTag, self.raw_sprite, tag) 236 | } 237 | 238 | pub fn get_tag(&self) -> Result { 239 | pd_func_caller!((*self.playdate_sprite).getTag, self.raw_sprite) 240 | } 241 | 242 | pub fn set_draw_mode(&self, mode: LCDBitmapDrawMode) -> Result<(), Error> { 243 | pd_func_caller!((*self.playdate_sprite).setDrawMode, self.raw_sprite, mode) 244 | } 245 | 246 | pub fn set_visible(&mut self, visible: bool) -> Result<(), Error> { 247 | pd_func_caller!( 248 | (*self.playdate_sprite).setVisible, 249 | self.raw_sprite, 250 | visible as i32 251 | ) 252 | } 253 | 254 | pub fn is_visible(&self) -> Result { 255 | let visible = pd_func_caller!((*self.playdate_sprite).isVisible, self.raw_sprite)?; 256 | Ok(visible != 0) 257 | } 258 | 259 | pub fn set_opaque(&self, opaque: bool) -> Result<(), Error> { 260 | pd_func_caller!( 261 | (*self.playdate_sprite).setOpaque, 262 | self.raw_sprite, 263 | opaque as i32 264 | ) 265 | } 266 | 267 | pub fn move_to(&mut self, x: f32, y: f32) -> Result<(), Error> { 268 | pd_func_caller!((*self.playdate_sprite).moveTo, self.raw_sprite, x, y) 269 | } 270 | 271 | pub fn get_position(&self) -> Result<(f32, f32), Error> { 272 | let mut x = 0.0; 273 | let mut y = 0.0; 274 | pd_func_caller!( 275 | (*self.playdate_sprite).getPosition, 276 | self.raw_sprite, 277 | &mut x, 278 | &mut y 279 | )?; 280 | Ok((x, y)) 281 | } 282 | 283 | pub fn set_collide_rect(&mut self, collide_rect: &PDRect) -> Result<(), Error> { 284 | pd_func_caller!( 285 | (*self.playdate_sprite).setCollideRect, 286 | self.raw_sprite, 287 | *collide_rect, 288 | ) 289 | } 290 | 291 | pub fn move_with_collisions( 292 | &mut self, 293 | goal_x: f32, 294 | goal_y: f32, 295 | ) -> Result<(f32, f32, Collisions), Error> { 296 | let mut actual_x = 0.0; 297 | let mut actual_y = 0.0; 298 | let mut count = 0; 299 | let raw_collision_info = pd_func_caller!( 300 | (*self.playdate_sprite).moveWithCollisions, 301 | self.raw_sprite, 302 | goal_x, 303 | goal_y, 304 | &mut actual_x, 305 | &mut actual_y, 306 | &mut count, 307 | )?; 308 | Ok((actual_x, actual_y, Collisions(raw_collision_info, count))) 309 | } 310 | 311 | pub fn mark_dirty(&mut self) -> Result<(), Error> { 312 | pd_func_caller!((*self.playdate_sprite).markDirty, self.raw_sprite,) 313 | } 314 | 315 | pub fn get_userdata(&self) -> Result>, Error> 316 | where 317 | T: 'static, 318 | { 319 | self.userdata 320 | .as_ref() 321 | .map( 322 | |userdata: &Rc| -> Result, Error> { 323 | userdata.clone().downcast::().map_err(|err| { 324 | anyhow!( 325 | "Failed to cast userdata type {}", 326 | core::any::type_name::(), 327 | ) 328 | }) 329 | }, 330 | ) 331 | .transpose() 332 | } 333 | 334 | pub fn set_userdata(&mut self, userdata: Rc) 335 | where 336 | T: 'static, 337 | { 338 | self.userdata = Some(userdata); 339 | } 340 | } 341 | 342 | impl Drop for SpriteInner { 343 | fn drop(&mut self) { 344 | pd_func_caller_log!((*self.playdate_sprite).freeSprite, self.raw_sprite); 345 | unsafe { 346 | if let Some(collision_responses) = SPRITE_COLLISION_RESPONSES.as_mut() { 347 | collision_responses.remove(&(self.raw_sprite as *const crankstart_sys::LCDSprite)); 348 | } 349 | } 350 | } 351 | } 352 | 353 | impl Debug for SpriteInner { 354 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::result::Result<(), core::fmt::Error> { 355 | f.debug_struct("Sprite") 356 | .field("raw_sprite", &self.raw_sprite) 357 | .finish() 358 | } 359 | } 360 | 361 | impl PartialEq for SpriteInner { 362 | fn eq(&self, other: &Self) -> bool { 363 | self.raw_sprite == other.raw_sprite 364 | } 365 | } 366 | 367 | #[derive(Clone, Debug)] 368 | pub struct Sprite { 369 | inner: SpritePtr, 370 | } 371 | 372 | impl Sprite { 373 | pub fn set_use_custom_draw(&mut self) -> Result<(), Error> { 374 | self.inner 375 | .try_borrow_mut() 376 | .map_err(Error::msg)? 377 | .set_use_custom_draw() 378 | } 379 | 380 | pub fn set_collision_response_type( 381 | &mut self, 382 | response_type: Option>, 383 | ) -> Result<(), Error> { 384 | self.inner 385 | .try_borrow_mut() 386 | .map_err(Error::msg)? 387 | .set_collision_response_type(response_type) 388 | } 389 | 390 | pub fn get_bounds(&self) -> Result { 391 | self.inner.try_borrow().map_err(Error::msg)?.get_bounds() 392 | } 393 | 394 | pub fn set_bounds(&self, bounds: &PDRect) -> Result<(), Error> { 395 | self.inner 396 | .try_borrow_mut() 397 | .map_err(Error::msg)? 398 | .set_bounds(bounds) 399 | } 400 | 401 | pub fn get_z_index(&self) -> Result { 402 | self.inner 403 | .try_borrow_mut() 404 | .map_err(Error::msg)? 405 | .get_z_index() 406 | } 407 | 408 | pub fn set_z_index(&self, z_index: i16) -> Result<(), Error> { 409 | self.inner 410 | .try_borrow_mut() 411 | .map_err(Error::msg)? 412 | .set_z_index(z_index) 413 | } 414 | 415 | /// Returns a reference to the bitmap assigned to the sprite, if any. Specifically, 416 | /// returns Err if the inner data is already mutably borrowed; Ok(None) if no sprite has 417 | /// been assigned; Ok(Some(Ref)) if a sprite has been assigned. 418 | pub fn get_image(&self) -> Result>> { 419 | let borrowed: Ref = self.inner.try_borrow().map_err(Error::msg)?; 420 | let filtered: Result, _> = 421 | Ref::filter_map(borrowed, |b: &SpriteInner| b.get_image()); 422 | // filter_map gives back the original if the closure returns None, which we don't need 423 | Ok(filtered.ok()) 424 | } 425 | 426 | pub fn set_image(&mut self, bitmap: Bitmap, flip: LCDBitmapFlip) -> Result<(), Error> { 427 | self.inner 428 | .try_borrow_mut() 429 | .map_err(Error::msg)? 430 | .set_image(bitmap, flip) 431 | } 432 | 433 | pub fn set_tag(&mut self, tag: u8) -> Result<(), Error> { 434 | self.inner 435 | .try_borrow_mut() 436 | .map_err(Error::msg)? 437 | .set_tag(tag) 438 | } 439 | 440 | pub fn get_tag(&self) -> Result { 441 | self.inner.try_borrow().map_err(Error::msg)?.get_tag() 442 | } 443 | 444 | pub fn set_draw_mode(&mut self, mode: LCDBitmapDrawMode) -> Result<(), Error> { 445 | self.inner 446 | .try_borrow_mut() 447 | .map_err(Error::msg)? 448 | .set_draw_mode(mode) 449 | } 450 | 451 | pub fn move_to(&mut self, x: f32, y: f32) -> Result<(), Error> { 452 | self.inner 453 | .try_borrow_mut() 454 | .map_err(Error::msg)? 455 | .move_to(x, y) 456 | } 457 | 458 | pub fn set_visible(&mut self, visible: bool) -> Result<(), Error> { 459 | self.inner 460 | .try_borrow_mut() 461 | .map_err(Error::msg)? 462 | .set_visible(visible) 463 | } 464 | 465 | pub fn is_visible(&self) -> Result { 466 | self.inner.try_borrow().map_err(Error::msg)?.is_visible() 467 | } 468 | 469 | pub fn set_opaque(&self, opaque: bool) -> Result<(), Error> { 470 | self.inner 471 | .try_borrow_mut() 472 | .map_err(Error::msg)? 473 | .set_opaque(opaque) 474 | } 475 | 476 | pub fn get_position(&self) -> Result<(f32, f32), Error> { 477 | self.inner.try_borrow().map_err(Error::msg)?.get_position() 478 | } 479 | 480 | pub fn set_collide_rect(&mut self, collide_rect: &PDRect) -> Result<(), Error> { 481 | self.inner 482 | .try_borrow_mut() 483 | .map_err(Error::msg)? 484 | .set_collide_rect(collide_rect) 485 | } 486 | 487 | pub fn move_with_collisions( 488 | &mut self, 489 | goal_x: f32, 490 | goal_y: f32, 491 | ) -> Result<(f32, f32, Collisions), Error> { 492 | self.inner 493 | .try_borrow_mut() 494 | .map_err(Error::msg)? 495 | .move_with_collisions(goal_x, goal_y) 496 | } 497 | 498 | pub fn mark_dirty(&mut self) -> Result<(), Error> { 499 | self.inner 500 | .try_borrow_mut() 501 | .map_err(Error::msg)? 502 | .mark_dirty() 503 | } 504 | 505 | pub fn get_userdata(&self) -> Result>, Error> 506 | where 507 | T: 'static, 508 | { 509 | self.inner.borrow().get_userdata() 510 | } 511 | 512 | pub fn set_userdata(&mut self, userdata: Rc) 513 | where 514 | T: 'static, 515 | { 516 | self.inner.borrow_mut().set_userdata(userdata) 517 | } 518 | } 519 | 520 | impl Hash for Sprite { 521 | fn hash(&self, state: &mut H) { 522 | self.inner.borrow().raw_sprite.hash(state); 523 | } 524 | } 525 | 526 | impl PartialEq for Sprite { 527 | fn eq(&self, other: &Self) -> bool { 528 | self.inner == other.inner 529 | } 530 | } 531 | 532 | impl Eq for Sprite {} 533 | 534 | pub struct SpriteManager { 535 | pub playdate_sprite: *const playdate_sprite, 536 | sprites: HashMap<*const crankstart_sys::LCDSprite, SpriteWeakPtr>, 537 | } 538 | 539 | impl SpriteManager { 540 | pub(crate) fn new( 541 | playdate_sprite: *const playdate_sprite, 542 | update: SpriteUpdateFunction, 543 | draw: SpriteDrawFunction, 544 | ) { 545 | unsafe { 546 | SPRITE_UPDATE = Some(update); 547 | SPRITE_DRAW = Some(draw); 548 | SPRITE_COLLISION_RESPONSES = Some(HashMap::with_capacity(32)) 549 | } 550 | let sm = Self { 551 | playdate_sprite, 552 | sprites: HashMap::with_capacity(32), 553 | }; 554 | 555 | unsafe { 556 | SPRITE_MANAGER = Some(sm); 557 | } 558 | } 559 | 560 | pub fn get_mut() -> &'static mut SpriteManager { 561 | unsafe { SPRITE_MANAGER.as_mut().expect("SpriteManager") } 562 | } 563 | 564 | pub fn new_sprite(&mut self) -> Result { 565 | let raw_sprite = pd_func_caller!((*self.playdate_sprite).newSprite)?; 566 | if raw_sprite.is_null() { 567 | Err(anyhow!("new sprite failed")) 568 | } else { 569 | let sprite = SpriteInner { 570 | raw_sprite, 571 | playdate_sprite: self.playdate_sprite, 572 | image: None, 573 | userdata: None, 574 | }; 575 | sprite.set_update_function(unsafe { SPRITE_UPDATE.expect("SPRITE_UPDATE") })?; 576 | let sprite_ptr = Rc::new(RefCell::new(sprite)); 577 | let weak_ptr = Rc::downgrade(&sprite_ptr); 578 | self.sprites.insert(raw_sprite, weak_ptr); 579 | Ok(Sprite { inner: sprite_ptr }) 580 | } 581 | } 582 | 583 | pub fn add_sprite(&self, sprite: &Sprite) -> Result<(), Error> { 584 | pd_func_caller!( 585 | (*self.playdate_sprite).addSprite, 586 | sprite.inner.borrow().raw_sprite 587 | ) 588 | } 589 | 590 | pub fn get_sprite_count(&self) -> Result { 591 | pd_func_caller!((*self.playdate_sprite).getSpriteCount) 592 | } 593 | 594 | pub fn remove_sprite(&mut self, sprite: &Sprite) -> Result<(), Error> { 595 | pd_func_caller!( 596 | (*self.playdate_sprite).removeSprite, 597 | sprite.inner.borrow_mut().raw_sprite 598 | ) 599 | } 600 | 601 | pub fn add_dirty_rect(dirty_rect: LCDRect) -> Result<(), Error> { 602 | pd_func_caller!((*Self::get_mut().playdate_sprite).addDirtyRect, dirty_rect) 603 | } 604 | 605 | pub fn get_sprite_static(raw_sprite: *const LCDSprite) -> Option { 606 | Self::get_mut().get_sprite(raw_sprite) 607 | } 608 | 609 | pub fn get_sprite(&self, raw_sprite: *const LCDSprite) -> Option { 610 | let weak_sprite = self.sprites.get(&raw_sprite); 611 | weak_sprite 612 | .and_then(|weak_sprite| weak_sprite.upgrade()) 613 | .map(|inner_ptr| Sprite { 614 | inner: inner_ptr.clone(), 615 | }) 616 | } 617 | 618 | pub fn update_and_draw_sprites(&mut self) -> Result<(), Error> { 619 | pd_func_caller!((*self.playdate_sprite).updateAndDrawSprites)?; 620 | self.sprites.retain(|k, v| v.weak_count() != 0); 621 | Ok(()) 622 | } 623 | } 624 | 625 | /// This is a helper type for drawing text into a sprite. Drawing text into a sprite is the 626 | /// recommended way to display text when using sprites in your game; it removes timing issues and 627 | /// gives you the flexibility of the sprite system rather than draw_text alone. 628 | /// 629 | /// After creation with `new`, you can `update_text` as desired, and use `get_sprite` or 630 | /// `get_sprite_mut` to access the `Sprite` for other operations like `move_to` and `get_bounds` 631 | /// (which can tell you the height and width of the generated bitmap). 632 | /// 633 | /// Note: it's assumed that you're using the system font and haven't changed its tracking; we have 634 | /// no way to retrieve the current font or tracking with C APIs. 635 | #[derive(Clone, Debug)] 636 | pub struct TextSprite { 637 | sprite: Sprite, 638 | background: LCDColor, 639 | } 640 | 641 | impl TextSprite { 642 | /// Creates a `TextSprite`, draws the given text into it over the given background color, 643 | /// and adds the underlying sprite to the `SpriteManager`. 644 | pub fn new(text: S, background: LCDColor) -> Result 645 | where 646 | S: AsRef, 647 | { 648 | let text = text.as_ref(); 649 | let graphics = Graphics::get(); 650 | let sprite_manager = SpriteManager::get_mut(); 651 | 652 | // Currently no getTextTracking C API; assume none has been set. 653 | let tracking = 0; 654 | 655 | let width = graphics.get_system_text_width(text, tracking)?; 656 | 657 | let text_bitmap = 658 | graphics.new_bitmap(size2(width, SYSTEM_FONT_HEIGHT), background.clone())?; 659 | graphics.with_context(&text_bitmap, || { 660 | graphics.draw_text(text, point2(0, 0))?; 661 | Ok(()) 662 | })?; 663 | 664 | let mut sprite = sprite_manager.new_sprite()?; 665 | sprite.set_image(text_bitmap, LCDBitmapFlip::kBitmapUnflipped)?; 666 | sprite_manager.add_sprite(&sprite)?; 667 | 668 | Ok(Self { sprite, background }) 669 | } 670 | 671 | pub fn get_sprite(&self) -> &Sprite { 672 | &self.sprite 673 | } 674 | 675 | pub fn get_sprite_mut(&mut self) -> &mut Sprite { 676 | &mut self.sprite 677 | } 678 | 679 | /// Recreates the underlying bitmap with the given text; use `get_sprite().get_bounds()` 680 | /// to see the new size. 681 | pub fn update_text(&mut self, text: S) -> Result<(), Error> 682 | where 683 | S: AsRef, 684 | { 685 | let text = text.as_ref(); 686 | let graphics = Graphics::get(); 687 | 688 | // Currently no getTextTracking C API; assume none has been set. 689 | let tracking = 0; 690 | 691 | let width = graphics.get_system_text_width(text, tracking)?; 692 | 693 | let text_bitmap = 694 | graphics.new_bitmap(size2(width, SYSTEM_FONT_HEIGHT), self.background.clone())?; 695 | graphics.with_context(&text_bitmap, || { 696 | graphics.draw_text(text, point2(0, 0))?; 697 | Ok(()) 698 | })?; 699 | 700 | self.sprite 701 | .set_image(text_bitmap, LCDBitmapFlip::kBitmapUnflipped)?; 702 | 703 | Ok(()) 704 | } 705 | } 706 | 707 | /// This is a helper type for rotating and scaling an image in a sprite. 708 | /// 709 | /// After creation with `new`, you can `set_rotation` to update the parameters, and use 710 | /// `get_sprite` or `get_sprite_mut` to access the `Sprite` for other operations like `move_to` 711 | /// and `get_bounds` (which can tell you the height and width of the generated bitmap). 712 | /// 713 | /// Note: the image is rotated around its center point. If you want to rotate around another 714 | /// point, there are a few options: 715 | /// 1. Extend the image with transparent pixels in one direction so it appears to be rotating 716 | /// about another point. 717 | /// 2. Rotate about the center, then move the sprite to an equivalent position. 718 | /// 3. Manage the image and sprite manually: do the math to find the size after rotation, create 719 | /// a fresh Bitmap of that size, and use Graphics.draw_rotated() to draw into it, since 720 | /// draw_rotated allows specifying the center point. 721 | #[derive(Clone, Debug)] 722 | pub struct RotatedSprite { 723 | /// The managed sprite. 724 | sprite: Sprite, 725 | /// The original, unrotated/unscaled bitmap; use this rather than reading back a 726 | /// rotated/scaled image because of compounding error introduced in that process. 727 | bitmap: Bitmap, 728 | } 729 | 730 | impl RotatedSprite { 731 | /// Creates a `RotatedSprite`, draws the rotated and scaled image into it, and adds the 732 | /// underlying sprite to the `SpriteManager`. 733 | pub fn new(bitmap: Bitmap, angle: f32, scaling: Vector2D) -> Result { 734 | let rotated_bitmap = bitmap.rotated(angle, scaling)?; 735 | 736 | let sprite_manager = SpriteManager::get_mut(); 737 | let mut sprite = sprite_manager.new_sprite()?; 738 | sprite.set_image(rotated_bitmap, LCDBitmapFlip::kBitmapUnflipped)?; 739 | sprite_manager.add_sprite(&sprite)?; 740 | 741 | Ok(Self { sprite, bitmap }) 742 | } 743 | 744 | pub fn get_sprite(&self) -> &Sprite { 745 | &self.sprite 746 | } 747 | 748 | pub fn get_sprite_mut(&mut self) -> &mut Sprite { 749 | &mut self.sprite 750 | } 751 | 752 | /// Recreates the underlying bitmap with the given rotation angle and scaling; use 753 | /// `get_sprite().get_bounds()` to see the new size. 754 | pub fn set_rotation(&mut self, angle: f32, scaling: Vector2D) -> Result<(), Error> { 755 | let rotated_bitmap = self.bitmap.rotated(angle, scaling)?; 756 | self.sprite 757 | .set_image(rotated_bitmap, LCDBitmapFlip::kBitmapUnflipped)?; 758 | Ok(()) 759 | } 760 | } 761 | -------------------------------------------------------------------------------- /src/system.rs: -------------------------------------------------------------------------------- 1 | use alloc::boxed::Box; 2 | use alloc::rc::Rc; 3 | use alloc::string::String; 4 | use alloc::vec::Vec; 5 | use core::cell::RefCell; 6 | 7 | use anyhow::anyhow; 8 | 9 | use crankstart_sys::ctypes::{c_char, c_int}; 10 | pub use crankstart_sys::PDButtons; 11 | use crankstart_sys::{PDDateTime, PDLanguage, PDMenuItem, PDPeripherals}; 12 | use { 13 | crate::pd_func_caller, anyhow::Error, core::ptr, crankstart_sys::ctypes::c_void, 14 | cstr_core::CString, 15 | }; 16 | 17 | static mut SYSTEM: System = System(ptr::null_mut()); 18 | 19 | #[derive(Clone, Debug)] 20 | pub struct System(*const crankstart_sys::playdate_sys); 21 | 22 | impl System { 23 | pub(crate) fn new(system: *const crankstart_sys::playdate_sys) { 24 | unsafe { 25 | SYSTEM = Self(system); 26 | } 27 | } 28 | 29 | pub fn get() -> Self { 30 | unsafe { SYSTEM.clone() } 31 | } 32 | 33 | pub(crate) fn realloc(&self, ptr: *mut c_void, size: usize) -> *mut c_void { 34 | unsafe { 35 | let realloc_fn = (*self.0).realloc.expect("realloc"); 36 | realloc_fn(ptr, size) 37 | } 38 | } 39 | 40 | pub fn set_update_callback(&self, f: crankstart_sys::PDCallbackFunction) -> Result<(), Error> { 41 | pd_func_caller!((*self.0).setUpdateCallback, f, ptr::null_mut()) 42 | } 43 | 44 | pub fn get_button_state(&self) -> Result<(PDButtons, PDButtons, PDButtons), Error> { 45 | let mut current: PDButtons = PDButtons(0); 46 | let mut pushed: PDButtons = PDButtons(0); 47 | let mut released: PDButtons = PDButtons(0); 48 | pd_func_caller!( 49 | (*self.0).getButtonState, 50 | &mut current, 51 | &mut pushed, 52 | &mut released 53 | )?; 54 | Ok((current, pushed, released)) 55 | } 56 | 57 | extern "C" fn menu_item_callback(user_data: *mut core::ffi::c_void) { 58 | unsafe { 59 | let callback = user_data as *mut Box; 60 | (*callback)() 61 | } 62 | } 63 | 64 | /// Adds a option to the menu. The callback is called when the option is selected. 65 | pub fn add_menu_item(&self, title: &str, callback: Box) -> Result { 66 | let c_text = CString::new(title).map_err(|e| anyhow!("CString::new: {}", e))?; 67 | let wrapped_callback = Box::new(callback); 68 | let raw_callback_ptr = Box::into_raw(wrapped_callback); 69 | let raw_menu_item = pd_func_caller!( 70 | (*self.0).addMenuItem, 71 | c_text.as_ptr(), 72 | Some(Self::menu_item_callback), 73 | raw_callback_ptr as *mut c_void 74 | )?; 75 | Ok(MenuItem { 76 | inner: Rc::new(RefCell::new(MenuItemInner { 77 | item: raw_menu_item, 78 | raw_callback_ptr, 79 | })), 80 | kind: MenuItemKind::Normal, 81 | }) 82 | } 83 | 84 | /// Adds a option to the menu that has a checkbox. The initial_checked_state is the initial 85 | /// state of the checkbox. Callback will only be called when the menu is closed, not when the 86 | /// option is toggled. Use `System::get_menu_item_value` to get the state of the checkbox when 87 | /// the callback is called. 88 | pub fn add_checkmark_menu_item( 89 | &self, 90 | title: &str, 91 | initial_checked_state: bool, 92 | callback: Box, 93 | ) -> Result { 94 | let c_text = CString::new(title).map_err(|e| anyhow!("CString::new: {}", e))?; 95 | let wrapped_callback = Box::new(callback); 96 | let raw_callback_ptr = Box::into_raw(wrapped_callback); 97 | let raw_menu_item = pd_func_caller!( 98 | (*self.0).addCheckmarkMenuItem, 99 | c_text.as_ptr(), 100 | initial_checked_state as c_int, 101 | Some(Self::menu_item_callback), 102 | raw_callback_ptr as *mut c_void 103 | )?; 104 | 105 | Ok(MenuItem { 106 | inner: Rc::new(RefCell::new(MenuItemInner { 107 | item: raw_menu_item, 108 | raw_callback_ptr, 109 | })), 110 | kind: MenuItemKind::Checkmark, 111 | }) 112 | } 113 | 114 | /// Adds a option to the menu that has multiple values that can be cycled through. The initial 115 | /// value is the first element in `options`. Callback will only be called when the menu is 116 | /// closed, not when the option is toggled. Use `System::get_menu_item_value` to get the index 117 | /// of the options list when the callback is called, which can be used to lookup the value. 118 | pub fn add_options_menu_item( 119 | &self, 120 | title: &str, 121 | options: Vec, 122 | callback: Box, 123 | ) -> Result { 124 | let c_text = CString::new(title).map_err(|e| anyhow!("CString::new: {}", e))?; 125 | let options_count = options.len() as c_int; 126 | let c_options: Vec = options 127 | .iter() 128 | .map(|s| CString::new(s.clone()).map_err(|e| anyhow!("CString::new: {}", e))) 129 | .collect::, Error>>()?; 130 | let c_options_ptrs: Vec<*const c_char> = c_options.iter().map(|c| c.as_ptr()).collect(); 131 | let c_options_ptrs_ptr = c_options_ptrs.as_ptr(); 132 | let option_titles = c_options_ptrs_ptr as *mut *const c_char; 133 | let wrapped_callback = Box::new(callback); 134 | let raw_callback_ptr = Box::into_raw(wrapped_callback); 135 | let raw_menu_item = pd_func_caller!( 136 | (*self.0).addOptionsMenuItem, 137 | c_text.as_ptr(), 138 | option_titles, 139 | options_count, 140 | Some(Self::menu_item_callback), 141 | raw_callback_ptr as *mut c_void 142 | )?; 143 | Ok(MenuItem { 144 | inner: Rc::new(RefCell::new(MenuItemInner { 145 | item: raw_menu_item, 146 | raw_callback_ptr, 147 | })), 148 | kind: MenuItemKind::Options(options), 149 | }) 150 | } 151 | 152 | /// Returns the state of a given menu item. The meaning depends on the type of menu item. 153 | /// If it is the checkbox, the int represents the boolean checked state. If it's a option the 154 | /// int represents the index of the option array. 155 | pub fn get_menu_item_value(&self, item: &MenuItem) -> Result { 156 | let value = pd_func_caller!((*self.0).getMenuItemValue, item.inner.borrow().item)?; 157 | Ok(value as usize) 158 | } 159 | 160 | /// set the value of a given menu item. The meaning depends on the type of menu item. Picking 161 | /// the right value is left up to the caller, but is protected by the `MenuItemKind` of the 162 | /// `item` passed 163 | pub fn set_menu_item_value(&self, item: &MenuItem, new_value: usize) -> Result<(), Error> { 164 | match &item.kind { 165 | MenuItemKind::Normal => {} 166 | MenuItemKind::Checkmark => { 167 | if new_value > 1 { 168 | return Err(anyhow!( 169 | "Invalid value ({}) for checkmark menu item", 170 | new_value 171 | )); 172 | } 173 | } 174 | MenuItemKind::Options(opts) => { 175 | if new_value >= opts.len() { 176 | return Err(anyhow!( 177 | "Invalid value ({}) for options menu item, must be between 0 and {}", 178 | new_value, 179 | opts.len() - 1 180 | )); 181 | } 182 | } 183 | } 184 | pd_func_caller!( 185 | (*self.0).setMenuItemValue, 186 | item.inner.borrow().item, 187 | new_value as c_int 188 | ) 189 | } 190 | 191 | /// Set the title of a given menu item 192 | pub fn set_menu_item_title(&self, item: &MenuItem, new_title: &str) -> Result<(), Error> { 193 | let c_text = CString::new(new_title).map_err(|e| anyhow!("CString::new: {}", e))?; 194 | pd_func_caller!( 195 | (*self.0).setMenuItemTitle, 196 | item.inner.borrow().item, 197 | c_text.as_ptr() as *mut c_char 198 | ) 199 | } 200 | pub fn remove_menu_item(&self, item: MenuItem) -> Result<(), Error> { 201 | // Explicitly drops item. The actual calling of the removeMenuItem 202 | // (via `remove_menu_item_internal`) is done in the drop impl to avoid calling it multiple 203 | // times, even though that's been experimentally shown to be safe. 204 | drop(item); 205 | Ok(()) 206 | } 207 | fn remove_menu_item_internal(&self, item_inner: &MenuItemInner) -> Result<(), Error> { 208 | pd_func_caller!((*self.0).removeMenuItem, item_inner.item) 209 | } 210 | 211 | pub fn set_peripherals_enabled(&self, peripherals: PDPeripherals) -> Result<(), Error> { 212 | pd_func_caller!((*self.0).setPeripheralsEnabled, peripherals) 213 | } 214 | 215 | pub fn get_accelerometer(&self) -> Result<(f32, f32, f32), Error> { 216 | let mut outx = 0.0; 217 | let mut outy = 0.0; 218 | let mut outz = 0.0; 219 | pd_func_caller!((*self.0).getAccelerometer, &mut outx, &mut outy, &mut outz)?; 220 | Ok((outx, outy, outz)) 221 | } 222 | 223 | pub fn is_crank_docked(&self) -> Result { 224 | let docked: bool = pd_func_caller!((*self.0).isCrankDocked)? != 0; 225 | Ok(docked) 226 | } 227 | pub fn get_crank_angle(&self) -> Result { 228 | pd_func_caller!((*self.0).getCrankAngle,) 229 | } 230 | 231 | pub fn get_crank_change(&self) -> Result { 232 | pd_func_caller!((*self.0).getCrankChange,) 233 | } 234 | 235 | pub fn set_crank_sound_disabled(&self, disable: bool) -> Result { 236 | let last = pd_func_caller!((*self.0).setCrankSoundsDisabled, disable as i32)?; 237 | Ok(last != 0) 238 | } 239 | 240 | pub fn set_auto_lock_disabled(&self, disable: bool) -> Result<(), Error> { 241 | pd_func_caller!((*self.0).setAutoLockDisabled, disable as i32) 242 | } 243 | 244 | pub fn log_to_console(text: &str) { 245 | unsafe { 246 | if !SYSTEM.0.is_null() { 247 | if let Ok(c_text) = CString::new(text) { 248 | let log_to_console_fn = (*SYSTEM.0).logToConsole.expect("logToConsole"); 249 | log_to_console_fn(c_text.as_ptr() as *mut crankstart_sys::ctypes::c_char); 250 | } 251 | } 252 | } 253 | } 254 | 255 | pub fn log_to_console_raw(text: &str) { 256 | unsafe { 257 | if !SYSTEM.0.is_null() { 258 | let log_to_console_fn = (*SYSTEM.0).logToConsole.expect("logToConsole"); 259 | log_to_console_fn(text.as_ptr() as *mut crankstart_sys::ctypes::c_char); 260 | } 261 | } 262 | } 263 | 264 | pub fn error(text: &str) { 265 | unsafe { 266 | if !SYSTEM.0.is_null() { 267 | if let Ok(c_text) = CString::new(text) { 268 | let error_fn = (*SYSTEM.0).error.expect("error"); 269 | error_fn(c_text.as_ptr() as *mut crankstart_sys::ctypes::c_char); 270 | } 271 | } 272 | } 273 | } 274 | 275 | pub fn error_raw(text: &str) { 276 | unsafe { 277 | if !SYSTEM.0.is_null() { 278 | let error_fn = (*SYSTEM.0).error.expect("error"); 279 | error_fn(text.as_ptr() as *mut crankstart_sys::ctypes::c_char); 280 | } 281 | } 282 | } 283 | 284 | pub fn get_seconds_since_epoch(&self) -> Result<(usize, usize), Error> { 285 | let mut miliseconds = 0; 286 | let seconds = pd_func_caller!((*self.0).getSecondsSinceEpoch, &mut miliseconds)?; 287 | Ok((seconds as usize, miliseconds as usize)) 288 | } 289 | 290 | pub fn get_current_time_milliseconds(&self) -> Result { 291 | Ok(pd_func_caller!((*self.0).getCurrentTimeMilliseconds)? as usize) 292 | } 293 | 294 | pub fn get_timezone_offset(&self) -> Result { 295 | pd_func_caller!((*self.0).getTimezoneOffset) 296 | } 297 | 298 | pub fn convert_epoch_to_datetime(&self, epoch: u32) -> Result { 299 | let mut datetime = PDDateTime::default(); 300 | pd_func_caller!((*self.0).convertEpochToDateTime, epoch, &mut datetime)?; 301 | Ok(datetime) 302 | } 303 | 304 | pub fn convert_datetime_to_epoch(&self, datetime: &mut PDDateTime) -> Result { 305 | Ok(pd_func_caller!((*self.0).convertDateTimeToEpoch, datetime)? as usize) 306 | } 307 | 308 | pub fn should_display_24_hour_time(&self) -> Result { 309 | Ok(pd_func_caller!((*self.0).shouldDisplay24HourTime)? != 0) 310 | } 311 | 312 | pub fn reset_elapsed_time(&self) -> Result<(), Error> { 313 | pd_func_caller!((*self.0).resetElapsedTime) 314 | } 315 | 316 | pub fn get_elapsed_time(&self) -> Result { 317 | pd_func_caller!((*self.0).getElapsedTime) 318 | } 319 | 320 | pub fn get_flipped(&self) -> Result { 321 | Ok(pd_func_caller!((*self.0).getFlipped)? != 0) 322 | } 323 | 324 | pub fn get_reduced_flashing(&self) -> Result { 325 | Ok(pd_func_caller!((*self.0).getReduceFlashing)? != 0) 326 | } 327 | 328 | pub fn draw_fps(&self, x: i32, y: i32) -> Result<(), Error> { 329 | pd_func_caller!((*self.0).drawFPS, x, y) 330 | } 331 | 332 | pub fn get_battery_percentage(&self) -> Result { 333 | pd_func_caller!((*self.0).getBatteryPercentage) 334 | } 335 | 336 | pub fn get_battery_voltage(&self) -> Result { 337 | pd_func_caller!((*self.0).getBatteryVoltage) 338 | } 339 | 340 | pub fn get_language(&self) -> Result { 341 | pd_func_caller!((*self.0).getLanguage) 342 | } 343 | } 344 | 345 | /// The kind of menu item. See `System::add_{,checkmark_,options_}menu_item` for more details. 346 | pub enum MenuItemKind { 347 | Normal, 348 | Checkmark, 349 | Options(Vec), 350 | } 351 | 352 | pub struct MenuItemInner { 353 | item: *mut PDMenuItem, 354 | raw_callback_ptr: *mut Box, 355 | } 356 | 357 | impl Drop for MenuItemInner { 358 | fn drop(&mut self) { 359 | // We must remove the menu item on drop to avoid a memory or having the firmware read 360 | // unmanaged memory. 361 | System::get().remove_menu_item_internal(self).unwrap(); 362 | unsafe { 363 | // Recast into box to let Box deal with freeing the right memory 364 | let _ = Box::from_raw(self.raw_callback_ptr); 365 | } 366 | } 367 | } 368 | 369 | pub struct MenuItem { 370 | inner: Rc>, 371 | pub kind: MenuItemKind, 372 | } 373 | --------------------------------------------------------------------------------