Skip to main content

shuyi_platform/
app_info.rs

1//! [`AppInfo`]: who a game is, the same on every target: the name players
2//! see, a reverse-DNS id, its version and build, its publisher, the name
3//! its saves are kept under (ADR-0059), and its window's icon
4//! ([`WindowIcon`], ADR-0061).
5//!
6//! A game writes it once, in its `shuyi.ron`'s `app` part, and
7//! `shuyi app-info` writes it as a Rust constant (`pub const APP:
8//! AppInfo`) its binary compiles. A game that keeps its own builds it
9//! here in a `const` item:
10//!
11//! ```
12//! use shuyi_platform::AppInfo;
13//!
14//! const APP: AppInfo = AppInfo::new("Moon Garden", "com.example.moon-garden", "0.1.0")
15//!     .with_storage("moon_garden")
16//!     .with_publisher("Moon Garden's makers");
17//!
18//! assert_eq!((APP.storage, APP.build), ("moon_garden", 1));
19//! ```
20
21/// Who a game is. Build it with [`AppInfo::new`] and the `with_` setters:
22/// a struct literal doesn't compile outside this crate, so a new field
23/// never breaks a game (ADR-0039).
24///
25/// ```compile_fail
26/// let app = shuyi_platform::AppInfo {
27///     name: "A game",
28///     id: "com.example.a-game",
29///     version: "0.1.0",
30///     publisher: "",
31///     storage: "com.example.a-game",
32///     build: 1,
33///     window_icon: None,
34/// };
35/// ```
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37#[non_exhaustive]
38pub struct AppInfo {
39    /// The name players see: the window's title and the loading card's
40    /// line.
41    pub name: &'static str,
42    /// Its reverse-DNS id, unique to the game (`com.example.moon-garden`).
43    pub id: &'static str,
44    /// Its version, `MAJOR.MINOR.PATCH` (semver).
45    pub version: &'static str,
46    /// Who publishes it; empty if no one is named.
47    pub publisher: &'static str,
48    /// The name its saves are kept under: the folder in the OS's data
49    /// folder on native, the prefix of every `localStorage` key on the
50    /// web. Never change it after a release, or every player's saves and
51    /// settings are lost.
52    pub storage: &'static str,
53    /// A number that only grows from one build handed out to the next,
54    /// where a target asks for one beside the version.
55    pub build: u32,
56    /// The window's icon, where a backend shows one; `None` for none.
57    /// `shuyi app-info` makes it from `shuyi.ron`'s `app.icon`.
58    pub window_icon: Option<WindowIcon>,
59}
60
61/// A window's icon: PNG files of 16, 32 and 64 pixels square, which
62/// `shuyi app-info` downscales from the game's one icon (ADR-0061). Build
63/// it with [`WindowIcon::new`].
64///
65/// ```compile_fail
66/// let icon = shuyi_platform::WindowIcon {
67///     small: b"",
68///     medium: b"",
69///     big: b"",
70/// };
71/// ```
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73#[non_exhaustive]
74pub struct WindowIcon {
75    /// The 16 × 16 picture.
76    pub small: &'static [u8],
77    /// The 32 × 32 picture.
78    pub medium: &'static [u8],
79    /// The 64 × 64 picture.
80    pub big: &'static [u8],
81}
82
83impl WindowIcon {
84    /// An icon of the three pictures, 16, 32 and 64 pixels square.
85    #[must_use]
86    pub const fn new(small: &'static [u8], medium: &'static [u8], big: &'static [u8]) -> Self {
87        Self { small, medium, big }
88    }
89}
90
91impl AppInfo {
92    /// The game called `name`, with the id `id`, at version `version`: its
93    /// saves kept under its id, at build 1, with no publisher named.
94    #[must_use]
95    pub const fn new(name: &'static str, id: &'static str, version: &'static str) -> Self {
96        Self {
97            name,
98            id,
99            version,
100            publisher: "",
101            storage: id,
102            build: 1,
103            window_icon: None,
104        }
105    }
106
107    /// The game with its saves kept under `storage`
108    /// ([`storage`](Self::storage)).
109    #[must_use]
110    pub const fn with_storage(self, storage: &'static str) -> Self {
111        Self { storage, ..self }
112    }
113
114    /// The game at build `build` ([`build`](Self::build)).
115    #[must_use]
116    pub const fn with_build(self, build: u32) -> Self {
117        Self { build, ..self }
118    }
119
120    /// The game published by `publisher`.
121    #[must_use]
122    pub const fn with_publisher(self, publisher: &'static str) -> Self {
123        Self { publisher, ..self }
124    }
125
126    /// The game with `icon` as its window's icon (`None` for none,
127    /// [`window_icon`](Self::window_icon)).
128    #[must_use]
129    pub const fn with_window_icon(self, icon: Option<WindowIcon>) -> Self {
130        Self {
131            window_icon: icon,
132            ..self
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    /// Built in a `const` item, as a game's generated file builds it.
142    const SET: AppInfo = AppInfo::new("A game", "com.example.a-game", "1.2.3")
143        .with_storage("a-game")
144        .with_build(7)
145        .with_publisher("Someone");
146
147    #[test]
148    fn a_new_app_keeps_its_saves_under_its_id_at_build_one() {
149        let app = AppInfo::new("A game", "com.example.a-game", "0.1.0");
150        assert_eq!(
151            (app.name, app.id, app.version),
152            ("A game", "com.example.a-game", "0.1.0")
153        );
154        assert_eq!(
155            (app.storage, app.build, app.publisher),
156            ("com.example.a-game", 1, "")
157        );
158        assert_eq!(app.window_icon, None);
159    }
160
161    /// An icon set in a `const` item, as the generated file sets it.
162    const ICONED: AppInfo = AppInfo::new("A game", "com.example.a-game", "1.2.3")
163        .with_window_icon(Some(WindowIcon::new(b"small", b"medium", b"big")));
164
165    #[test]
166    fn with_window_icon_sets_the_icon_alone() {
167        let icon = ICONED.window_icon.unwrap();
168        assert_eq!(
169            (icon.small, icon.medium, icon.big),
170            (&b"small"[..], &b"medium"[..], &b"big"[..])
171        );
172        assert_eq!(
173            ICONED.with_window_icon(None),
174            AppInfo::new("A game", "com.example.a-game", "1.2.3")
175        );
176        assert_eq!(SET.with_window_icon(Some(icon)).window_icon, Some(icon));
177        assert_eq!(SET.with_window_icon(Some(icon)).storage, "a-game");
178    }
179
180    #[test]
181    fn each_setter_changes_its_field_alone() {
182        assert_eq!(
183            (SET.storage, SET.build, SET.publisher),
184            ("a-game", 7, "Someone")
185        );
186        assert_eq!(
187            (SET.name, SET.id, SET.version),
188            ("A game", "com.example.a-game", "1.2.3")
189        );
190        let plain = AppInfo::new("A game", "com.example.a-game", "1.2.3");
191        assert_eq!(
192            plain
193                .with_storage("a-game")
194                .with_build(7)
195                .with_publisher("Someone"),
196            SET
197        );
198    }
199}