Skip to main content

shuyi_playdate/
backend.rs

1//! [`Playdate`]: the [`Platform`] on the SDK ([`Sdk`]).
2
3use alloc::boxed::Box;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use shuyi_assets::keymap::ActionSet;
8use shuyi_assets::target::TargetProfile;
9use shuyi_platform::audio::player::PlayerDevice;
10use shuyi_platform::{
11    Audio, DisplayInfo, Frame, Millis, Platform, Storage, SystemEvent, TextInput,
12};
13use shuyi_ui::frame_cap::{FrameCap, Pacing};
14use shuyi_ui::input::pad::PadKind;
15use shuyi_ui::input::state::Device;
16use shuyi_ui::perf::{MemoryReading, PerfClock};
17use shuyi_ui::{Game, GameData, PlatformFacts, RawInputEvent};
18
19use crate::audio::{self, Speaker};
20use crate::display::{Screen, Showing};
21use crate::input::{PadInput, menu_button, menu_press};
22use crate::report::Measured;
23use crate::sdk::{PACK, Sdk, SdkEvent};
24use crate::storage::SdkStorage;
25
26/// The target's profile, embedded: the numbers here are its, never typed
27/// twice.
28const PROFILE: &str = include_str!("../../../targets/playdate.ron");
29
30/// The text key of a system menu item's title: this, then the name of the
31/// action on it (`system_menu.Map`). Without it, the action's name.
32pub const MENU_TITLE: &str = "system_menu.";
33
34/// The profile of the target, read once.
35fn profile() -> TargetProfile {
36    TargetProfile::from_source("targets/playdate.ron", "playdate", PROFILE)
37        .unwrap_or_else(|errors| panic!("targets/playdate.ron: {errors:?}"))
38}
39
40/// The game's baked pack in the `.pdx` of `sdk`, read whole into the heap and
41/// kept for as long as the game runs.
42pub fn read_pack(sdk: &impl Sdk) -> Result<&'static [u8], String> {
43    let bytes = sdk.read_bundled(PACK)?;
44    Ok(Vec::leak(bytes))
45}
46
47/// The Playdate's [`Platform`].
48#[derive(Debug)]
49pub struct Playdate<S: Sdk> {
50    sdk: S,
51    facts: PlatformFacts,
52    /// The rate the update callback runs at: the profile's.
53    rate: u16,
54    /// The most items the system menu holds: the profile's.
55    menu_slots: u8,
56    pad: PadInput,
57    /// Whether the crank was docked when the game was last told.
58    docked: Option<bool>,
59    /// The slots of the items chosen since the last poll.
60    chosen: Vec<u8>,
61    screen: Screen,
62    /// The sound device: its player comes with the game's data.
63    audio: PlayerDevice<Speaker<S>>,
64    /// The system's events since the last call.
65    events: Vec<SystemEvent>,
66    /// How many of the system's holds are on: the lock and the menu may
67    /// overlap, and the game runs again when both are off.
68    holds: u8,
69    /// The frame cap's serial on the last frame presented.
70    serial: u32,
71}
72
73impl<S: Sdk> Playdate<S> {
74    /// The backend on `sdk`: it sets the display's rate from the profile.
75    pub fn new(sdk: S) -> Self {
76        let profile = profile();
77        let rate = profile.frame_rate.target;
78        sdk.set_refresh_rate(rate);
79        let audio = PlayerDevice::new(Speaker::new(sdk.clone()));
80        Self {
81            sdk,
82            facts: PlatformFacts::from_profile(&profile)
83                .with_device(Device::Pad(PadKind::Playdate)),
84            rate,
85            menu_slots: profile.input.system_menu_slots,
86            pad: PadInput::new(shuyi_assets::keymap::CrankDef::default()),
87            docked: None,
88            chosen: Vec::new(),
89            screen: Screen::default(),
90            audio,
91            events: Vec::new(),
92            holds: 0,
93            serial: 0,
94        }
95    }
96
97    /// The SDK.
98    pub fn sdk(&self) -> &S {
99        &self.sdk
100    }
101
102    /// The game has loaded: loads its sounds, reads the crank's notch from its
103    /// keymap, adds the system menu's items its optional actions are bound to,
104    /// and lets it hear of the crank from the next frame.
105    pub fn start<G: GameData>(&mut self, game: &Game<G>) {
106        let data = &game.ctx().game;
107        let manifest = data.audio_manifest().clone();
108        let seed = u64::from(self.sdk.millis());
109        let player = audio::player(self.audio.backend_mut(), manifest, seed);
110        self.audio.set_player(player);
111        self.pad.set_crank(data.keymap_def().crank);
112        self.docked = None;
113        for slot in 0..self.menu_slots {
114            let bound =
115                menu_button(slot).and_then(|button| game.input().keymap().pad_action(button));
116            let Some(action) = bound else {
117                continue;
118            };
119            let name = action.name();
120            let key = alloc::format!("{MENU_TITLE}{name}");
121            let title = if data.lang().has(&key) {
122                game.ctx().text(&key)
123            } else {
124                name
125            };
126            if !self.sdk.add_menu_item(title, slot) {
127                // check-text: not player text (for the log)
128                self.sdk.log("the system menu refused an item");
129            }
130        }
131    }
132
133    /// The player chose the system menu's item in `slot`: a press of its
134    /// button at the next poll, so it goes where input goes.
135    pub fn menu_chosen(&mut self, slot: u8) {
136        if slot < self.menu_slots {
137            self.chosen.push(slot);
138        }
139    }
140
141    /// The SDK's event, as the [`SystemEvent`]s the next frame reads. True
142    /// when it should be heard now: no update comes until the system gives
143    /// the game back, so what it saves must be saved before.
144    pub fn event(&mut self, event: SdkEvent) -> bool {
145        match event {
146            SdkEvent::Lock | SdkEvent::Pause => {
147                self.holds = self.holds.saturating_add(1);
148                if self.holds > 1 {
149                    return false;
150                }
151                self.events.push(SystemEvent::Suspend);
152                true
153            }
154            SdkEvent::Unlock | SdkEvent::Resume => {
155                self.holds = self.holds.saturating_sub(1);
156                if self.holds == 0 {
157                    self.events.push(SystemEvent::Resume);
158                    self.screen.invalidate();
159                }
160                false
161            }
162            SdkEvent::Terminate => {
163                self.events.push(SystemEvent::QuitRequested);
164                true
165            }
166            SdkEvent::LowPower => {
167                self.events.push(SystemEvent::LowBattery);
168                true
169            }
170        }
171    }
172
173    /// Whether anything reached the screen since the last call: the update
174    /// callback returns it.
175    pub fn take_changed(&mut self) -> bool {
176        self.screen.take_changed()
177    }
178
179    /// What the screen last showed.
180    pub(crate) fn showing(&self) -> Showing {
181        self.screen.showing()
182    }
183
184    /// Times each draw from now on, and each of its phases if `phases`.
185    pub(crate) fn measure(&mut self, phases: bool) {
186        self.screen.measure(phases);
187    }
188
189    /// The draws' times since the last take.
190    pub(crate) fn take_measured(&mut self) -> Measured {
191        self.screen.take_measured()
192    }
193
194    /// The music's cue heard at `now`, if any.
195    pub(crate) fn music(&self, now: Millis) -> Option<&str> {
196        self.audio.music_playing(now).map(|(cue, _)| cue)
197    }
198
199    /// Whether a file player open now ran out of data.
200    pub(crate) fn underran(&self) -> bool {
201        self.sdk.file_did_underrun()
202    }
203
204    /// What it applied, for `App::set_pacing`: the device runs at the
205    /// profile's rate whatever a game asks, so a request is confirmed.
206    pub fn pacing(&self, fps: u16) -> Pacing {
207        let mut pacing = Pacing::new(FrameCap::Fps(self.rate), self.serial);
208        pacing.fps = fps;
209        pacing
210    }
211}
212
213impl<S: Sdk> Platform for Playdate<S> {
214    fn display(&self) -> DisplayInfo {
215        DisplayInfo::new(400, 240).one_bit()
216    }
217
218    fn present(&mut self, frame: &Frame<'_>) {
219        self.serial = frame.frame_cap_serial;
220        self.screen.present(&self.sdk, frame);
221    }
222
223    fn poll_input(&mut self, out: &mut Vec<RawInputEvent>) {
224        let (current, pushed) = self.sdk.buttons();
225        let turned = self.sdk.crank_change();
226        let docked = self.sdk.crank_docked();
227        out.extend(self.pad.poll(current, pushed, turned, docked));
228        for slot in self.chosen.drain(..) {
229            out.extend(menu_press(slot).into_iter().flatten());
230        }
231    }
232
233    fn system_events(&mut self, out: &mut Vec<SystemEvent>) {
234        out.append(&mut self.events);
235        let docked = self.sdk.crank_docked();
236        if self.docked != Some(docked) {
237            self.docked = Some(docked);
238            out.push(if docked {
239                SystemEvent::CrankDocked
240            } else {
241                SystemEvent::CrankUndocked
242            });
243        }
244    }
245
246    fn storage(&mut self) -> Box<dyn Storage> {
247        Box::new(SdkStorage::new(self.sdk.clone()))
248    }
249
250    fn audio(&mut self) -> &mut dyn Audio {
251        &mut self.audio
252    }
253
254    fn text_input(&mut self) -> Option<&mut dyn TextInput> {
255        None
256    }
257
258    fn facts(&self) -> PlatformFacts {
259        self.facts.clone()
260    }
261
262    fn clock(&self) -> Option<PerfClock> {
263        cfg!(debug_assertions).then_some(PerfClock::Real(S::clock_us))
264    }
265
266    fn memory(&mut self) -> Option<MemoryReading> {
267        S::heap().map(MemoryReading::new)
268    }
269}