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}