From 95f4200cfb030d4a986c09e115bd6a87571f3e4b Mon Sep 17 00:00:00 2001 From: Alexander Koz Date: Sun, 1 Oct 2023 01:16:26 +0400 Subject: [PATCH] add scoreboards api (closes #45) --- Cargo.lock | 18 +- Cargo.toml | 1 + api/playdate/Cargo.toml | 9 +- api/playdate/src/lib.rs | 12 +- api/scoreboards/Cargo.toml | 62 ++++ api/scoreboards/README.md | 26 ++ api/scoreboards/examples/boards.rs | 83 +++++ api/scoreboards/src/error.rs | 43 +++ api/scoreboards/src/lib.rs | 525 +++++++++++++++++++++++++++++ api/scoreboards/src/storage.rs | 86 +++++ 10 files changed, 861 insertions(+), 4 deletions(-) create mode 100644 api/scoreboards/Cargo.toml create mode 100644 api/scoreboards/README.md create mode 100644 api/scoreboards/examples/boards.rs create mode 100644 api/scoreboards/src/error.rs create mode 100644 api/scoreboards/src/lib.rs create mode 100644 api/scoreboards/src/storage.rs diff --git a/Cargo.lock b/Cargo.lock index 3f165209..65cfe03e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -831,6 +831,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased_set" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5aa24577083f8190ad401e376b55887c7cd9083ae95d83ceec5d28ea78125" + [[package]] name = "errno" version = "0.3.3" @@ -2507,13 +2513,14 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "playdate" -version = "0.1.10" +version = "0.1.11" dependencies = [ "playdate-controls", "playdate-display", "playdate-fs", "playdate-graphics", "playdate-menu", + "playdate-scoreboards", "playdate-sound", "playdate-sprite", "playdate-sys", @@ -2615,6 +2622,15 @@ dependencies = [ "playdate-system", ] +[[package]] +name = "playdate-scoreboards" +version = "0.1.0" +dependencies = [ + "erased_set", + "playdate-sys", + "playdate-system", +] + [[package]] name = "playdate-sound" version = "0.2.7" diff --git a/Cargo.toml b/Cargo.toml index d68addcc..0ce7d727 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ display = { version = "0.3", path = "api/display", package = "playdate-display", fs = { version = "0.2", path = "api/fs", package = "playdate-fs", default-features = false } gfx = { version = "0.3", path = "api/gfx", package = "playdate-graphics", default-features = false } menu = { version = "0.2", path = "api/menu", package = "playdate-menu", default-features = false } +scoreboards = { version = "0.1", path = "api/scoreboards", package = "playdate-scoreboards", default-features = false } sound = { version = "0.2", path = "api/sound", package = "playdate-sound", default-features = false } sprite = { version = "0.2", path = "api/sprite", package = "playdate-sprite", default-features = false } system = { version = "0.3", path = "api/system", package = "playdate-system", default-features = false } diff --git a/api/playdate/Cargo.toml b/api/playdate/Cargo.toml index 02523dce..6200d48b 100644 --- a/api/playdate/Cargo.toml +++ b/api/playdate/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "playdate" -version = "0.1.10" +version = "0.1.11" readme = "README.md" description = "High-level Playdate API" keywords = ["playdate", "sdk", "api", "gamedev"] @@ -18,6 +18,7 @@ display = { workspace = true, default-features = false } fs = { workspace = true, default-features = false } gfx = { workspace = true, default-features = false } menu = { workspace = true, default-features = false } +scoreboards = { workspace = true, default-features = false } sound = { workspace = true, default-features = false } sprite = { workspace = true, default-features = false } system = { workspace = true, default-features = false } @@ -33,6 +34,7 @@ default = [ "fs/default", "gfx/default", "menu/default", + "scoreboards/default", "sound/default", "sprite/default", "system/default", @@ -40,7 +42,7 @@ default = [ ] # SDK v2.1 compatibility -sdk_2_1= ["gfx/sdk_2_1", "sprite/sdk_2_1"] +sdk_2_1 = ["gfx/sdk_2_1", "sprite/sdk_2_1"] lang-items = ["sys/lang-items"] allocator = ["sys/allocator"] @@ -58,6 +60,7 @@ bindgen-runtime = [ "fs/bindgen-runtime", "gfx/bindgen-runtime", "menu/bindgen-runtime", + "scoreboards/bindgen-runtime", "sound/bindgen-runtime", "sprite/bindgen-runtime", "system/bindgen-runtime", @@ -69,6 +72,7 @@ bindgen-static = [ "fs/bindgen-static", "gfx/bindgen-static", "menu/bindgen-static", + "scoreboards/bindgen-static", "sound/bindgen-static", "sprite/bindgen-static", "system/bindgen-static", @@ -83,6 +87,7 @@ bindings-derive-debug = [ "fs/bindings-derive-debug", "gfx/bindings-derive-debug", "menu/bindings-derive-debug", + "scoreboards/bindings-derive-debug", "sound/bindings-derive-debug", "sprite/bindings-derive-debug", "system/bindings-derive-debug", diff --git a/api/playdate/src/lib.rs b/api/playdate/src/lib.rs index b78e731b..3b4d0718 100644 --- a/api/playdate/src/lib.rs +++ b/api/playdate/src/lib.rs @@ -63,7 +63,8 @@ pub mod ext { // fn lua() -> lua::Lua; // fn json() -> json::Json; - // fn scoreboards() -> scoreboards::Scoreboards; + + fn scoreboards(&self) -> scoreboards::Scoreboards; } @@ -92,6 +93,10 @@ pub mod ext { fn sound(&self) -> sound::Sound { sound::Sound::new_with(sound::api::Cache::from(unsafe { self.as_ref() }.sound)) } + + fn scoreboards(&self) -> scoreboards::Scoreboards { + scoreboards::Scoreboards::new_with(scoreboards::api::Cache::from(unsafe { self.as_ref() }.scoreboards)) + } } impl PlaydateAPIExt for *const sys::ffi::PlaydateAPI { @@ -119,5 +124,10 @@ pub mod ext { fn sound(&self) -> sound::Sound { sound::Sound::new_with(sound::api::Cache::from(unsafe { self.as_ref() }.expect("api").sound)) } + + fn scoreboards(&self) -> scoreboards::Scoreboards { + let api = scoreboards::api::Cache::from(unsafe { self.as_ref() }.expect("api").scoreboards); + scoreboards::Scoreboards::new_with(api) + } } } diff --git a/api/scoreboards/Cargo.toml b/api/scoreboards/Cargo.toml new file mode 100644 index 00000000..61dc9f1f --- /dev/null +++ b/api/scoreboards/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "playdate-scoreboards" +version = "0.1.0" +readme = "README.md" +description = "High-level Scoreboards API built on-top of Playdate API" +keywords = ["playdate", "sdk", "api", "gamedev"] +categories = ["game-development", "api-bindings", "no-std"] +edition.workspace = true +license.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true + + +[features] +default = ["sys/default"] + +# playdate-sys features, should be shared because it's build configuration: + +bindgen-runtime = ["sys/bindgen-runtime"] +bindgen-static = ["sys/bindgen-static"] +bindings-derive-debug = ["sys/bindings-derive-debug"] + + +[dependencies] +sys = { workspace = true, default-features = false } +erased_set = "0.7.0" + + +[dev-dependencies] +system = { workspace = true, default-features = false, features = [ "try-trait-v2" ] } + + +[[example]] +name = "boards" +crate-type = ["dylib", "staticlib"] +path = "examples/boards.rs" +required-features = ["sys/lang-items", "sys/entry-point"] + +[package.metadata.playdate] +bundle-id = "rs.playdate.scoreboards" + + +[package.metadata.docs.rs] +all-features = false +features = [ + "bindings-derive-default", + "bindings-derive-eq", + "bindings-derive-copy", + "bindings-derive-debug", + "bindings-derive-hash", + "bindings-derive-ord", + "bindings-derive-partialeq", + "bindings-derive-partialord", +] +rustdoc-args = ["--cfg", "docsrs", "--show-type-layout"] +default-target = "thumbv7em-none-eabihf" +cargo-args = [ + "-Zunstable-options", + "-Zrustdoc-scrape-examples", + "-Zbuild-std=core,alloc", +] diff --git a/api/scoreboards/README.md b/api/scoreboards/README.md new file mode 100644 index 00000000..102a9824 --- /dev/null +++ b/api/scoreboards/README.md @@ -0,0 +1,26 @@ +# Scoreboards API for PlayDate + +High-level scoreboards API built on-top of [playdate-sys][]. + + +## Usage + +```rust +use playdate_scoreboards::*; +use playdate_sys::println; + +let scoreboards = Scoreboards::Cached(); + +scoreboards.get_scoreboards(|boards| { + println!("{boards:?}"); + }); +``` + + +[playdate-sys]: https://crates.io/crates/playdate-sys + + + +- - - + +This software is not sponsored or supported by Panic. diff --git a/api/scoreboards/examples/boards.rs b/api/scoreboards/examples/boards.rs new file mode 100644 index 00000000..9a969205 --- /dev/null +++ b/api/scoreboards/examples/boards.rs @@ -0,0 +1,83 @@ +#![no_std] +extern crate alloc; +use core::ptr::NonNull; + +#[macro_use] +extern crate sys; +extern crate playdate_scoreboards as scoreboards; + +use sys::EventLoopCtrl; +use sys::ffi::*; +use system::prelude::*; + +use scoreboards::ScoresResult; +use scoreboards::Scoreboards; + + +/// Entry point +#[no_mangle] +fn event_handler(_: NonNull, event: SystemEvent, _: u32) -> EventLoopCtrl { + // Ignore any other events, just for this minimalistic example + if !matches!(event, SystemEvent::Init) { + return EventLoopCtrl::Continue; + } + + const BOARD_ID: &str = "ID101"; + + let scoreboards = Scoreboards::Cached(); + + let res = scoreboards.add_score(BOARD_ID, 42, |res| { + println!("Add score callback"); + match res { + Ok(_) => println!("scores added"), + Err(err) => println!("{err}"), + } + }); + match res { + Ok(_) => println!("add_score res: F"), + Err(err) => println!("add_score res: ERR: {err}"), + } + + + scoreboards.get_scoreboards(|boards| { + println!("1: Get boards callback"); + println!("{boards:?}"); + }); + scoreboards.get_scoreboards(|boards| { + println!("2: Get boards callback"); + println!("{boards:?}"); + }); + + + fn get_scores(scores: ScoresResult) { + println!("1: Get scores callback"); + println!("{scores:?}"); + } + + scoreboards.get_scores(BOARD_ID, get_scores).ok(); + scoreboards.get_scores(BOARD_ID, |res| { + println!("2: Get scores callback"); + println!("{res:?}"); + }) + .ok(); + + + scoreboards.get_personal_best(BOARD_ID, |res| { + println!("Get personal best callback"); + match res { + Ok(_) => todo!("scores received"), + Err(err) => println!("{err}"), + } + }) + .ok(); + + + // Set no-op update callback + system::System::Default().set_update_callback_boxed(|_| UpdateCtrl::Continue, ()); + + EventLoopCtrl::Continue +} + + +// Needed for debug build +ll_symbols!(); diff --git a/api/scoreboards/src/error.rs b/api/scoreboards/src/error.rs new file mode 100644 index 00000000..e169d4d6 --- /dev/null +++ b/api/scoreboards/src/error.rs @@ -0,0 +1,43 @@ +use core::ffi::c_char; +use alloc::borrow::Cow; +use sys::ffi::CStr; + + +pub type ApiError = sys::error::Error; + + +#[derive(Debug, Clone)] +#[must_use = "Error message doesn’t live long enough"] +pub enum Error { + Response(Cow<'static, str>), + + /// Unknown error. + /// Usually means invalid input or something not found. + Unknown, +} + +impl Error { + pub fn from_ptr(ptr: *const c_char) -> Option { + if ptr.is_null() { + None + } else { + let s = unsafe { CStr::from_ptr(ptr as _) }.to_string_lossy(); + Self::Response(s).into() + } + } +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Error::Response(s) => write!(f, "{s}"), + Error::Unknown => write!(f, "Unknown"), + } + } +} + +impl core::error::Error for Error {} + +impl Into for Error { + fn into(self) -> ApiError { ApiError::Api(self) } +} diff --git a/api/scoreboards/src/lib.rs b/api/scoreboards/src/lib.rs new file mode 100644 index 00000000..1dac4125 --- /dev/null +++ b/api/scoreboards/src/lib.rs @@ -0,0 +1,525 @@ +//! Playdate Scoreboards API. +//! +//! Wraps C-API. +//! [Official documentation](https://help.play.date/catalog-developer/scoreboard-api/#c-api-reference). + +#![cfg_attr(not(test), no_std)] +#![feature(error_in_core)] + +#[macro_use] +extern crate sys; +extern crate alloc; + +use core::ffi::c_char; +use core::ffi::c_uint; +use alloc::borrow::Cow; + +use sys::ffi::CStr; +use sys::ffi::CString; +use sys::ffi::PDBoard; +use sys::ffi::PDBoardsList; +use sys::ffi::PDScore; +use sys::ffi::PDScoresList; + + +pub mod error; +mod storage; + +use error::*; +use storage::*; + + +pub type ScoresResult = Result; + + +#[derive(Debug, Clone, Copy)] +pub struct Scoreboards(Api); + +impl Scoreboards { + /// Creates default [`Scoreboards`] without type parameter requirement. + /// + /// Uses ZST [`api::Default`]. + #[allow(non_snake_case)] + pub fn Default() -> Self { Self(Default::default()) } +} + +impl Scoreboards { + /// Creates [`Scoreboards`] without type parameter requirement. + /// + /// Uses [`api::Cache`]. + #[allow(non_snake_case)] + pub fn Cached() -> Self { Self(Default::default()) } +} + +impl Default for Scoreboards { + fn default() -> Self { Self(Default::default()) } +} + +impl Scoreboards { + pub fn new() -> Self { Self(Default::default()) } +} + +impl Scoreboards { + pub fn new_with(api: Api) -> Self { Self(api) } +} + + +impl Scoreboards { + /// Requests to add score `value` to the board with given `board_id`. + /// + /// Safety: read description for [`Scoreboards::get_scoreboards`]. + /// + /// Equivalent to [`sys::ffi::playdate_scoreboards::addScore`]. + #[doc(alias = "sys::ffi::scoreboards::addScore")] + pub fn add_score, F: FnMut(ScoresResult)>(&self, + board_id: S, + value: u32, + callback: F) + -> Result, ApiError> + where F: 'static + Send + { + let id = CString::new(board_id.as_ref())?; + + init_store(); + let prev = unsafe { STORE.as_mut() }.expect("impossible") + .insert::(callback); + let f = self.0.add_score(); + + let result = unsafe { f(id.as_ptr() as _, value, Some(proxy_score::)) }; + + if result != 0 { + Err(Error::Unknown.into()) + } else { + Ok(prev) + } + } + + + /// Requests user's personal best scores for the given `board`. + /// + /// Safety: read description for [`Scoreboards::get_scoreboards`]. + /// + /// Equivalent to [`sys::ffi::playdate_scoreboards::getPersonalBest`]. + #[doc(alias = "sys::ffi::scoreboards::getPersonalBest")] + pub fn get_personal_best_for)>(&self, + board: &Board, + callback: F) + -> Result, ApiError> + where F: 'static + Send + { + self.get_personal_best(board.id().expect("board.id"), callback) + } + + /// Requests user's personal best scores for the given `board_id`. + /// + /// Safety: read description for [`Scoreboards::get_scoreboards`]. + /// + /// Equivalent to [`sys::ffi::playdate_scoreboards::getPersonalBest`]. + #[doc(alias = "sys::ffi::scoreboards::getPersonalBest")] + pub fn get_personal_best, F: FnMut(ScoresResult)>(&self, + board_id: S, + callback: F) + -> Result, ApiError> + where F: 'static + Send + { + let id = CString::new(board_id.as_ref())?; + + init_store(); + let prev = unsafe { STORE.as_mut() }.expect("impossible") + .insert::(callback); + let f = self.0.get_personal_best(); + + let result = unsafe { f(id.as_ptr() as _, Some(proxy_score::)) }; + + if result != 0 { + Err(Error::Unknown.into()) + } else { + Ok(prev) + } + } + + + /// Requests scores list [`Scores`] for the given `board_id`. + /// + /// Safety: read description for [`Scoreboards::get_scoreboards`]. + /// + /// Equivalent to [`sys::ffi::playdate_scoreboards::getScores`]. + #[doc(alias = "sys::ffi::scoreboards::getScores")] + pub fn get_scores, F: FnMut(ScoresResult)>(&self, + board_id: S, + callback: F) + -> Result, ApiError> + where F: 'static + Send + { + let id = CString::new(board_id.as_ref())?; + + init_store(); + let prev = unsafe { STORE.as_mut() }.expect("impossible") + .insert::(callback); + let f = self.0.get_scores(); + + let result = unsafe { f(id.as_ptr() as _, Some(proxy_scores::)) }; + + if result != 0 { + Err(Error::Unknown.into()) + } else { + Ok(prev) + } + } + + + /// Requests boards list [`Boards`] for the given `board_id`. + /// + /// Returns previous callback `F` if it exists, so it was overwritten. + /// Usually, it's not possible fo closures because until it's type is not erased. + /// Anyway if it happened, we just override it with new one, given as `callback`, + /// so responses will be passed to the new callback. + /// + /// Equivalent to [`sys::ffi::playdate_scoreboards::getScoreboards`]. + #[doc(alias = "sys::ffi::scoreboards::getScoreboards")] + pub fn get_scoreboards)>(&self, callback: F) -> Option + where F: 'static + Send { + init_store(); + let prev = unsafe { STORE.as_mut() }.expect("impossible") + .insert::(callback); + let f = self.0.get_scoreboards(); + unsafe { f(Some(proxy_boards::)) }; + + prev + } +} + + +pub struct Boards(*mut PDBoardsList); + +impl core::fmt::Debug for Boards { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let mut t = f.debug_tuple("Boards"); + self.boards().into_iter().for_each(|board| { + t.field(board); + }); + t.finish() + } +} + +impl Boards { + pub fn last_updated(&self) -> u32 { unsafe { (*self.0).lastUpdated } } + + pub fn boards(&self) -> &[Board] { + let count = unsafe { (*self.0).count }; + let ptr = unsafe { (*self.0).boards }; + let slice = unsafe { core::slice::from_raw_parts(ptr, count as _) }; + unsafe { core::mem::transmute(slice) } + } + + pub fn boards_mut(&mut self) -> &mut [Board] { + let count = unsafe { (*self.0).count }; + let ptr = unsafe { (*self.0).boards }; + let slice = unsafe { core::slice::from_raw_parts_mut(ptr, count as _) }; + unsafe { core::mem::transmute(slice) } + } +} + + +#[repr(transparent)] +pub struct Board(PDBoard); + +impl core::fmt::Debug for Board { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Board") + .field("id", &self.id()) + .field("name", &self.name()) + .finish() + } +} + +impl Board { + pub fn id<'s>(&'s self) -> Option> { + let ptr = self.0.boardID; + if ptr.is_null() { + None + } else { + unsafe { CStr::from_ptr(ptr as _) }.to_string_lossy().into() + } + } + + pub fn name<'s>(&'s self) -> Option> { + let ptr = self.0.name; + if ptr.is_null() { + None + } else { + unsafe { CStr::from_ptr(ptr as _) }.to_string_lossy().into() + } + } +} + +impl Drop for Boards { + fn drop(&mut self) { + if !self.0.is_null() { + let get_fn = || sys::api_opt!(scoreboards.freeBoardsList); + if let Some(f) = get_fn() { + unsafe { f(self.0) } + } + } + } +} + + +pub struct Scores(*mut PDScoresList); + +impl core::fmt::Debug for Scores { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Scores") + .field("id", &self.id()) + .field("count", &self.len()) + .field("capacity", &self.capacity()) + .field("last_updated", &self.last_updated()) + .field("playerIncluded", &self.player_included()) + .finish() + } +} + +impl Drop for Scores { + fn drop(&mut self) { + if !self.0.is_null() { + let get_fn = || sys::api_opt!(scoreboards.freeScoresList); + if let Some(f) = get_fn() { + unsafe { f(self.0) } + } + } + } +} + +impl Scores { + /// ID of associated board. + pub fn id<'s>(&'s self) -> Option> { + let ptr = unsafe { (*self.0).boardID }; + if ptr.is_null() { + None + } else { + unsafe { CStr::from_ptr(ptr as _) }.to_string_lossy().into() + } + } + + pub fn last_updated(&self) -> u32 { unsafe { (*self.0).lastUpdated } } + pub fn player_included(&self) -> bool { unsafe { (*self.0).playerIncluded == 1 } } + + pub fn len(&self) -> c_uint { unsafe { (*self.0).count } } + pub fn capacity(&self) -> c_uint { unsafe { (*self.0).limit } } + + pub fn scores(&self) -> &[Score] { + let count = self.len(); + let ptr = unsafe { (*self.0).scores }; + let slice = unsafe { core::slice::from_raw_parts(ptr, count as _) }; + unsafe { core::mem::transmute(slice) } + } +} + + +#[repr(transparent)] +pub struct Score(PDScore); + +impl core::fmt::Debug for Score { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Score") + .field("rank", &self.rank()) + .field("value", &self.value()) + .field("player", &self.player()) + .finish() + } +} + +impl Score { + pub fn rank(&self) -> u32 { self.0.rank } + pub fn value(&self) -> u32 { self.0.value } + + pub fn player<'s>(&'s self) -> Option> { + let ptr = self.0.player; + if ptr.is_null() { + None + } else { + unsafe { CStr::from_ptr(ptr as _) }.to_string_lossy().into() + } + } +} + +#[repr(transparent)] +pub struct ScoreRef(*mut PDScore); + +impl Drop for ScoreRef { + fn drop(&mut self) { + if !self.0.is_null() { + let get_fn = || sys::api_opt!(scoreboards.freeScore); + if let Some(f) = get_fn() { + unsafe { f(self.0) } + } + } + } +} + + +#[cfg(test)] +mod tests { + use super::*; + use core::mem::size_of; + + + #[test] + fn board_size() { + assert_eq!(size_of::(), size_of::()); + } + + #[test] + fn score_size() { + assert_eq!(size_of::(), size_of::()); + } + + #[test] + fn score_ref_size() { + assert_eq!(size_of::(), size_of::<*mut PDScore>()); + } +} + + +pub mod api { + use core::ffi::c_char; + use core::ffi::c_int; + use core::ptr::NonNull; + + use sys::ffi::AddScoreCallback; + use sys::ffi::PDBoardsList; + use sys::ffi::PDScore; + use sys::ffi::PDScoresList; + use sys::ffi::ScoresCallback; + use sys::ffi::BoardsListCallback; + use sys::ffi::PersonalBestCallback; + use sys::ffi::playdate_scoreboards; + + + /// Default scoreboards api end-point, ZST. + /// + /// All calls approximately costs ~3 derefs. + #[derive(Debug, Clone, Copy, core::default::Default)] + pub struct Default; + impl Api for Default {} + + + /// Cached scoreboards api end-point. + /// + /// Stores one reference, so size on stack is eq `usize`. + /// + /// All calls approximately costs ~1 deref. + #[derive(Clone, Copy)] + #[cfg_attr(feature = "bindings-derive-debug", derive(Debug))] + pub struct Cache(&'static playdate_scoreboards); + + impl core::default::Default for Cache { + fn default() -> Self { Self(sys::api!(scoreboards)) } + } + + impl From<*const playdate_scoreboards> for Cache { + #[inline(always)] + fn from(ptr: *const playdate_scoreboards) -> Self { Self(unsafe { ptr.as_ref() }.expect("scoreboards")) } + } + + impl From<&'static playdate_scoreboards> for Cache { + #[inline(always)] + fn from(r: &'static playdate_scoreboards) -> Self { Self(r) } + } + + impl From> for Cache { + #[inline(always)] + fn from(ptr: NonNull) -> Self { Self(unsafe { ptr.as_ref() }) } + } + + impl From<&'_ NonNull> for Cache { + #[inline(always)] + fn from(ptr: &NonNull) -> Self { Self(unsafe { ptr.as_ref() }) } + } + + impl Api for Cache { + fn add_score( + &self) + -> unsafe extern "C" fn(boardId: *const c_char, value: u32, callback: AddScoreCallback) -> c_int { + self.0.addScore.expect("addScore") + } + + fn get_personal_best( + &self) + -> unsafe extern "C" fn(boardId: *const c_char, callback: PersonalBestCallback) -> c_int { + self.0.getPersonalBest.expect("getPersonalBest") + } + + fn free_score(&self) -> unsafe extern "C" fn(score: *mut PDScore) { self.0.freeScore.expect("freeScore") } + + fn get_scoreboards(&self) -> unsafe extern "C" fn(callback: BoardsListCallback) -> c_int { + self.0.getScoreboards.expect("getScoreboards") + } + + fn free_boards_list(&self) -> unsafe extern "C" fn(boardsList: *mut PDBoardsList) { + self.0.freeBoardsList.expect("freeBoardsList") + } + + fn get_scores(&self) -> unsafe extern "C" fn(board_id: *const c_char, callback: ScoresCallback) -> c_int { + self.0.getScores.expect("getScores") + } + + fn free_scores_list(&self) -> unsafe extern "C" fn(scores_list: *mut PDScoresList) { + self.0.freeScoresList.expect("freeScoresList") + } + } + + + pub trait Api { + /// Returns [`sys::ffi::playdate_scoreboards::addScore`] + #[doc(alias = "sys::ffi::scoreboards::addScore")] + #[inline(always)] + fn add_score( + &self) + -> unsafe extern "C" fn(boardId: *const c_char, value: u32, callback: AddScoreCallback) -> c_int { + *sys::api!(scoreboards.addScore) + } + + /// Returns [`sys::ffi::playdate_scoreboards::getPersonalBest`] + #[doc(alias = "sys::ffi::scoreboards::getPersonalBest")] + #[inline(always)] + fn get_personal_best( + &self) + -> unsafe extern "C" fn(boardId: *const c_char, callback: PersonalBestCallback) -> c_int { + *sys::api!(scoreboards.getPersonalBest) + } + + /// Returns [`sys::ffi::playdate_scoreboards::freeScore`] + #[doc(alias = "sys::ffi::scoreboards::freeScore")] + #[inline(always)] + fn free_score(&self) -> unsafe extern "C" fn(score: *mut PDScore) { *sys::api!(scoreboards.freeScore) } + + /// Returns [`sys::ffi::playdate_scoreboards::getScoreboards`] + #[doc(alias = "sys::ffi::scoreboards::getScoreboards")] + #[inline(always)] + fn get_scoreboards(&self) -> unsafe extern "C" fn(callback: BoardsListCallback) -> c_int { + *sys::api!(scoreboards.getScoreboards) + } + + /// Returns [`sys::ffi::playdate_scoreboards::freeBoardsList`] + #[doc(alias = "sys::ffi::scoreboards::freeBoardsList")] + #[inline(always)] + fn free_boards_list(&self) -> unsafe extern "C" fn(boardsList: *mut PDBoardsList) { + *sys::api!(scoreboards.freeBoardsList) + } + + /// Returns [`sys::ffi::playdate_scoreboards::getScores`] + #[doc(alias = "sys::ffi::scoreboards::getScores")] + #[inline(always)] + fn get_scores(&self) -> unsafe extern "C" fn(board_id: *const c_char, callback: ScoresCallback) -> c_int { + *sys::api!(scoreboards.getScores) + } + + /// Returns [`sys::ffi::playdate_scoreboards::freeScoresList`] + #[doc(alias = "sys::ffi::scoreboards::freeScoresList")] + #[inline(always)] + fn free_scores_list(&self) -> unsafe extern "C" fn(scores_list: *mut PDScoresList) { + *sys::api!(scoreboards.freeScoresList) + } + } +} diff --git a/api/scoreboards/src/storage.rs b/api/scoreboards/src/storage.rs new file mode 100644 index 00000000..aa45834c --- /dev/null +++ b/api/scoreboards/src/storage.rs @@ -0,0 +1,86 @@ +use crate::*; +use erased_set::ErasedSendSet; + + +pub static mut STORE: Option = None; + + +pub fn init_store() { + if unsafe { STORE.is_none() } { + unsafe { STORE = Some(ErasedSendSet::new()) } + } +} + +pub fn clean_store() { + if let Some(true) = unsafe { STORE.as_mut() }.map(|store| store.is_empty()) { + unsafe { STORE = None } + println!("store cleaned up"); + } +} + + +pub unsafe extern "C" fn proxy_boards)>(boards: *mut PDBoardsList, + error: *const c_char) { + let res = if boards.is_null() { + Err(Error::from_ptr(error).expect("unable read err")) + } else { + if !error.is_null() { + let err = Error::from_ptr(error).expect("unable read err"); + sys::println!("Err: {err}"); + } + + Ok(Boards(boards)) + }; + + + let f = unsafe { STORE.as_mut() }.map(|store| store.remove::()) + .flatten(); + f.map(|mut f| f(res)).or_else(|| panic!("missed callback")); + + // cleanup the storage + clean_store(); +} + + +pub unsafe extern "C" fn proxy_scores)>(scores: *mut PDScoresList, + error_message: *const c_char) +{ + let res = if scores.is_null() { + Err(Error::from_ptr(error_message).expect("unable read err")) + } else { + if !error_message.is_null() { + let err = Error::from_ptr(error_message).expect("unable read err"); + sys::println!("Err: {err}"); + } + + Ok(Scores(scores)) + }; + + let f = unsafe { STORE.as_mut() }.map(|store| store.remove::()) + .flatten(); + f.map(|mut f| f(res)).or_else(|| panic!("missed callback")); + + // cleanup the storage + clean_store(); +} + +pub unsafe extern "C" fn proxy_score)>(score: *mut PDScore, + error: *const c_char) { + let res = if score.is_null() { + Err(Error::from_ptr(error).expect("unable read err")) + } else { + if !error.is_null() { + let err = Error::from_ptr(error).expect("unable read err"); + sys::println!("Err: {err}"); + } + + Ok(ScoreRef(score)) + }; + + let f = unsafe { STORE.as_mut() }.map(|store| store.remove::()) + .flatten(); + f.map(|mut f| f(res)).or_else(|| panic!("missed callback")); + + // cleanup the storage + clean_store(); +}