Skip to main content

shuyi_ui/
game.rs

1//! [`Game`]: a whole game's UI behind one call per frame. The platform
2//! feeds it raw key and controller-button events and the frame time and
3//! shows the buffer it returns; a test drives it the same way without a
4//! window.
5//!
6//! The game gives the screen it starts on and its [`GameHooks`]: the
7//! screen the first key opens while no layout is chosen (a layout picker),
8//! and the debug menu. Modules from outside the default engine join it
9//! with [`Game::with_module`] ([`crate::module`]).
10
11use crate::prelude::*;
12use core::any::Any;
13use core::fmt;
14
15use shuyi_core::StateHash;
16
17use shuyi_assets::audio::AudioManifest;
18use shuyi_assets::dialogue::LineId;
19use shuyi_assets::keymap::{ActionSet, Button, Chord, Key, UiAction};
20
21use crate::audio::{AudioConfig, AudioRequest, MusicClock, MusicCommand, MusicState};
22use crate::ctx::{Ctx, GameData, KeyPrompt, Keyboard};
23use crate::debug;
24use crate::frame::EngineColor;
25use crate::frame::glyph_buffer::{Cell, GlyphBuffer, Rect};
26use crate::frame_cap::{FrameCap, Pacing};
27use crate::input::crank::{Crank, Notches, only_on_the_crank};
28use crate::input::frame_seconds;
29use crate::input::pad::{PadId, PadKind, PadState, Pads};
30use crate::input::state::{Device, InputState};
31use crate::log::{LogCategory, LogLevel};
32use crate::module::{self, Hook, Install, InstalledModule, Module, ModuleError};
33use crate::perf::{self, TagClass};
34use crate::screen::{FrameInput, Screen, ScreenStack, Transition};
35use shuyi_assets::font::CellWidths;
36
37/// A keyboard or controller event as the platform reports it.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
39pub enum RawInputEvent {
40    /// A key went down, with the Shift state at the time.
41    Down(Chord),
42    /// A key went up.
43    Up(Key),
44    /// A character was typed (the platform's text input: the keyboard's own
45    /// layout and Shift applied), for text boxes such as a name. Comes with
46    /// the key's own `Down`.
47    Text(#[serde(with = "one_char")] char),
48    /// A controller button went down on a pad of this kind (its binding
49    /// position, from [`Pads::update`]).
50    PadDown(Button, PadKind),
51    /// A controller button went up on every pad.
52    PadUp(Button),
53}
54
55/// A typed character in a file, as a string of one character: RON escapes a
56/// string, and writes a `char` as it is, so a typed line break would break the
57/// line it is written on.
58mod one_char {
59    use serde::de::Error;
60    use serde::{Deserialize, Deserializer, Serializer};
61
62    use crate::prelude::*;
63
64    /// Serde hands it a reference, whatever a `char`'s size.
65    pub fn serialize<C: Copy + Into<char>, S: Serializer>(
66        c: &C,
67        serializer: S,
68    ) -> Result<S::Ok, S::Error> {
69        serializer.serialize_str((*c).into().encode_utf8(&mut [0; 4]))
70    }
71
72    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<char, D::Error> {
73        let text = String::deserialize(deserializer)?;
74        let mut chars = text.chars();
75        match (chars.next(), chars.next()) {
76            (Some(c), None) => Ok(c),
77            _ => Err(D::Error::custom("expected one character")),
78        }
79    }
80}
81
82impl RawInputEvent {
83    /// This frame's controller events: what [`Pads::update`] reports for
84    /// the pads `connected` now, releases first, each press with the kind
85    /// of the pad that pressed it.
86    pub fn from_pads(pads: &mut Pads, connected: &[(PadId, PadKind, PadState)]) -> Vec<Self> {
87        let changes = pads.update(connected);
88        let event = |(button, pressed)| {
89            if pressed {
90                let kind = pads.kind_holding(button, connected).unwrap_or_default();
91                Self::PadDown(button, kind)
92            } else {
93                Self::PadUp(button)
94            }
95        };
96        changes.into_iter().map(event).collect()
97    }
98
99    /// This frame's crank events: the crank turned `degrees` since the
100    /// last frame (forwards positive), on a pad of `kind`; `docked` if it
101    /// is folded away (then nothing, and a part notch is forgotten). Each
102    /// notch is a press and a release of [`Button::CrankBack`] or
103    /// [`Button::CrankForward`] ([`Crank`]).
104    pub fn from_crank(crank: &mut Crank, degrees: f32, docked: bool, kind: PadKind) -> Vec<Self> {
105        if docked {
106            crank.dock();
107            return Vec::new();
108        }
109        Self::crank_notches(crank.turn(degrees), kind)
110    }
111
112    /// [`from_crank`](Self::from_crank) of a turn in whole `steps`
113    /// ([`Crank::turn_steps`]): what a backend that reads a float hands on
114    /// from its boundary.
115    pub fn from_crank_steps(
116        crank: &mut Crank,
117        steps: i32,
118        docked: bool,
119        kind: PadKind,
120    ) -> Vec<Self> {
121        if docked {
122            crank.dock();
123            return Vec::new();
124        }
125        Self::crank_notches(crank.turn_steps(steps), kind)
126    }
127
128    /// A press and a release for each of `notches`.
129    fn crank_notches(notches: Notches, kind: PadKind) -> Vec<Self> {
130        let Some((button, times)) = Crank::presses(notches) else {
131            return Vec::new();
132        };
133        (0..times)
134            .flat_map(|_| [Self::PadDown(button, kind), Self::PadUp(button)])
135            .collect()
136    }
137}
138
139/// The result of one frame. `V` is the game's voice clip variant. Only
140/// [`Game::frame`] builds one, so a field may come without breaking a game
141/// (ADR-0039).
142#[derive(Debug, Clone, Copy)]
143#[non_exhaustive]
144pub struct FrameOutput<'a, V> {
145    /// What to show.
146    pub buffer: &'a GlyphBuffer,
147    /// Whether the game has asked to quit (it then ignores further input).
148    pub quit: bool,
149    /// Every audio request screens made this frame, in order. The platform
150    /// plays the sounds; the music requests are already turned into
151    /// [`music`].
152    ///
153    /// [`music`]: Self::music
154    pub audio: &'a [AudioRequest<V>],
155    /// What to do to the music this frame.
156    pub music: &'a [MusicCommand],
157    /// The player's music volume, 0โ€“1: the platform multiplies the music's
158    /// own volume by it, also for a track already playing.
159    pub music_volume: f32,
160    /// The player's sound volume, 0โ€“1: the platform multiplies every
161    /// sound's own volume by it.
162    pub sound_volume: f32,
163    /// Whether the game should fill the screen (the Fullscreen setting);
164    /// the platform switches when this changes.
165    pub fullscreen: bool,
166    /// How loud voice clips play, 0โ€“1
167    /// ([`Settings::voice_volume`](crate::settings::Settings::voice_volume)). The
168    /// platform applies it to the voice that is playing too.
169    pub voice_volume: f32,
170    /// The most frames a second to show (the player's frame cap): the
171    /// platform paces its frames to it from the next.
172    pub frame_cap: FrameCap,
173    /// The serial of the cap's latest change ([`Ctx::frame_cap_serial`]):
174    /// the platform reports it back once it has applied the cap
175    /// ([`Game::set_pacing`]).
176    pub frame_cap_serial: u32,
177}
178
179/// A screen the engine opens for the game, by name.
180pub struct ScreenHook<G: GameData> {
181    /// The screen's [`name`](Screen::name).
182    pub name: &'static str,
183    /// Makes the screen.
184    pub open: fn(&Ctx<G>) -> Box<dyn Screen<G>>,
185}
186
187impl<G: GameData> Clone for ScreenHook<G> {
188    fn clone(&self) -> Self {
189        *self
190    }
191}
192
193impl<G: GameData> Copy for ScreenHook<G> {}
194
195// check-text: not player text (debug output)
196impl<G: GameData> fmt::Debug for ScreenHook<G> {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        write!(f, "ScreenHook({})", self.name)
199    }
200}
201
202/// Where the game has a say in the frame loop. [`Default`] has none.
203#[derive(Debug)]
204pub struct GameHooks<G: GameData> {
205    /// What the first key press opens while no layout is chosen, on a
206    /// platform with a keyboard (a layout picker): it opens over whatever
207    /// shows, and the key does nothing else. Never while a screen of its
208    /// name is open.
209    pub first_key: Option<ScreenHook<G>>,
210    /// What the Debug action ([`UiAction::Debug`]) opens with debug tools
211    /// on ([`Ctx::debug_tools`]), unless the top screen is one of
212    /// [`debug_screens`](Self::debug_screens).
213    pub debug: Option<ScreenHook<G>>,
214    /// The debug tools' screens, over which the Debug action does nothing.
215    pub debug_screens: &'static [&'static str],
216}
217
218impl<G: GameData> Default for GameHooks<G> {
219    fn default() -> Self {
220        Self {
221            first_key: None,
222            debug: None,
223            debug_screens: &[],
224        }
225    }
226}
227
228/// Owns the screens, input state, shared context and the console buffer.
229pub struct Game<G: GameData> {
230    stack: ScreenStack<G>,
231    input: InputState<G::Action>,
232    ctx: Ctx<G>,
233    hooks: GameHooks<G>,
234    buffer: GlyphBuffer,
235    quit: bool,
236    music: MusicState,
237    /// This frame's audio requests and music commands.
238    audio_out: Vec<AudioRequest<G::Voice>>,
239    music_out: Vec<MusicCommand>,
240    /// The fixed simulation steps due in the next frame
241    /// ([`set_ticks`](Self::set_ticks)).
242    ticks: u32,
243    /// Whether the game came back from a suspend before the next frame
244    /// ([`set_resumed`](Self::set_resumed)).
245    resumed: bool,
246    /// The installed modules, and what each declared, in install order.
247    modules: Vec<Box<dyn Module<G>>>,
248    installed: Vec<InstalledModule>,
249}
250
251impl<G: GameData> Game<G> {
252    /// A game showing `root`, with the game's `hooks`.
253    pub fn new(ctx: Ctx<G>, root: Box<dyn Screen<G>>, hooks: GameHooks<G>) -> Self {
254        Self::with_stack(ctx, ScreenStack::new(root), hooks)
255    }
256
257    /// A game showing the screens of `stack`, with the game's `hooks`,
258    /// already drawn. Music switches as the audio manifest's default switch
259    /// says ([`AudioManifest::music_switch`]).
260    pub fn with_stack(ctx: Ctx<G>, stack: ScreenStack<G>, hooks: GameHooks<G>) -> Self {
261        let input = InputState::new(ctx.keymap.clone()).starting_on(ctx.device);
262        let blank = ctx.game.blank_cell(&ctx.palette);
263        let switch = ctx.game.audio_manifest().music_switch();
264        let mut game = Self {
265            stack,
266            input,
267            buffer: GlyphBuffer::with_spec(ctx.console, blank).with_widths(ctx.widths().clone()),
268            ctx,
269            hooks,
270            quit: false,
271            music: MusicState::with_switch(switch),
272            audio_out: Vec::new(),
273            music_out: Vec::new(),
274            ticks: 0,
275            resumed: false,
276            modules: Vec::new(),
277            installed: Vec::new(),
278        };
279        game.stack.own_top(&mut game.ctx);
280        game.redraw();
281        game
282    }
283
284    /// The same game with `module` installed after those it has: its
285    /// [`install`](Module::install) runs, and the frame is drawn again
286    /// with what it draws. Refused for a name already installed or a state
287    /// another module put on the context.
288    pub fn with_module(self, module: impl Module<G>) -> Result<Self, ModuleError> {
289        self.with_modules([Box::new(module) as Box<dyn Module<G>>])
290    }
291
292    /// The same game with `modules` installed in order, as
293    /// [`with_module`](Self::with_module) installs one.
294    pub fn with_modules(
295        mut self,
296        modules: impl IntoIterator<Item = Box<dyn Module<G>>>,
297    ) -> Result<Self, ModuleError> {
298        for module in modules {
299            self.install(module)?;
300        }
301        self.redraw();
302        Ok(self)
303    }
304
305    fn install(&mut self, mut module: Box<dyn Module<G>>) -> Result<(), ModuleError> {
306        let name = module.name();
307        debug_assert!(
308            module::is_snake_case(name),
309            "a module's name is snake_case ASCII: {name:?}"
310        );
311        if self.installed.iter().any(|m| m.name == name) {
312            return Err(ModuleError::SameName { name });
313        }
314        let mut install = Install::new(&mut self.ctx, name);
315        module.install(&mut install);
316        self.installed.push(install.finish()?);
317        self.modules.push(module);
318        Ok(())
319    }
320
321    /// The installed modules, in install order, with what each declared.
322    pub fn modules(&self) -> &[InstalledModule] {
323        &self.installed
324    }
325
326    /// Turns the debug tools (the Debug action) on or off
327    /// ([`Ctx::debug_tools`]).
328    #[must_use]
329    pub fn with_debug_screens(mut self, on: bool) -> Self {
330        self.ctx.debug_tools = on;
331        self
332    }
333
334    /// Turns the screens' action guard on or off
335    /// ([`ScreenStack::guard_actions`]): while it is on, a screen that
336    /// declares its actions is handed only those. The test Harness turns
337    /// it on.
338    pub fn guard_actions(&mut self, on: bool) {
339        self.stack.guard_actions(on);
340    }
341
342    /// Every action held back from a screen that doesn't declare it, with
343    /// the screen's name ([`ScreenStack::undeclared`]).
344    pub fn undeclared_actions(&self) -> &[(&'static str, G::Action)] {
345        self.stack.undeclared()
346    }
347
348    /// Tells the game what music is sounding, for [`Ctx::music_clock`]: the
349    /// cue and the seconds since the track really started, or `None` in
350    /// silence. The platform knows (a track starts once its file has
351    /// loaded) and calls this before every [`frame`](Self::frame), so
352    /// screens read a clock at most one frame old. The music's fades learn
353    /// from it when a new track sounds ([`MusicState::heard`]).
354    pub fn set_music_playing(&mut self, playing: Option<(&str, f64)>) {
355        let manifest = self.ctx.game.audio_manifest();
356        self.ctx.music_clock =
357            playing.and_then(|(cue, elapsed)| MusicClock::from_elapsed(manifest, cue, elapsed));
358        self.music.heard(self.ctx.music_clock.as_ref());
359    }
360
361    /// Tells the game which line's voice clip is sounding, for
362    /// [`Ctx::voice_playing`], or `None` in silence. The platform knows
363    /// (a clip sounds once its file has loaded, and until it ends) and
364    /// calls this before every [`frame`](Self::frame), as it does
365    /// [`set_music_playing`](Self::set_music_playing).
366    pub fn set_voice_playing(&mut self, line: Option<&LineId>) {
367        if self.ctx.voice_playing.as_ref() != line {
368            self.ctx.voice_playing = line.cloned();
369        }
370    }
371
372    /// Tells the game how the platform paces its frames: the frame cap it
373    /// applied with its serial, and the frames a second it measured
374    /// ([`Pacing`]). The platform calls this before every
375    /// [`frame`](Self::frame), as it does
376    /// [`set_music_playing`](Self::set_music_playing), so a request for a
377    /// cap is confirmed ([`Ctx::frame_cap_status`]) and a screen can show
378    /// the frame rate ([`Ctx::pacing`]).
379    pub fn set_pacing(&mut self, report: Pacing) {
380        self.ctx.set_pacing(report);
381    }
382
383    /// Says how many fixed simulation steps are due in the next
384    /// [`frame`](Self::frame) (ADR-0003 ยง2): its screen reads them as
385    /// [`FrameInput::ticks`]. The platform's `App` counts them from its
386    /// clock and calls this before every frame; a frame after none has 0.
387    pub fn set_ticks(&mut self, ticks: u32) {
388        self.ticks = ticks;
389    }
390
391    /// Says the system resumed the game from a suspend before the next
392    /// [`frame`](Self::frame): its screen reads it as
393    /// [`FrameInput::resumed`], and a tick stage's
394    /// [`TickDriver`](crate::tick::TickDriver) forgets the presses it kept
395    /// from before. That frame lets go of every key and button held before
396    /// the suspend ([`InputState::release_all`]) before it applies its
397    /// events, so a key let go while suspended doesn't stay held, and one
398    /// pressed in that frame counts. The platform's `App` calls it on the
399    /// system's `Resume`.
400    pub fn set_resumed(&mut self) {
401        self.resumed = true;
402    }
403
404    /// Lets go of every key and button held now ([`InputState::release_all`]):
405    /// the next frame reports their actions let go, and one still down
406    /// counts once it is pressed again. The platform's `App` calls it on
407    /// the system's `FocusLost`, since a window without focus hears no
408    /// release.
409    pub fn forget_held_input(&mut self) {
410        self.input.release_all();
411    }
412
413    /// Flips the Fullscreen setting, as a window's own shortcut does. The
414    /// platform then switches the window on the next frame, and an options
415    /// screen shows the new value. Does nothing where the game isn't in a
416    /// window ([`PlatformFacts::windowed`](crate::PlatformFacts::windowed)
417    /// false): a device that is always full screen.
418    pub fn toggle_fullscreen(&mut self) {
419        if self.ctx.platform.windowed {
420            let _ = self.ctx.change_settings(|s| s.fullscreen = !s.fullscreen);
421        }
422    }
423
424    /// Tells the modules that declared [`Hook::Reloaded`], in install order,
425    /// that the files at `changed` changed on disk (paths in the bundle).
426    /// The platform's `App` calls it before the frame's update, in a build
427    /// with debug tools on. After a quit, nothing.
428    pub fn reloaded(&mut self, changed: &[String]) {
429        if self.quit {
430            return;
431        }
432        for (module, installed) in self.modules.iter_mut().zip(&self.installed) {
433            if installed.hooks.contains(&Hook::Reloaded) {
434                module.reloaded(&mut self.ctx, changed);
435            }
436        }
437    }
438
439    /// Runs one frame: applies `events` (in order), advances input by `dt`
440    /// seconds, updates the top screen with the resulting actions, collects
441    /// its audio requests and redraws. After a quit, frames do nothing. In
442    /// a frame the platform measures ([`Ctx::begin_perf_frame`]) it times
443    /// the update and the drawing, and with memory scopes
444    /// ([`Ctx::set_memory_scopes`]) it tags what each allocates `update`
445    /// and `draw` ([`crate::perf`]).
446    pub fn frame(&mut self, events: &[RawInputEvent], dt: f32) -> FrameOutput<'_, G::Voice> {
447        self.audio_out.clear();
448        self.music_out.clear();
449        // Nonsense frame times pass no time, for the screens, the clock and
450        // the music alike.
451        let dt = frame_seconds(dt);
452        let ticks = core::mem::take(&mut self.ticks);
453        let resumed = core::mem::take(&mut self.resumed);
454        if !self.quit {
455            // The update's and the drawing's times, in a frame the
456            // platform measures, and with memory scopes their tags
457            // (crate::perf).
458            let tagged = self.ctx.enter_tag(TagClass::Phase, "update");
459            let start = self.ctx.perf_now_in_frame();
460            self.step(events, dt, (ticks, resumed));
461            let drawing = self.ctx.perf_now_in_frame();
462            perf::leave_tag(tagged);
463            let tagged = self.ctx.enter_tag(TagClass::Phase, "draw");
464            self.redraw();
465            let drawn = self.ctx.perf_now_in_frame();
466            perf::leave_tag(tagged);
467            self.ctx.set_perf_frame_times(start, drawing, drawn);
468            self.collect_audio(dt);
469        }
470        FrameOutput {
471            buffer: &self.buffer,
472            quit: self.quit,
473            audio: &self.audio_out,
474            music: &self.music_out,
475            music_volume: self.ctx.settings().music_factor(),
476            sound_volume: self.ctx.settings().sound_factor(),
477            fullscreen: self.ctx.settings().fullscreen,
478            voice_volume: self.ctx.settings().voice_factor(),
479            frame_cap: self.ctx.settings().frame_cap,
480            frame_cap_serial: self.ctx.frame_cap_serial(),
481        }
482    }
483
484    /// Advances the sounds' fades, moves the screens' audio requests into
485    /// this frame's output and runs the music state machine. In debug
486    /// builds, a cue the manifest lacks is a bug in the screen and panics,
487    /// and so does a looped cue played without a handle, which nothing
488    /// could stop (a release build plays it once). The engine's widgets
489    /// play the game's [`AudioConfig`](crate::audio::AudioConfig) cues
490    /// whatever sounds it has, so one the manifest lacks is silence: a game
491    /// with no sounds has quiet menus.
492    fn collect_audio(&mut self, dt: f32) {
493        self.ctx.audio.advance(dt);
494        self.audio_out = self.ctx.audio.take();
495        for problem in self.ctx.audio.take_problems() {
496            self.ctx.log(LogLevel::Warn, LogCategory::Audio, problem);
497        }
498        for request in &self.audio_out {
499            let manifest = self.ctx.game.audio_manifest();
500            debug_assert!(
501                is_known(manifest, request)
502                    || is_widget_cue(&self.ctx.game.audio_config(), request)
503                    || request
504                        .cue()
505                        .is_some_and(|cue| self.ctx.audio.knows_added(cue)),
506                "audio request for a cue not in the audio manifest: {request:?}"
507            );
508            debug_assert!(
509                !plays_a_loop(manifest, request),
510                "play_sound of the looped cue {:?}: start it with start_sound, whose handle stops it",
511                request.cue().unwrap_or_default()
512            );
513            self.music.request(request, &mut self.music_out);
514        }
515        self.music.update(dt, &mut self.music_out);
516    }
517
518    /// One frame's update: `events` in order, `dt` seconds, and the fixed
519    /// steps due with whether the game just resumed.
520    fn step(&mut self, events: &[RawInputEvent], dt: f32, (ticks, resumed): (u32, bool)) {
521        // Bindings changed between frames (a test rebinding keys).
522        self.sync_keymap();
523        // A key let go while the game was suspended never came up: let go
524        // of everything held before, ahead of this frame's presses.
525        if resumed {
526            self.input.release_all();
527        }
528        let mut pressed = Vec::new();
529        let mut text = Vec::new();
530        let mut buttons_down = Vec::new();
531        let mut buttons_up = Vec::new();
532        // Until a layout is chosen no key acts: the first one opens the
533        // game's first-key screen over whatever is showing and does nothing
534        // else; that screen then takes the keys. Without a keyboard, never.
535        let first_key = self.hooks.first_key.filter(|hook| {
536            self.ctx.platform.keyboard == Keyboard::Present
537                && self.ctx.layout().is_none()
538                && !self.stack.names().contains(&hook.name)
539                && events.iter().any(|e| matches!(e, RawInputEvent::Down(_)))
540        });
541        for &event in events {
542            // Any key or any controller button ends the wait for one.
543            let press = matches!(event, RawInputEvent::Down(_) | RawInputEvent::PadDown(..));
544            let ends_wait = press && self.ctx.key_prompt == KeyPrompt::Waiting;
545            if ends_wait {
546                self.ctx.key_prompt = KeyPrompt::Pressed;
547            }
548            match event {
549                RawInputEvent::Down(_) if first_key.is_some() => self.input.keyboard_used(),
550                RawInputEvent::Down(chord) => {
551                    self.input.key_down(chord);
552                    pressed.push(chord);
553                }
554                RawInputEvent::Up(key) => self.input.key_up(key),
555                // Control characters (Enter, Backspace, Escape on some
556                // platforms) are keys, not text.
557                RawInputEvent::Text(c) if !c.is_control() => text.push(c),
558                RawInputEvent::Text(_) => {}
559                RawInputEvent::PadDown(button, kind) => {
560                    self.input.pad_down(button, kind);
561                    if ends_wait {
562                        // The wait takes it: nothing when let go either
563                        // (a chord button's action).
564                        self.input.pad_taken(button);
565                    }
566                    buttons_down.push(button);
567                }
568                RawInputEvent::PadUp(button) => {
569                    self.input.pad_up(button);
570                    buttons_up.push(button);
571                }
572            }
573        }
574        self.ctx.clock_s += f64::from(dt);
575        self.ctx.device = self.input.device();
576        let actions = self.input.update(dt);
577        let held = self.held();
578        for (module, installed) in self.modules.iter_mut().zip(&self.installed) {
579            if installed.hooks.contains(&Hook::BeforeUpdate) {
580                module.before_update(&mut self.ctx, dt);
581            }
582        }
583        let debug = self.hooks.debug.filter(|_| {
584            self.ctx.debug_tools
585                && actions.iter().any(|a| a.ui() == Some(UiAction::Debug))
586                && !self
587                    .stack
588                    .top_name()
589                    .is_some_and(|n| self.hooks.debug_screens.contains(&n))
590        });
591        if let Some(hook) = first_key {
592            self.open(hook);
593        }
594        if let Some(hook) = debug {
595            self.open(hook);
596        } else {
597            // A chord button gives its own action when let go or after the
598            // chord window: that is pad input too.
599            let from_pad = !buttons_down.is_empty()
600                || (!actions.is_empty() && matches!(self.ctx.device, Device::Pad(_)));
601            // The actions that went down and came up, for the fixed steps.
602            let downs = self.input.presses().to_vec();
603            let ups = self.input.released().to_vec();
604            let input = FrameInput::new(actions, dt, held)
605                .with_presses(downs, ups)
606                .with_typing(pressed, text)
607                .with_buttons(buttons_down, buttons_up)
608                .with_pad(from_pad)
609                .with_ticks(ticks)
610                .with_resumed(resumed);
611            self.quit = self.stack.update(&mut self.ctx, &input);
612            self.sync_keymap();
613        }
614    }
615
616    /// Hands the top screen `action` alone, in an update of its own with no
617    /// time passing, as if a key bound to it had just been pressed: an
618    /// action from outside the keys and buttons, such as an entry of the
619    /// platform's own menu (the Playdate's system menu, ticket 1035). The
620    /// screen does with it what it does with the key, or nothing. Its
621    /// sounds come out with the next frame. No step is due in it: a tick
622    /// stage keeps it as a tap (pressed, and let go unless a key holds it)
623    /// for its next step. After a quit, nothing.
624    pub fn act(&mut self, action: G::Action) {
625        if self.quit {
626            return;
627        }
628        let held = self.held();
629        let from_pad = matches!(self.ctx.device, Device::Pad(_));
630        let input = FrameInput::new(vec![action], 0.0, held).with_pad(from_pad);
631        self.quit = self.stack.update(&mut self.ctx, &input);
632        self.sync_keymap();
633        self.redraw();
634    }
635
636    /// Opens `hook`'s screen over the top one, which hears it is covered.
637    fn open(&mut self, hook: ScreenHook<G>) {
638        let screen = (hook.open)(&self.ctx);
639        self.stack.apply_in(&mut self.ctx, Transition::Push(screen));
640    }
641
642    /// The actions whose keys or buttons are held.
643    fn held(&self) -> Vec<G::Action> {
644        G::Action::ALL
645            .iter()
646            .copied()
647            .filter(|&a| self.input.is_held(a))
648            .collect()
649    }
650
651    /// Hands `ctx`'s keymap to the input if it changed (a layout was
652    /// picked, keys were rebound), so the new keys work from the next press.
653    fn sync_keymap(&mut self) {
654        if *self.input.keymap() != self.ctx.keymap {
655            self.input.set_keymap(self.ctx.keymap.clone());
656        }
657    }
658
659    /// Prints the characters `widths` has wide in two cells, as the font
660    /// atlas the frame is drawn with has them ([`CellWidths::of`]), in the
661    /// language text is shown in ([`Ctx::widths`], which the frame's
662    /// [`GlyphBuffer::widths`] follow). The frame is drawn again if that
663    /// changed them. The platform calls it once the game has loaded, with
664    /// its look's atlas, and again after its files change.
665    ///
666    /// [`CellWidths::of`]: shuyi_assets::font::CellWidths::of
667    pub fn set_widths(&mut self, widths: CellWidths) {
668        self.ctx.set_widths(widths);
669        if self.buffer.widths() != self.ctx.widths() {
670            self.redraw();
671        }
672    }
673
674    /// Clears the buffer and draws the stack into it, then the modules
675    /// over it, then, with debug tools on, the debug view's gizmos over
676    /// everything: after a change made from outside the frame loop (a test
677    /// changing the context). The buffer takes the widths of the language
678    /// text is shown in first ([`Ctx::widths`]).
679    pub fn redraw(&mut self) {
680        if self.buffer.widths() != self.ctx.widths() {
681            self.buffer.set_widths(self.ctx.widths().clone());
682        }
683        let blank = self.ctx.game.blank_cell(&self.ctx.palette);
684        self.buffer.fill_rect(self.buffer.bounds(), blank);
685        self.stack.draw(&self.ctx, &mut self.buffer);
686        for (module, installed) in self.modules.iter().zip(&self.installed) {
687            if installed.hooks.contains(&Hook::DrawOver) {
688                module.draw_over(&self.ctx, &mut self.buffer);
689            }
690        }
691        self.draw_crank_hint();
692        self.draw_gizmos();
693    }
694
695    /// With debug tools on, asks the screens drawn for the debug view's
696    /// gizmos ([`Screen::debug_draw`], ADR-0085), keeps the channels they
697    /// offer for the debug menu, and paints those of the channels on over
698    /// everything. It asks with no channel on too: that is how a channel
699    /// is offered. With debug tools off, nothing is asked.
700    fn draw_gizmos(&mut self) {
701        if !self.ctx.debug_tools {
702            return;
703        }
704        let mut gizmos = self
705            .ctx
706            .debug_view
707            .gizmos()
708            .within(self.buffer.pixel_bounds());
709        self.stack.debug_draw(&self.ctx, &mut gizmos);
710        self.ctx.debug_view.offer(gizmos.offered());
711        if !gizmos.is_empty() {
712            debug::glyph::paint_gizmos(&self.ctx.palette, &gizmos, &mut self.buffer);
713        }
714    }
715
716    /// The hint to unfold the crank, on the last row, while it is docked
717    /// and an action the top screen requires can only be pressed by
718    /// winding it: a docked crank gives no notches.
719    fn draw_crank_hint(&mut self) {
720        if !self.ctx.crank_docked {
721            return;
722        }
723        let needs_crank = self.stack.top_actions().is_some_and(|actions| {
724            actions
725                .into_iter()
726                .filter(|action| action.is_required())
727                .any(|action| only_on_the_crank(&self.input.keymap().buttons_for(action)))
728        });
729        if !needs_crank {
730            return;
731        }
732        let hint = if self.ctx.game.lang().has(CRANK_HINT) {
733            self.ctx.text(CRANK_HINT)
734        } else {
735            CRANK_HINT_ENGLISH
736        };
737        let ink = self.ctx.palette.engine(EngineColor::Warning);
738        let paper = self.ctx.palette.engine(EngineColor::Panel);
739        let (width, y) = (
740            i32::from(self.buffer.width()),
741            i32::from(self.buffer.height()) - 1,
742        );
743        self.buffer
744            .fill_rect(Rect::new(0, y, width, 1), Cell::new(' ', ink, paper));
745        let len = self.buffer.text_cells(hint);
746        self.buffer
747            .print((width - len).max(0) / 2, y, hint, ink, paper);
748    }
749
750    /// The top-most screen of type `T` on the stack, if it opts in
751    /// ([`Screen::as_any`]).
752    pub fn screen<T: Any>(&self) -> Option<&T> {
753        self.stack.find()
754    }
755
756    /// The state hash of the top-most screen that has one
757    /// ([`Screen::state_hash`]).
758    pub fn state_hash(&self) -> Option<StateHash> {
759        self.stack.state_hash()
760    }
761
762    /// [`screen`](Self::screen), mutable, for scripted tests.
763    pub fn screen_mut<T: Any>(&mut self) -> Option<&mut T> {
764        self.stack.find_mut()
765    }
766
767    /// The music state machine (which track plays).
768    pub fn music(&self) -> &MusicState {
769        &self.music
770    }
771
772    /// The input state (held keys, the keymap in use).
773    pub fn input(&self) -> &InputState<G::Action> {
774        &self.input
775    }
776
777    /// The last frame drawn.
778    pub fn buffer(&self) -> &GlyphBuffer {
779        &self.buffer
780    }
781
782    /// Whether the game has asked to quit.
783    pub fn quit_requested(&self) -> bool {
784        self.quit
785    }
786
787    /// The shared context.
788    pub fn ctx(&self) -> &Ctx<G> {
789        &self.ctx
790    }
791
792    /// The shared context, to change settings mid-run (tests and tools;
793    /// screens get theirs in [`Screen::update`]).
794    pub fn ctx_mut(&mut self) -> &mut Ctx<G> {
795        &mut self.ctx
796    }
797
798    /// Ends the game, handing back the shared context (and so its storage).
799    pub fn into_ctx(self) -> Ctx<G> {
800        self.ctx
801    }
802
803    /// Name of the top screen, or `None` once the last one has closed.
804    pub fn top_screen(&self) -> Option<&'static str> {
805        self.stack.top_name()
806    }
807
808    /// Names of the open screens, bottom first.
809    pub fn screens(&self) -> Vec<&'static str> {
810        self.stack.names()
811    }
812}
813
814/// The text key of the hint to unfold the crank.
815pub const CRANK_HINT: &str = "crank.unfold";
816
817/// The hint in English, for a game whose text has no [`CRANK_HINT`].
818// check-text: the engine's fallback, for a game that says nothing else
819const CRANK_HINT_ENGLISH: &str = "Unfold the crank to go on";
820
821/// Whether `request` names a cue of the right kind in `manifest`.
822/// Whether `request` plays a cue the engine's widgets play: `config`'s.
823fn is_widget_cue<V>(config: &AudioConfig, request: &AudioRequest<V>) -> bool {
824    let AudioRequest::PlaySound { cue, .. } = request else {
825        return false;
826    };
827    [
828        config.move_cue,
829        config.select_cue,
830        config.cancel_cue,
831        config.denied_cue,
832        config.cursor_cue,
833    ]
834    .contains(&cue.as_str())
835}
836
837fn is_known<V>(manifest: &AudioManifest, request: &AudioRequest<V>) -> bool {
838    match request {
839        AudioRequest::PlaySound { cue, .. } | AudioRequest::StartSound { cue, .. } => {
840            manifest.sounds.contains_key(cue)
841        }
842        AudioRequest::PlayMusic { cue, .. } => manifest.music.contains_key(cue),
843        AudioRequest::StopSound { .. }
844        | AudioRequest::SoundVolume { .. }
845        | AudioRequest::PauseSounds
846        | AudioRequest::ResumeSounds
847        | AudioRequest::StopMusic
848        | AudioRequest::StopMusicWith { .. }
849        | AudioRequest::PlayVoice { .. }
850        | AudioRequest::StopVoice
851        | AudioRequest::PreloadVoices { .. }
852        | AudioRequest::AddSound { .. }
853        | AudioRequest::RemoveSound { .. } => true,
854    }
855}
856
857/// Whether `request` plays a looped sound cue once, with no handle to
858/// stop it.
859fn plays_a_loop<V>(manifest: &AudioManifest, request: &AudioRequest<V>) -> bool {
860    matches!(request, AudioRequest::PlaySound { cue, .. }
861        if manifest.sounds.get(cue).is_some_and(|s| s.looped))
862}
863
864#[cfg(test)]
865mod tests;