Skip to main content

shuyi_ui/
module.rs

1//! [`Module`]: a piece from outside the default engine that joins a
2//! running [`Game`](crate::Game) (ADR-0040).
3//!
4//! A module is a library first: the game's screens call its types. It
5//! installs a `Module` only for what must happen on every frame whatever
6//! screen is on top, or be reached by screens it didn't make. Everything
7//! it does is declared in [`Module::install`]: the [`Hook`]s the game
8//! calls, its state on the [`Ctx`] (found by type,
9//! [`Ctx::module_state`]) and its entries on the debug menu
10//! ([`Install::debug_entry`]). The game lists each module with what it
11//! declared ([`Game::modules`](crate::Game::modules)).
12//!
13//! A module hears of files changed on disk through [`Module::reloaded`].
14//! A hook changes the module's own state and the context's UI side (a
15//! sound), never the game's data or a stage's state, which change only
16//! through commands or inputs per step. A module takes no actions: one
17//! that needs input has a screen, which declares them.
18
19use crate::prelude::*;
20use alloc::collections::BTreeMap;
21use core::any::{Any, TypeId, type_name};
22use core::fmt;
23
24use shuyi_assets::hot::Reload;
25
26use crate::ctx::{Ctx, GameData};
27use crate::debug::DebugEntry;
28use crate::frame::glyph_buffer::GlyphBuffer;
29
30/// A piece that joins a running game: the game installs it with
31/// [`Game::with_module`](crate::Game::with_module), and calls only the
32/// hooks its [`install`](Self::install) declares, in install order.
33pub trait Module<G: GameData>: 'static {
34    /// Its name: `snake_case` ASCII, unique in a game.
35    fn name(&self) -> &'static str;
36
37    /// Declares what it does: its hooks and its state on the context. By
38    /// default nothing.
39    fn install(&mut self, _install: &mut Install<'_, G>) {}
40
41    /// Runs each frame `dt` seconds long, after the input and before the
42    /// top screen's update ([`Hook::BeforeUpdate`]).
43    fn before_update(&mut self, _ctx: &mut Ctx<G>, _dt: f32) {}
44
45    /// Draws over the screens, after them ([`Hook::DrawOver`]).
46    fn draw_over(&self, _ctx: &Ctx<G>, _buf: &mut GlyphBuffer) {}
47
48    /// Runs when files changed on disk, before the frame's update, with
49    /// their paths in the bundle ([`Hook::Reloaded`]): in a build with debug
50    /// tools, where the platform watches them. The module refreshes the
51    /// [`Hot`](shuyi_assets::hot) values it keeps as its own state, and says
52    /// how each went ([`Ctx::note_reload`]).
53    fn reloaded(&mut self, _ctx: &mut Ctx<G>, _changed: &[String]) {}
54}
55
56/// A place in the frame loop where the game calls a module.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58#[non_exhaustive]
59pub enum Hook {
60    /// [`Module::before_update`].
61    BeforeUpdate,
62    /// [`Module::draw_over`].
63    DrawOver,
64    /// [`Module::reloaded`].
65    Reloaded,
66}
67
68impl Hook {
69    /// Its name: the method's.
70    pub const fn name(self) -> &'static str {
71        match self {
72            Self::BeforeUpdate => "before_update",
73            Self::DrawOver => "draw_over",
74            Self::Reloaded => "reloaded",
75        }
76    }
77}
78
79/// The reloads that failed and haven't reloaded since ([`Ctx::note_reload`]),
80/// by what failed, and their errors as the frame shows them.
81#[derive(Debug, Default)]
82pub(crate) struct ReloadErrors {
83    by_id: BTreeMap<String, Vec<String>>,
84    lines: Vec<String>,
85}
86
87impl ReloadErrors {
88    /// Takes how the asset `id` reloaded.
89    pub(crate) fn note(&mut self, id: &str, reload: &Reload) {
90        match reload {
91            Reload::Failed(errors) => {
92                let errors = errors.iter().map(ToString::to_string).collect();
93                self.by_id.insert(id.to_owned(), errors);
94            }
95            Reload::Reloaded => {
96                self.by_id.remove(id);
97            }
98            _ => return,
99        }
100        self.lines = self.by_id.values().flatten().cloned().collect();
101    }
102
103    /// Every error, one a line.
104    pub(crate) fn lines(&self) -> &[String] {
105        &self.lines
106    }
107}
108
109/// What a module declared, as [`Game::modules`](crate::Game::modules)
110/// lists it.
111#[derive(Debug, Clone, PartialEq, Eq)]
112#[non_exhaustive]
113pub struct InstalledModule {
114    /// Its name.
115    pub name: &'static str,
116    /// The hooks the game calls.
117    pub hooks: Vec<Hook>,
118    /// The types it put on the context, by [`type_name`].
119    pub states: Vec<&'static str>,
120    /// Its entries' names on the debug menu, as they read at install, in
121    /// the order declared.
122    pub debug_entries: Vec<String>,
123}
124
125impl InstalledModule {
126    /// A module called `name` that declared nothing.
127    pub fn new(name: &'static str) -> Self {
128        Self {
129            name,
130            hooks: Vec::new(),
131            states: Vec::new(),
132            debug_entries: Vec::new(),
133        }
134    }
135}
136
137/// Why a module couldn't be installed.
138#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
139#[non_exhaustive]
140pub enum ModuleError {
141    /// A module of this name is already installed.
142    #[error("the module {name} is installed twice")]
143    SameName {
144        /// The name.
145        name: &'static str,
146    },
147    /// Two modules put a value of the same type on the context.
148    #[error("the modules {other} and {module} both put a {state} on the context")]
149    SameState {
150        /// The type, by [`type_name`].
151        state: &'static str,
152        /// The module installed second.
153        module: &'static str,
154        /// The module that put it there first.
155        other: &'static str,
156    },
157}
158
159/// What [`Module::install`] declares with.
160pub struct Install<'a, G: GameData> {
161    ctx: &'a mut Ctx<G>,
162    installed: InstalledModule,
163    /// The first mistake, reported when the install ends.
164    error: Option<ModuleError>,
165}
166
167impl<G: GameData> fmt::Debug for Install<'_, G> {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        f.debug_struct("Install")
170            .field("installed", &self.installed)
171            .finish_non_exhaustive()
172    }
173}
174
175impl<'a, G: GameData> Install<'a, G> {
176    /// The install of the module `name` on `ctx`.
177    pub(crate) fn new(ctx: &'a mut Ctx<G>, name: &'static str) -> Self {
178        Self {
179            ctx,
180            installed: InstalledModule::new(name),
181            error: None,
182        }
183    }
184
185    /// Declares `hook`: the game calls it.
186    pub fn hook(&mut self, hook: Hook) {
187        if !self.installed.hooks.contains(&hook) {
188            self.installed.hooks.push(hook);
189        }
190    }
191
192    /// Puts `value` on the context, where screens and the module's hooks
193    /// find it by its type ([`Ctx::module_state`]). One value per type in
194    /// a game.
195    pub fn state<T: Any>(&mut self, value: T) {
196        let module = self.installed.name;
197        match self.ctx.modules.put(module, value) {
198            Ok(()) => self.installed.states.push(type_name::<T>()),
199            Err(other) => {
200                let state = type_name::<T>();
201                self.error.get_or_insert(ModuleError::SameState {
202                    state,
203                    module,
204                    other,
205                });
206            }
207        }
208    }
209
210    /// Adds an entry to the debug menu, after the game's and the entries
211    /// of the modules installed before it: `make` makes it each time the
212    /// menu opens ([`DebugMenuScreen::new`](crate::debug::DebugMenuScreen::new)).
213    pub fn debug_entry(&mut self, make: fn() -> DebugEntry<G>) {
214        self.installed.debug_entries.push(make().label(self.ctx));
215        self.ctx.modules.makers.push(make);
216    }
217
218    /// What it declared, or its first mistake.
219    pub(crate) fn finish(self) -> Result<InstalledModule, ModuleError> {
220        match self.error {
221            Some(error) => Err(error),
222            None => Ok(self.installed),
223        }
224    }
225}
226
227/// Whether `name` is a module's name: `snake_case` ASCII.
228pub(crate) fn is_snake_case(name: &str) -> bool {
229    name.starts_with(|c: char| c.is_ascii_lowercase())
230        && name
231            .bytes()
232            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
233}
234
235/// One module's state on the context.
236struct State {
237    id: TypeId,
238    type_name: &'static str,
239    /// The module that put it there.
240    module: &'static str,
241    value: Box<dyn Any>,
242}
243
244/// The modules' part of the [`Ctx`]: their states, one per type, and
245/// the makers of their debug menu entries, in install order.
246pub(crate) struct States<G: GameData> {
247    states: Vec<State>,
248    makers: Vec<fn() -> DebugEntry<G>>,
249}
250
251impl<G: GameData> Default for States<G> {
252    fn default() -> Self {
253        Self {
254            states: Vec::new(),
255            makers: Vec::new(),
256        }
257    }
258}
259
260impl<G: GameData> fmt::Debug for States<G> {
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        f.debug_list()
263            .entries(self.states.iter().map(|s| s.type_name))
264            .finish()
265    }
266}
267
268impl<G: GameData> States<G> {
269    /// Puts `module`'s `value` here, or names the module whose value of
270    /// its type is here already.
271    fn put<T: Any>(&mut self, module: &'static str, value: T) -> Result<(), &'static str> {
272        if let Some(other) = self.find(TypeId::of::<T>()) {
273            return Err(other.module);
274        }
275        self.states.push(State {
276            id: TypeId::of::<T>(),
277            type_name: type_name::<T>(),
278            module,
279            value: Box::new(value),
280        });
281        Ok(())
282    }
283
284    fn find(&self, id: TypeId) -> Option<&State> {
285        self.states.iter().find(|s| s.id == id)
286    }
287
288    /// The value of type `T`, if a module put one here.
289    pub(crate) fn get<T: Any>(&self) -> Option<&T> {
290        self.find(TypeId::of::<T>())?.value.downcast_ref()
291    }
292
293    /// [`get`](Self::get), mutable.
294    pub(crate) fn get_mut<T: Any>(&mut self) -> Option<&mut T> {
295        let state = self.states.iter_mut().find(|s| s.id == TypeId::of::<T>())?;
296        state.value.downcast_mut()
297    }
298
299    /// The modules' debug menu entries, made now, in install order.
300    pub(crate) fn debug_entries(&self) -> impl Iterator<Item = DebugEntry<G>> + '_ {
301        self.makers.iter().map(|make| make())
302    }
303}
304
305#[cfg(test)]
306mod tests;