Skip to main content

shuyi_playdate/
lib.rs

1//! Shuyi Engine's Playdate backend (ADR-0008 ยง2, ADR-0062): the
2//! [`Platform`](shuyi_platform::Platform) on Panic's SDK.
3//!
4//! A game's `playdate` feature names this crate, and its library says how
5//! it starts with [`main!`]; `shuyi export playdate` builds that library as
6//! a static library with `--cfg shuyi_playdate_sdk`, links it with
7//! `shim/shim.c` and the SDK's `setup.c`, and packs the `.pdx`. The rest of
8//! the time the crate is the plain part, so the workspace's host builds and
9//! tests never need the SDK:
10//!
11//! - [`sdk`]: the small trait of SDK calls the rest is written against, which
12//!   the tests' fake and the device's real calls implement;
13//! - [`backend`]: [`Playdate`], the [`Platform`](shuyi_platform::Platform)
14//!   itself;
15//! - [`driver`]: [`Driver`], the update callback's body around
16//!   [`App::frame`](shuyi_platform::App::frame);
17//! - [`input`]: the buttons and the crank; [`display`]: the frame buffer and
18//!   its changed rows; [`storage`]: the game's saves on the SDK's files;
19//! - [`audio`]: the shared player's backend, on the SDK's sample and file
20//!   players;
21//! - [`stack`]: the arithmetic of the game's own stack, which every call from
22//!   the SDK into the game runs on;
23//! - [`report`]: the run report a game may turn on, of its start and its
24//!   speed, in the console.
25#![no_std]
26
27extern crate alloc;
28
29#[cfg(test)]
30extern crate std;
31
32pub mod audio;
33pub mod backend;
34pub mod display;
35pub mod driver;
36pub mod heap;
37pub mod input;
38pub mod report;
39pub mod sdk;
40pub mod stack;
41pub mod storage;
42pub mod text;
43
44#[cfg(shuyi_playdate_sdk)]
45#[doc(hidden)]
46pub mod device;
47
48#[cfg(test)]
49mod fake;
50
51pub use backend::Playdate;
52pub use driver::Driver;
53pub use report::RunReport;
54
55#[cfg(shuyi_playdate_sdk)]
56pub use device::{pack, stack_high_water};
57
58/// The game's baked pack: off the device there is none.
59#[cfg(not(shuyi_playdate_sdk))]
60pub fn pack() -> Result<&'static [u8], alloc::string::String> {
61    use alloc::borrow::ToOwned;
62    // check-text: not player text (a build problem for the log)
63    Err("the pack is on the device alone".to_owned())
64}
65
66/// The bytes of the game's stack it has ever used: on the device alone, since
67/// the Simulator runs on the computer's stack.
68#[cfg(not(shuyi_playdate_sdk))]
69pub fn stack_high_water() -> Option<usize> {
70    None
71}
72
73/// The game's entry points, for the crate `shuyi export playdate` builds:
74/// `main!(start)`, with `start` a `fn() -> Start<G>`, or `main!(start,
75/// stack_bytes = 128 * 1024)` for a game's own stack of another size than
76/// [`stack::DEFAULT_BYTES`]. `report = RunReport::new()` after either turns
77/// on the run report ([`report`]). Off the device it only checks they exist.
78#[cfg(shuyi_playdate_sdk)]
79#[macro_export]
80macro_rules! main {
81    ($start:path) => {
82        $crate::main!(@entry $start, $crate::stack::DEFAULT_BYTES, None);
83    };
84    ($start:path, report = $report:expr) => {
85        $crate::main!(@entry $start, $crate::stack::DEFAULT_BYTES, Some($report));
86    };
87    ($start:path, stack_bytes = $bytes:expr) => {
88        $crate::main!(@entry $start, $bytes, None);
89    };
90    ($start:path, stack_bytes = $bytes:expr, report = $report:expr) => {
91        $crate::main!(@entry $start, $bytes, Some($report));
92    };
93    (@entry $start:path, $bytes:expr, $report:expr) => {
94        /// The SDK's first event.
95        #[unsafe(no_mangle)]
96        pub extern "C" fn shuyi_playdate_init() {
97            let report: Option<$crate::RunReport> = $report;
98            $crate::device::init($start, $bytes, report);
99        }
100
101        /// The SDK's update callback; 1 when the screen changed.
102        #[unsafe(no_mangle)]
103        pub extern "C" fn shuyi_playdate_update() -> core::ffi::c_int {
104            $crate::device::update()
105        }
106
107        /// The SDK's other events.
108        #[unsafe(no_mangle)]
109        pub extern "C" fn shuyi_playdate_event(event: core::ffi::c_int) {
110            $crate::device::event(event);
111        }
112
113        /// The player chose the game's system menu item in `slot`.
114        #[unsafe(no_mangle)]
115        pub extern "C" fn shuyi_playdate_menu_chosen(slot: core::ffi::c_int) {
116            $crate::device::menu_chosen(slot);
117        }
118    };
119}
120
121/// The game's entry points, for the crate `shuyi export playdate` builds:
122/// `main!(start)`, with `start` a `fn() -> Start<G>`, or `main!(start,
123/// stack_bytes = 128 * 1024)` for a game's own stack of another size than
124/// [`stack::DEFAULT_BYTES`]. `report = RunReport::new()` after either turns
125/// on the run report ([`report`]). Off the device it only checks they exist.
126#[cfg(not(shuyi_playdate_sdk))]
127#[macro_export]
128macro_rules! main {
129    ($start:path) => {
130        const _: () = {
131            let _ = $start;
132        };
133    };
134    ($start:path, report = $report:expr) => {
135        const _: () = {
136            let _ = $start;
137        };
138        const _: fn() -> $crate::RunReport = || $report;
139    };
140    ($start:path, stack_bytes = $bytes:expr) => {
141        const _: () = {
142            let _ = $start;
143            let _: usize = $bytes;
144        };
145    };
146    ($start:path, stack_bytes = $bytes:expr, report = $report:expr) => {
147        const _: () = {
148            let _ = $start;
149            let _: usize = $bytes;
150        };
151        const _: fn() -> $crate::RunReport = || $report;
152    };
153}
154
155#[cfg(test)]
156mod tests {
157    use alloc::borrow::ToOwned;
158    use alloc::string::String;
159    use alloc::vec::Vec;
160
161    /// The lines of a TOML table in `text`, without comments.
162    fn table(text: &str, name: &str) -> Vec<String> {
163        text.lines()
164            .skip_while(|l| l.trim() != name)
165            .skip(1)
166            .take_while(|l| !l.starts_with('['))
167            .map(|l| l.split('#').next().unwrap_or("").trim().to_owned())
168            .filter(|l| !l.is_empty())
169            .collect()
170    }
171
172    #[test]
173    fn off_the_device_there_is_no_pack() {
174        assert!(crate::pack().is_err());
175    }
176
177    #[test]
178    fn off_the_device_no_stack_is_measured() {
179        assert_eq!(crate::stack_high_water(), None);
180    }
181
182    /// `[lints]` is copied from the workspace's because one lint differs
183    /// (`unsafe_code`): the copy must not drift.
184    #[test]
185    fn the_lints_are_the_workspace_lints_except_unsafe_code() {
186        let workspace = include_str!("../../../Cargo.toml");
187        let manifest = include_str!("../Cargo.toml");
188        let clippy = table(workspace, "[workspace.lints.clippy]");
189        assert!(clippy.len() > 5, "{clippy:?}");
190        let rust: Vec<String> = table(workspace, "[workspace.lints.rust]")
191            .iter()
192            .map(|l| l.replace("unsafe_code = \"forbid\"", "unsafe_code = \"deny\""))
193            .collect();
194        assert_eq!(table(manifest, "[lints.clippy]"), clippy);
195        assert_eq!(table(manifest, "[lints.rust]"), rust);
196    }
197}