Skip to main content

shuyi_platform/
basic.rs

1//! [`BasicGame`]: a game on the short path (ADR-0036), described once for
2//! its binary, its tools and its tests. It holds what the engine can't
3//! read from the game's files: who it is ([`AppInfo`]), its files,
4//! its first screen and, if it has them, its console, its own part,
5//! every screen it can show, the modules it installs and its record of
6//! its play for a crash report. From it come
7//! its data ([`BasicGame::data`]), its context ([`BasicGame::ctx`]), its
8//! [`Game`] and how a backend starts it ([`BasicGame::start`]).
9
10use crate::alloc_prelude::*;
11use alloc::{format, vec};
12use core::fmt::{self, Debug};
13
14use shuyi_assets::ContentError;
15use shuyi_assets::bundle::{Bundle, Layer};
16use shuyi_assets::keymap::ActionSet;
17use shuyi_core::Made;
18use shuyi_ui::basic::{BasicData, Own};
19use shuyi_ui::frame::ConsoleSpec;
20use shuyi_ui::module::{Module, ModuleError};
21use shuyi_ui::{Ctx, Game, GameData, GameHooks, Screen};
22
23use crate::app::{Card, CrashRecord, Start};
24use crate::app_info::AppInfo;
25use crate::platform::Look;
26
27/// The screen a game on the short path starts on, made on its context.
28pub type First<A, T> = fn(&mut Ctx<BasicData<A, T>>) -> Box<dyn Screen<BasicData<A, T>>>;
29
30/// Every screen a game on the short path can show, overlays on their own,
31/// for the check that its controls fit each target.
32pub type Screens<A, T> = fn(&mut Ctx<BasicData<A, T>>) -> Vec<Box<dyn Screen<BasicData<A, T>>>>;
33
34/// The modules a game on the short path installs, made each time its
35/// [`Game`] is: `shuyi::enabled_modules`, for those its features turn on.
36pub type Modules<A, T> = fn() -> Vec<Box<dyn Module<BasicData<A, T>>>>;
37
38/// The console of a game that doesn't say: 48 × 24 cells of 8 × 16 pixels.
39pub const BASIC_CONSOLE: ConsoleSpec = ConsoleSpec::text(48, 24);
40
41/// A game on the short path: its data is a [`BasicData`] of actions `A`
42/// and its own part `T`, read from one layer of files.
43pub struct BasicGame<A: ActionSet, T: Debug + 'static = ()> {
44    app: AppInfo,
45    files: Layer,
46    /// A layer laid over `files`, if it has one.
47    over: Option<Layer>,
48    own: Own<T>,
49    first: First<A, T>,
50    console: ConsoleSpec,
51    screens: Option<Screens<A, T>>,
52    modules: Option<Modules<A, T>>,
53    crash_record: Option<CrashRecord<BasicData<A, T>>>,
54}
55
56/// What a game on the short path is painted with: its font and pictures.
57fn look<A: ActionSet, T: Debug + 'static>(data: &BasicData<A, T>) -> Look<'_> {
58    Look::new(&data.font, &data.images)
59}
60
61impl<A: ActionSet> BasicGame<A> {
62    /// The game `app` (its name, and the storage its saves are kept
63    /// under), its files in `files`, starting on the screen `first` makes.
64    pub fn new(app: AppInfo, files: Layer, first: First<A, ()>) -> Self {
65        // No part of its own: there is none to read.
66        Self::with_own(app, files, |_| Ok(()), first)
67    }
68}
69
70impl<A: ActionSet, T: Debug + 'static> BasicGame<A, T> {
71    /// As [`BasicGame::new`], for a game with a part of its own, which
72    /// `own` reads from its files ([`BasicData::own`]).
73    pub fn with_own(app: AppInfo, files: Layer, own: Own<T>, first: First<A, T>) -> Self {
74        Self {
75            app,
76            files,
77            over: None,
78            own,
79            first,
80            console: BASIC_CONSOLE,
81            screens: None,
82            modules: None,
83            crash_record: None,
84        }
85    }
86
87    /// The same game on `console` ([`BASIC_CONSOLE`] until said).
88    #[must_use]
89    pub fn console(mut self, console: ConsoleSpec) -> Self {
90        self.console = console;
91        self
92    }
93
94    /// The same game, with `screens` every screen it can show (its first
95    /// alone until said).
96    #[must_use]
97    pub fn screens(mut self, screens: Screens<A, T>) -> Self {
98        self.screens = Some(screens);
99        self
100    }
101
102    /// The same game, with the modules `modules` makes installed in its
103    /// [`Game`] (none until said).
104    #[must_use]
105    pub fn modules(mut self, modules: Modules<A, T>) -> Self {
106        self.modules = Some(modules);
107        self
108    }
109
110    /// The same game, with `hook` its record of its play in a crash
111    /// report ([`Start::crash_record`]; none until said).
112    #[must_use]
113    pub fn crash_record(mut self, hook: CrashRecord<BasicData<A, T>>) -> Self {
114        self.crash_record = Some(hook);
115        self
116    }
117
118    /// Who it is.
119    pub fn app(&self) -> AppInfo {
120        self.app
121    }
122
123    /// Its name: the window's title and the loading card's line.
124    pub fn name(&self) -> &'static str {
125        self.app.name
126    }
127
128    /// The name its saves are kept under.
129    pub fn storage_name(&self) -> &'static str {
130        self.app.storage
131    }
132
133    /// The console it starts on.
134    pub fn console_spec(&self) -> ConsoleSpec {
135        self.console
136    }
137
138    /// The same game with `layer` laid over its files: a folder on disk
139    /// (`shuyi_app::watched_layer`, `cargo shuyi run --watch`), or a mod.
140    #[must_use]
141    pub fn over(mut self, layer: Layer) -> Self {
142        self.over = Some(layer);
143        self
144    }
145
146    /// Its files, as a bundle of their one layer, under the one laid
147    /// [`over`](Self::over) them if it has one.
148    pub fn bundle(&self) -> Bundle {
149        Bundle::new(self.over.into_iter().chain([self.files]).collect())
150    }
151
152    /// Its data from its files, or every problem of every file.
153    pub fn data(&self) -> Result<BasicData<A, T>, Vec<ContentError>> {
154        BasicData::load(self.bundle(), self.console, self.own)
155    }
156
157    /// Its context, in its keymap file's first layout; or every problem
158    /// of its files, one per line.
159    pub fn ctx(&self) -> Result<Ctx<BasicData<A, T>>, String> {
160        let data = self.data().map_err(|errors| {
161            let lines: Vec<String> = errors.iter().map(ToString::to_string).collect();
162            lines.join("\n")
163        })?;
164        let mut ctx = Ctx::new(data)
165            .map_err(|missing| format!("the palette lacks colours: {}", missing.join(", ")))?;
166        if let Some(layout) = ctx.game.keymap_def().first_layout() {
167            ctx.use_layout(layout);
168        }
169        Ok(ctx)
170    }
171
172    /// The game on `ctx`, at its first screen, with its
173    /// [`modules`](Self::modules) installed; or why one could not be.
174    pub fn game(
175        &self,
176        mut ctx: Ctx<BasicData<A, T>>,
177    ) -> Result<Game<BasicData<A, T>>, ModuleError> {
178        let first = (self.first)(&mut ctx);
179        let game = Game::new(ctx, first, GameHooks::default());
180        match self.modules {
181            Some(modules) => game.with_modules(modules()),
182            None => Ok(game),
183        }
184    }
185
186    /// Every screen it can show ([`screens`](Self::screens)), made on
187    /// `ctx`.
188    pub fn every_screen(
189        &self,
190        ctx: &mut Ctx<BasicData<A, T>>,
191    ) -> Vec<Box<dyn Screen<BasicData<A, T>>>> {
192        match self.screens {
193            Some(screens) => screens(ctx),
194            None => vec![(self.first)(ctx)],
195        }
196    }
197
198    /// What made this build, for its records: its id and version as the
199    /// game's word, and the fingerprint of its files.
200    pub fn made(&self) -> Made {
201        let build = format!("{} {}", self.app.id, self.app.version);
202        Made::new(build, self.bundle().fingerprint())
203    }
204
205    /// How a backend starts it: a card with its name, then its context on
206    /// the platform and its first screen. A file that doesn't load, or a
207    /// module that can't be installed, is the load's error, which the
208    /// platform shows in place of the game. A session record of it carries
209    /// its [`made`](Self::made), and each frame's hash the state hash of its
210    /// screens ([`Screen::state_hash`]). A crash report carries its
211    /// [`crash_record`](Self::crash_record), if it has one.
212    pub fn start(self) -> Start<BasicData<A, T>> {
213        let made = self.made();
214        let crash_record = self.crash_record;
215        let start = Start::new(Card::text(self.app.name), look::<A, T>, move |setup| {
216            let ctx = self.ctx()?;
217            self.game(setup.ctx(ctx)).map_err(|e| e.to_string())
218        })
219        .made(made)
220        .state_hash(Game::state_hash);
221        match crash_record {
222            Some(hook) => start.crash_record(hook),
223            None => start,
224        }
225    }
226
227    /// The game in the test Harness, at its first screen.
228    ///
229    /// # Panics
230    ///
231    /// If its files don't load, or a module can't be installed: the panic
232    /// gives every problem.
233    #[cfg(any(test, feature = "harness"))]
234    pub fn harness(&self) -> crate::harness::Harness<BasicData<A, T>> {
235        let ctx = self
236            .ctx()
237            .unwrap_or_else(|errors| panic!("the game's files:\n{errors}"));
238        let game = self
239            .game(ctx)
240            .unwrap_or_else(|e| panic!("the game's modules: {e}"));
241        crate::harness::Harness::new(game)
242    }
243}
244
245// By hand: a derive would ask `A` and `T` to be `Clone` and `Copy`, and
246// every field already is.
247impl<A: ActionSet, T: Debug + 'static> Clone for BasicGame<A, T> {
248    fn clone(&self) -> Self {
249        *self
250    }
251}
252
253impl<A: ActionSet, T: Debug + 'static> Copy for BasicGame<A, T> {}
254
255impl<A: ActionSet, T: Debug + 'static> Debug for BasicGame<A, T> {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        f.debug_struct("BasicGame")
258            .field("app", &self.app)
259            .field("files", &self.files.name)
260            .field("console", &self.console)
261            .finish_non_exhaustive()
262    }
263}
264
265#[cfg(test)]
266mod tests;