playdate_scoreboards/
lib.rs

1//! Playdate Scoreboards API.
2//!
3//! Wraps C-API.
4//! [Official documentation](https://help.play.date/catalog-developer/scoreboard-api/#c-api-reference).
5
6#![cfg_attr(not(test), no_std)]
7
8#[macro_use]
9extern crate sys;
10extern crate alloc;
11
12use core::ffi::c_char;
13use core::ffi::c_uint;
14use alloc::borrow::Cow;
15
16use sys::ffi::CStr;
17use sys::ffi::CString;
18use sys::ffi::PDBoard;
19use sys::ffi::PDBoardsList;
20use sys::ffi::PDScore;
21use sys::ffi::PDScoresList;
22
23
24pub mod error;
25mod storage;
26
27use error::*;
28use storage::*;
29
30
31pub type ScoresResult<T> = Result<T, Error>;
32
33
34#[derive(Debug, Clone, Copy)]
35pub struct Scoreboards<Api = api::Default>(Api);
36
37impl Scoreboards<api::Default> {
38	/// Creates default [`Scoreboards`] without type parameter requirement.
39	///
40	/// Uses ZST [`api::Default`].
41	#[allow(non_snake_case)]
42	pub fn Default() -> Self { Self(Default::default()) }
43}
44
45impl Scoreboards<api::Cache> {
46	/// Creates [`Scoreboards`] without type parameter requirement.
47	///
48	/// Uses [`api::Cache`].
49	#[allow(non_snake_case)]
50	pub fn Cached() -> Self { Self(Default::default()) }
51}
52
53impl<Api: Default + api::Api> Default for Scoreboards<Api> {
54	fn default() -> Self { Self(Default::default()) }
55}
56
57impl<Api: Default + api::Api> Scoreboards<Api> {
58	pub fn new() -> Self { Self(Default::default()) }
59}
60
61impl<Api: api::Api> Scoreboards<Api> {
62	pub fn new_with(api: Api) -> Self { Self(api) }
63}
64
65
66impl<Api: api::Api> Scoreboards<Api> {
67	/// Requests to add score `value` to the board with given `board_id`.
68	///
69	/// Safety: read description for [`Scoreboards::get_scoreboards`].
70	///
71	/// Equivalent to [`sys::ffi::playdate_scoreboards::addScore`].
72	#[doc(alias = "sys::ffi::scoreboards::addScore")]
73	pub fn add_score<S: AsRef<str>, F: FnMut(ScoresResult<ScoreRef>)>(&self,
74	                                                                  board_id: S,
75	                                                                  value: u32,
76	                                                                  callback: F)
77	                                                                  -> Result<Option<F>, ApiError>
78		where F: 'static + Send
79	{
80		let id = CString::new(board_id.as_ref())?;
81
82		init_store();
83		let prev = unsafe { STORE.as_mut() }.expect("impossible")
84		                                    .insert::<F>(callback);
85		let f = self.0.add_score();
86
87		let result = unsafe { f(id.as_ptr() as _, value, Some(proxy_score::<F>)) };
88
89		if result != 0 {
90			Err(Error::Unknown.into())
91		} else {
92			Ok(prev)
93		}
94	}
95
96
97	/// Requests user's personal best scores for the given `board`.
98	///
99	/// Safety: read description for [`Scoreboards::get_scoreboards`].
100	///
101	/// Equivalent to [`sys::ffi::playdate_scoreboards::getPersonalBest`].
102	#[doc(alias = "sys::ffi::scoreboards::getPersonalBest")]
103	pub fn get_personal_best_for<F: FnMut(ScoresResult<ScoreRef>)>(&self,
104	                                                               board: &Board,
105	                                                               callback: F)
106	                                                               -> Result<Option<F>, ApiError>
107		where F: 'static + Send
108	{
109		self.get_personal_best(board.id().expect("board.id"), callback)
110	}
111
112	/// Requests user's personal best scores for the given `board_id`.
113	///
114	/// Safety: read description for [`Scoreboards::get_scoreboards`].
115	///
116	/// Equivalent to [`sys::ffi::playdate_scoreboards::getPersonalBest`].
117	#[doc(alias = "sys::ffi::scoreboards::getPersonalBest")]
118	pub fn get_personal_best<S: AsRef<str>, F: FnMut(ScoresResult<ScoreRef>)>(&self,
119	                                                                          board_id: S,
120	                                                                          callback: F)
121	                                                                          -> Result<Option<F>, ApiError>
122		where F: 'static + Send
123	{
124		let id = CString::new(board_id.as_ref())?;
125
126		init_store();
127		let prev = unsafe { STORE.as_mut() }.expect("impossible")
128		                                    .insert::<F>(callback);
129		let f = self.0.get_personal_best();
130
131		let result = unsafe { f(id.as_ptr() as _, Some(proxy_score::<F>)) };
132
133		if result != 0 {
134			Err(Error::Unknown.into())
135		} else {
136			Ok(prev)
137		}
138	}
139
140
141	/// Requests scores list [`Scores`] for the given `board_id`.
142	///
143	/// Safety: read description for [`Scoreboards::get_scoreboards`].
144	///
145	/// Equivalent to [`sys::ffi::playdate_scoreboards::getScores`].
146	#[doc(alias = "sys::ffi::scoreboards::getScores")]
147	pub fn get_scores<S: AsRef<str>, F: FnMut(ScoresResult<Scores>)>(&self,
148	                                                                 board_id: S,
149	                                                                 callback: F)
150	                                                                 -> Result<Option<F>, ApiError>
151		where F: 'static + Send
152	{
153		let id = CString::new(board_id.as_ref())?;
154
155		init_store();
156		let prev = unsafe { STORE.as_mut() }.expect("impossible")
157		                                    .insert::<F>(callback);
158		let f = self.0.get_scores();
159
160		let result = unsafe { f(id.as_ptr() as _, Some(proxy_scores::<F>)) };
161
162		if result != 0 {
163			Err(Error::Unknown.into())
164		} else {
165			Ok(prev)
166		}
167	}
168
169
170	/// Requests boards list [`Boards`] for the given `board_id`.
171	///
172	/// Returns previous callback `F` if it exists, so it was overwritten.
173	/// Usually, it's not possible fo closures because until it's type is not erased.
174	/// Anyway if it happened, we just override it with new one, given as `callback`,
175	/// so responses will be passed to the new callback.
176	///
177	/// Equivalent to [`sys::ffi::playdate_scoreboards::getScoreboards`].
178	#[doc(alias = "sys::ffi::scoreboards::getScoreboards")]
179	pub fn get_scoreboards<F: FnMut(ScoresResult<Boards>)>(&self, callback: F) -> Option<F>
180		where F: 'static + Send {
181		init_store();
182		let prev = unsafe { STORE.as_mut() }.expect("impossible")
183		                                    .insert::<F>(callback);
184		let f = self.0.get_scoreboards();
185		unsafe { f(Some(proxy_boards::<F>)) };
186
187		prev
188	}
189}
190
191
192pub struct Boards(*mut PDBoardsList);
193
194impl core::fmt::Debug for Boards {
195	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
196		let mut t = f.debug_tuple("Boards");
197		self.boards().into_iter().for_each(|board| {
198			                         t.field(board);
199		                         });
200		t.finish()
201	}
202}
203
204impl Boards {
205	pub fn last_updated(&self) -> u32 { unsafe { (*self.0).lastUpdated } }
206
207	pub fn boards(&self) -> &[Board] {
208		let count = unsafe { (*self.0).count };
209		let ptr = unsafe { (*self.0).boards };
210		let slice = unsafe { core::slice::from_raw_parts(ptr, count as _) };
211		unsafe { core::mem::transmute(slice) }
212	}
213
214	pub fn boards_mut(&mut self) -> &mut [Board] {
215		let count = unsafe { (*self.0).count };
216		let ptr = unsafe { (*self.0).boards };
217		let slice = unsafe { core::slice::from_raw_parts_mut(ptr, count as _) };
218		unsafe { core::mem::transmute(slice) }
219	}
220}
221
222
223#[repr(transparent)]
224pub struct Board(PDBoard);
225
226impl core::fmt::Debug for Board {
227	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
228		f.debug_struct("Board")
229		 .field("id", &self.id())
230		 .field("name", &self.name())
231		 .finish()
232	}
233}
234
235impl Board {
236	pub fn id<'s>(&'s self) -> Option<Cow<'s, str>> {
237		let ptr = self.0.boardID;
238		if ptr.is_null() {
239			None
240		} else {
241			unsafe { CStr::from_ptr(ptr as _) }.to_string_lossy().into()
242		}
243	}
244
245	pub fn name<'s>(&'s self) -> Option<Cow<'s, str>> {
246		let ptr = self.0.name;
247		if ptr.is_null() {
248			None
249		} else {
250			unsafe { CStr::from_ptr(ptr as _) }.to_string_lossy().into()
251		}
252	}
253}
254
255impl Drop for Boards {
256	fn drop(&mut self) {
257		if !self.0.is_null() {
258			let get_fn = || sys::api_opt!(scoreboards.freeBoardsList);
259			if let Some(f) = get_fn() {
260				unsafe { f(self.0) }
261			}
262		}
263	}
264}
265
266
267pub struct Scores(*mut PDScoresList);
268
269impl core::fmt::Debug for Scores {
270	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
271		f.debug_struct("Scores")
272		 .field("id", &self.id())
273		 .field("count", &self.len())
274		 .field("capacity", &self.capacity())
275		 .field("last_updated", &self.last_updated())
276		 .field("playerIncluded", &self.player_included())
277		 .finish()
278	}
279}
280
281impl Drop for Scores {
282	fn drop(&mut self) {
283		if !self.0.is_null() {
284			let get_fn = || sys::api_opt!(scoreboards.freeScoresList);
285			if let Some(f) = get_fn() {
286				unsafe { f(self.0) }
287			}
288		}
289	}
290}
291
292impl Scores {
293	/// ID of associated board.
294	pub fn id<'s>(&'s self) -> Option<Cow<'s, str>> {
295		let ptr = unsafe { (*self.0).boardID };
296		if ptr.is_null() {
297			None
298		} else {
299			unsafe { CStr::from_ptr(ptr as _) }.to_string_lossy().into()
300		}
301	}
302
303	pub fn last_updated(&self) -> u32 { unsafe { (*self.0).lastUpdated } }
304	pub fn player_included(&self) -> bool { unsafe { (*self.0).playerIncluded == 1 } }
305
306	pub fn len(&self) -> c_uint { unsafe { (*self.0).count } }
307	pub fn capacity(&self) -> c_uint { unsafe { (*self.0).limit } }
308
309	pub fn scores(&self) -> &[Score] {
310		let count = self.len();
311		let ptr = unsafe { (*self.0).scores };
312		let slice = unsafe { core::slice::from_raw_parts(ptr, count as _) };
313		unsafe { core::mem::transmute(slice) }
314	}
315}
316
317
318#[repr(transparent)]
319pub struct Score(PDScore);
320
321impl core::fmt::Debug for Score {
322	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
323		f.debug_struct("Score")
324		 .field("rank", &self.rank())
325		 .field("value", &self.value())
326		 .field("player", &self.player())
327		 .finish()
328	}
329}
330
331impl Score {
332	pub fn rank(&self) -> u32 { self.0.rank }
333	pub fn value(&self) -> u32 { self.0.value }
334
335	pub fn player<'s>(&'s self) -> Option<Cow<'s, str>> {
336		let ptr = self.0.player;
337		if ptr.is_null() {
338			None
339		} else {
340			unsafe { CStr::from_ptr(ptr as _) }.to_string_lossy().into()
341		}
342	}
343}
344
345#[repr(transparent)]
346pub struct ScoreRef(*mut PDScore);
347
348impl Drop for ScoreRef {
349	fn drop(&mut self) {
350		if !self.0.is_null() {
351			let get_fn = || sys::api_opt!(scoreboards.freeScore);
352			if let Some(f) = get_fn() {
353				unsafe { f(self.0) }
354			}
355		}
356	}
357}
358
359
360#[cfg(test)]
361mod tests {
362	use super::*;
363	use core::mem::size_of;
364
365
366	#[test]
367	fn board_size() {
368		assert_eq!(size_of::<Board>(), size_of::<PDBoard>());
369	}
370
371	#[test]
372	fn score_size() {
373		assert_eq!(size_of::<Score>(), size_of::<PDScore>());
374	}
375
376	#[test]
377	fn score_ref_size() {
378		assert_eq!(size_of::<ScoreRef>(), size_of::<*mut PDScore>());
379	}
380}
381
382
383pub mod api {
384	use core::ffi::c_char;
385	use core::ffi::c_int;
386	use core::ptr::NonNull;
387
388	use sys::ffi::AddScoreCallback;
389	use sys::ffi::PDBoardsList;
390	use sys::ffi::PDScore;
391	use sys::ffi::PDScoresList;
392	use sys::ffi::ScoresCallback;
393	use sys::ffi::BoardsListCallback;
394	use sys::ffi::PersonalBestCallback;
395	use sys::ffi::playdate_scoreboards;
396
397
398	/// Default scoreboards api end-point, ZST.
399	///
400	/// All calls approximately costs ~3 derefs.
401	#[derive(Debug, Clone, Copy, core::default::Default)]
402	pub struct Default;
403	impl Api for Default {}
404
405
406	/// Cached scoreboards api end-point.
407	///
408	/// Stores one reference, so size on stack is eq `usize`.
409	///
410	/// All calls approximately costs ~1 deref.
411	#[derive(Clone, Copy)]
412	#[cfg_attr(feature = "bindings-derive-debug", derive(Debug))]
413	pub struct Cache(&'static playdate_scoreboards);
414
415	impl core::default::Default for Cache {
416		fn default() -> Self { Self(sys::api!(scoreboards)) }
417	}
418
419	impl From<*const playdate_scoreboards> for Cache {
420		#[inline(always)]
421		fn from(ptr: *const playdate_scoreboards) -> Self { Self(unsafe { ptr.as_ref() }.expect("scoreboards")) }
422	}
423
424	impl From<&'static playdate_scoreboards> for Cache {
425		#[inline(always)]
426		fn from(r: &'static playdate_scoreboards) -> Self { Self(r) }
427	}
428
429	impl From<NonNull<playdate_scoreboards>> for Cache {
430		#[inline(always)]
431		fn from(ptr: NonNull<playdate_scoreboards>) -> Self { Self(unsafe { ptr.as_ref() }) }
432	}
433
434	impl From<&'_ NonNull<playdate_scoreboards>> for Cache {
435		#[inline(always)]
436		fn from(ptr: &NonNull<playdate_scoreboards>) -> Self { Self(unsafe { ptr.as_ref() }) }
437	}
438
439	impl Api for Cache {
440		fn add_score(
441			&self)
442			-> unsafe extern "C" fn(boardId: *const c_char, value: u32, callback: AddScoreCallback) -> c_int {
443			self.0.addScore.expect("addScore")
444		}
445
446		fn get_personal_best(
447			&self)
448			-> unsafe extern "C" fn(boardId: *const c_char, callback: PersonalBestCallback) -> c_int {
449			self.0.getPersonalBest.expect("getPersonalBest")
450		}
451
452		fn free_score(&self) -> unsafe extern "C" fn(score: *mut PDScore) { self.0.freeScore.expect("freeScore") }
453
454		fn get_scoreboards(&self) -> unsafe extern "C" fn(callback: BoardsListCallback) -> c_int {
455			self.0.getScoreboards.expect("getScoreboards")
456		}
457
458		fn free_boards_list(&self) -> unsafe extern "C" fn(boardsList: *mut PDBoardsList) {
459			self.0.freeBoardsList.expect("freeBoardsList")
460		}
461
462		fn get_scores(&self) -> unsafe extern "C" fn(board_id: *const c_char, callback: ScoresCallback) -> c_int {
463			self.0.getScores.expect("getScores")
464		}
465
466		fn free_scores_list(&self) -> unsafe extern "C" fn(scores_list: *mut PDScoresList) {
467			self.0.freeScoresList.expect("freeScoresList")
468		}
469	}
470
471
472	pub trait Api {
473		/// Returns [`sys::ffi::playdate_scoreboards::addScore`]
474		#[doc(alias = "sys::ffi::scoreboards::addScore")]
475		#[inline(always)]
476		fn add_score(
477			&self)
478			-> unsafe extern "C" fn(boardId: *const c_char, value: u32, callback: AddScoreCallback) -> c_int {
479			*sys::api!(scoreboards.addScore)
480		}
481
482		/// Returns [`sys::ffi::playdate_scoreboards::getPersonalBest`]
483		#[doc(alias = "sys::ffi::scoreboards::getPersonalBest")]
484		#[inline(always)]
485		fn get_personal_best(
486			&self)
487			-> unsafe extern "C" fn(boardId: *const c_char, callback: PersonalBestCallback) -> c_int {
488			*sys::api!(scoreboards.getPersonalBest)
489		}
490
491		/// Returns [`sys::ffi::playdate_scoreboards::freeScore`]
492		#[doc(alias = "sys::ffi::scoreboards::freeScore")]
493		#[inline(always)]
494		fn free_score(&self) -> unsafe extern "C" fn(score: *mut PDScore) { *sys::api!(scoreboards.freeScore) }
495
496		/// Returns [`sys::ffi::playdate_scoreboards::getScoreboards`]
497		#[doc(alias = "sys::ffi::scoreboards::getScoreboards")]
498		#[inline(always)]
499		fn get_scoreboards(&self) -> unsafe extern "C" fn(callback: BoardsListCallback) -> c_int {
500			*sys::api!(scoreboards.getScoreboards)
501		}
502
503		/// Returns [`sys::ffi::playdate_scoreboards::freeBoardsList`]
504		#[doc(alias = "sys::ffi::scoreboards::freeBoardsList")]
505		#[inline(always)]
506		fn free_boards_list(&self) -> unsafe extern "C" fn(boardsList: *mut PDBoardsList) {
507			*sys::api!(scoreboards.freeBoardsList)
508		}
509
510		/// Returns [`sys::ffi::playdate_scoreboards::getScores`]
511		#[doc(alias = "sys::ffi::scoreboards::getScores")]
512		#[inline(always)]
513		fn get_scores(&self) -> unsafe extern "C" fn(board_id: *const c_char, callback: ScoresCallback) -> c_int {
514			*sys::api!(scoreboards.getScores)
515		}
516
517		/// Returns [`sys::ffi::playdate_scoreboards::freeScoresList`]
518		#[doc(alias = "sys::ffi::scoreboards::freeScoresList")]
519		#[inline(always)]
520		fn free_scores_list(&self) -> unsafe extern "C" fn(scores_list: *mut PDScoresList) {
521			*sys::api!(scoreboards.freeScoresList)
522		}
523	}
524}