shuyi_playdate/sdk.rs
1//! [`Sdk`]: the calls of Panic's SDK the backend is written against, small
2//! enough that a fake stands in for them in the tests (`crate::fake`) and
3//! `crate::device` puts them on the real SDK through `shim/shim.c`.
4
5use alloc::string::String;
6use alloc::vec::Vec;
7use core::fmt;
8
9use shuyi_ui::frame::onebit::Frame;
10use shuyi_ui::perf::HeapCounts;
11use shuyi_ui::storage::StorageError;
12
13/// The file of the game's baked pack in the `.pdx`: `shuyi export playdate`
14/// places it, and `pdc` copies a file it doesn't know as it is.
15pub const PACK: &str = "game.pack";
16
17/// What the SDK tells a game between updates (`PDSystemEvent`, by number;
18/// `shim/shim.c` checks them against the header).
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum SdkEvent {
21 /// The device was locked (`kEventLock`).
22 Lock,
23 /// The device was unlocked (`kEventUnlock`).
24 Unlock,
25 /// The system menu opened (`kEventPause`).
26 Pause,
27 /// The system menu closed (`kEventResume`).
28 Resume,
29 /// The system is closing the game (`kEventTerminate`).
30 Terminate,
31 /// The battery is nearly empty (`kEventLowPower`).
32 LowPower,
33}
34
35impl SdkEvent {
36 /// The event of the SDK's number `event`, if the backend hears it.
37 pub const fn from_raw(event: i32) -> Option<Self> {
38 match event {
39 2 => Some(Self::Lock),
40 3 => Some(Self::Unlock),
41 4 => Some(Self::Pause),
42 5 => Some(Self::Resume),
43 6 => Some(Self::Terminate),
44 9 => Some(Self::LowPower),
45 _ => None,
46 }
47 }
48}
49
50/// The SDK's calls. A clone is the same SDK: the storage keeps its own, as a
51/// game's context outlives nothing the backend holds.
52pub trait Sdk: Clone + fmt::Debug + 'static {
53 /// A sound the SDK holds in memory (`AudioSample`).
54 type Sample: Copy + PartialEq + fmt::Debug;
55 /// A sample player (`SamplePlayer`).
56 type SamplePlayer: Copy + fmt::Debug;
57 /// A file player (`FilePlayer`).
58 type FilePlayer: Copy + fmt::Debug;
59
60 /// `getCurrentTimeMilliseconds`: milliseconds from a fixed start, which
61 /// stands still while the device sleeps and wraps at `u32::MAX`.
62 fn millis(&self) -> u32;
63 /// The high-resolution timer in whole microseconds from its start
64 /// (`getElapsedTime`), never going back; never reset by the backend.
65 fn clock_us() -> u64;
66 /// The debug build's heap counter, read: `None` in a release build.
67 fn heap() -> Option<HeapCounts>;
68 /// `getButtonState`: the bits held now, and those pressed since the
69 /// last call.
70 fn buttons(&self) -> (u32, u32);
71 /// `getCrankChange`: degrees turned since the last call, clockwise
72 /// positive.
73 fn crank_change(&self) -> f32;
74 /// `isCrankDocked`.
75 fn crank_docked(&self) -> bool;
76 /// `addMenuItem`: an item named `title` that gives `slot` back when
77 /// chosen; false if the SDK refused it.
78 fn add_menu_item(&self, title: &str, slot: u8) -> bool;
79 /// `display->setRefreshRate`.
80 fn set_refresh_rate(&self, fps: u16);
81 /// Runs `draw` on the frame buffer (`getFrame`).
82 fn with_frame(&self, draw: impl FnOnce(&mut Frame));
83 /// `markUpdatedRows`: rows `first` to `last` of the frame buffer changed.
84 fn mark_rows(&self, first: i32, last: i32);
85 /// Clears the screen and draws `text`, centred and wrapped, in the
86 /// system's font: black on white if `white`, else white on black.
87 fn draw_text(&self, text: &str, white: bool);
88 /// `logToConsole`: one line.
89 fn log(&self, line: &str);
90 /// Whether `path` in the game's data folder is a file.
91 fn is_file(&self, path: &str) -> bool;
92 /// The whole of file `path` in the game's data folder.
93 fn read_data(&self, path: &str) -> Result<Vec<u8>, String>;
94 /// The whole of file `path` in the game's `.pdx`.
95 fn read_bundled(&self, path: &str) -> Result<Vec<u8>, String>;
96 /// Replaces file `path` in the game's data folder with `bytes`, flushed.
97 fn write_data(&self, path: &str, bytes: &[u8]) -> Result<(), String>;
98 /// Moves file `from` to `to` in the game's data folder.
99 fn rename_data(&self, from: &str, to: &str) -> Result<(), String>;
100 /// Deletes file `path` in the game's data folder.
101 fn remove_data(&self, path: &str) -> Result<(), String>;
102 /// The names in `folder` of the game's data folder (`""`: the data
103 /// folder itself); a folder's has a `/` after it.
104 fn data_names(&self, folder: &str) -> Result<Vec<String>, String>;
105 /// Makes `folder` in the game's data folder; an error if it is there.
106 fn make_data_folder(&self, folder: &str) -> Result<(), String>;
107 /// `sample->load`: the sound at `path` in the `.pdx`, without its
108 /// extension; `None` if there is none.
109 fn sample_load(&self, path: &str) -> Option<Self::Sample>;
110 /// `sample->getLength`, in seconds.
111 fn sample_length(&self, sample: Self::Sample) -> f32;
112 /// `sample->newSampleFromData`: a sound of `samples`, 16 bits each,
113 /// interleaved, at `rate` a second, mono or `stereo`. The SDK keeps a copy
114 /// of its own. `None` if it made none (no memory, or no samples). Without
115 /// it, none.
116 fn sample_from_data(&self, samples: &[i16], rate: u32, stereo: bool) -> Option<Self::Sample> {
117 let _ = (samples, rate, stereo);
118 None
119 }
120 /// `sample->freeSample`, with its copy of the samples: a sample
121 /// [`sample_from_data`](Self::sample_from_data) made, which no sample
122 /// player plays any more. Without it, nothing.
123 fn sample_free(&self, sample: Self::Sample) {
124 let _ = sample;
125 }
126 /// `sampleplayer->newPlayer`.
127 fn sample_player_new(&self) -> Option<Self::SamplePlayer>;
128 /// Plays `sample` from its start on `player`, stopping what it played, at
129 /// `volume`, once or looped, at `rate` (1 as recorded).
130 fn sample_player_play(
131 &self,
132 player: Self::SamplePlayer,
133 sample: Self::Sample,
134 volume: f32,
135 looped: bool,
136 rate: f32,
137 );
138 /// `sampleplayer->setVolume`, the same on both sides.
139 fn sample_player_set_volume(&self, player: Self::SamplePlayer, volume: f32);
140 /// `sampleplayer->stop`.
141 fn sample_player_stop(&self, player: Self::SamplePlayer);
142 /// `sampleplayer->isPlaying`.
143 fn sample_player_is_playing(&self, player: Self::SamplePlayer) -> bool;
144 /// `sampleplayer->setPaused`.
145 fn sample_player_set_paused(&self, player: Self::SamplePlayer, paused: bool);
146 /// A new file player on the file at `path` in the `.pdx`, without its
147 /// extension; `None` if there is no such file.
148 fn file_open(&self, path: &str) -> Option<Self::FilePlayer>;
149 /// Starts `player`, or goes on from where its pause left it, once or
150 /// looped: false if it could not (`fileplayer->play`).
151 fn file_play(&self, player: Self::FilePlayer, looped: bool) -> bool;
152 /// `fileplayer->setVolume`, the same on both sides.
153 fn file_set_volume(&self, player: Self::FilePlayer, volume: f32);
154 /// `fileplayer->setRate`.
155 fn file_set_rate(&self, player: Self::FilePlayer, rate: f32);
156 /// `fileplayer->pause`.
157 fn file_pause(&self, player: Self::FilePlayer);
158 /// `fileplayer->stop`.
159 fn file_stop(&self, player: Self::FilePlayer);
160 /// `fileplayer->setOffset` to the file's start.
161 fn file_rewind(&self, player: Self::FilePlayer);
162 /// `fileplayer->isPlaying`.
163 fn file_is_playing(&self, player: Self::FilePlayer) -> bool;
164 /// `fileplayer->getLength`, in seconds.
165 fn file_length(&self, player: Self::FilePlayer) -> f32;
166 /// Stops `player` and frees it: it isn't used again.
167 fn file_close(&self, player: Self::FilePlayer);
168 /// Whether a file player open now ran out of data to play
169 /// (`fileplayer->didUnderrun`): the music stuttered. Without it, never.
170 fn file_did_underrun(&self) -> bool {
171 false
172 }
173
174 /// The bytes of the game's own stack it has ever used
175 /// ([`stack_high_water`](crate::stack_high_water)): `None` where they
176 /// aren't measured, as on the Simulator.
177 fn stack_used(&self) -> Option<usize> {
178 None
179 }
180}
181
182/// A file call's error, as storage reports it.
183pub(crate) fn backend(path: &str, message: &str) -> StorageError {
184 // check-text: not player text (for the log)
185 StorageError::Backend(alloc::format!("{path}: {message}"))
186}