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;