1use crate::alloc_prelude::*;
44use alloc::format;
45use core::time::Duration;
46
47use serde::Serialize;
48use shuyi_assets::dialogue::LineId;
49use shuyi_assets::font::CellWidths;
50use shuyi_assets::voice::clip_file;
51use shuyi_core::made::Made;
52use shuyi_core::{StateHash, state_hash};
53use shuyi_ui::audio::{AudioRequest, MusicCommand};
54use shuyi_ui::frame::Rgb;
55use shuyi_ui::frame_cap::Pacing;
56use shuyi_ui::input::frame_seconds;
57use shuyi_ui::log::{LOG_LINES, LogCategory, LogFilter, LogLevel, LogLine, LogRing};
58use shuyi_ui::perf::{
59 FrameRecord, HeapSnapshot, MemoryScopes, PERF_FRAMES, PerfClock, PerfRing, TagClass,
60};
61use shuyi_ui::settings::{Background, BackgroundAudio, GameSettings};
62use shuyi_ui::storage::Storage;
63use shuyi_ui::system_keyboard::TextAnswer;
64use shuyi_ui::{Ctx, Game, GameData, IdleMotion, KeyPrompt, PlatformFacts, RawInputEvent};
65
66use crate::audio::{AudioFrame, SoundRequest};
67use crate::crash::{self, LastCrash};
68use crate::platform::{DisplayInfo, Frame, Look, Millis, Platform, Show, SystemEvent};
69use crate::session::{Recorder, SessionRecord};
70use crate::text_input::TextInput;
71use crate::tick::TickClock;
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75#[must_use]
76pub enum FrameOutcome {
77 Continue,
79 Quit,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
90#[non_exhaustive]
91pub enum Card {
92 Text {
95 line: String,
97 ink: Rgb,
99 background: Rgb,
101 },
102 Picture {
105 path: String,
107 background: Rgb,
109 },
110}
111
112const WHITE: Rgb = Rgb::new(255, 255, 255);
114const BLACK: Rgb = Rgb::new(0, 0, 0);
116
117impl Card {
118 pub fn text(line: impl Into<String>) -> Self {
120 Self::Text {
121 line: line.into(),
122 ink: WHITE,
123 background: BLACK,
124 }
125 }
126
127 pub fn picture(path: impl Into<String>) -> Self {
129 Self::Picture {
130 path: path.into(),
131 background: BLACK,
132 }
133 }
134
135 #[must_use]
137 pub fn colored(self, ink: Rgb, background: Rgb) -> Self {
138 match self {
139 Self::Text { line, .. } => Self::Text {
140 line,
141 ink,
142 background,
143 },
144 Self::Picture { path, .. } => Self::Picture { path, background },
145 }
146 }
147
148 pub fn background(&self) -> Rgb {
150 match self {
151 Self::Text { background, .. } | Self::Picture { background, .. } => *background,
152 }
153 }
154}
155
156#[derive(Debug)]
160#[non_exhaustive]
161pub struct Setup {
162 pub display: DisplayInfo,
164 pub storage: Box<dyn Storage>,
166 pub seed: u64,
169 pub voice_manifest: Option<String>,
171 pub platform: PlatformFacts,
175 pub key_prompt: KeyPrompt,
178 pub last_crash: Option<LastCrash>,
183}
184
185impl Setup {
186 pub fn ctx<G: GameData>(self, mut ctx: Ctx<G>) -> Ctx<G> {
191 ctx.idle_motion = IdleMotion::On;
192 ctx.music_seed = self.seed;
193 ctx.key_prompt = self.key_prompt;
194 if let Some(device) = self.platform.device {
195 ctx.device = device;
196 }
197 ctx.platform = self.platform;
198 let mut ctx = ctx.with_storage(self.storage);
199 if let Some(manifest) = &self.voice_manifest {
200 ctx.set_voice_manifest(manifest);
201 }
202 ctx
203 }
204}
205
206pub type Load<G> = Box<dyn FnOnce(Setup) -> Result<Game<G>, String>>;
209
210pub type LookOf<G> = fn(&G) -> Look<'_>;
212
213pub type SystemHook<G> = fn(&mut Game<G>, SystemEvent);
221
222pub type GameHash<G> = fn(&Game<G>) -> Option<StateHash>;
224
225pub type CrashRecord<G> = fn(&Game<G>) -> Option<String>;
229
230#[non_exhaustive]
234pub struct Start<G: GameData> {
235 pub card: Card,
237 pub load: Load<G>,
239 pub look: LookOf<G>,
241 pub on_system: Option<SystemHook<G>>,
243 pub made: Option<Made>,
245 pub state_hash: Option<GameHash<G>>,
247 pub crash_record: Option<CrashRecord<G>>,
249}
250
251impl<G: GameData> Start<G> {
252 pub fn new(
255 card: Card,
256 look: LookOf<G>,
257 load: impl FnOnce(Setup) -> Result<Game<G>, String> + 'static,
258 ) -> Self {
259 Self {
260 card,
261 load: Box::new(load),
262 look,
263 on_system: None,
264 made: None,
265 state_hash: None,
266 crash_record: None,
267 }
268 }
269
270 #[must_use]
272 pub fn on_system(mut self, hook: SystemHook<G>) -> Self {
273 self.on_system = Some(hook);
274 self
275 }
276
277 #[must_use]
282 pub fn made(mut self, made: Made) -> Self {
283 self.made = Some(made);
284 self
285 }
286
287 #[must_use]
292 pub fn state_hash(mut self, hash: GameHash<G>) -> Self {
293 self.state_hash = Some(hash);
294 self
295 }
296
297 #[must_use]
302 pub fn crash_record(mut self, hook: CrashRecord<G>) -> Self {
303 self.crash_record = Some(hook);
304 self
305 }
306
307 #[must_use]
310 pub fn before_load(self, before: impl FnOnce(&mut Setup) + 'static) -> Self {
311 let load = self.load;
312 Self {
313 load: Box::new(move |mut setup| {
314 before(&mut setup);
315 load(setup)
316 }),
317 ..self
318 }
319 }
320}
321
322enum Phase<G: GameData> {
324 Loading {
326 card: Card,
327 load: Option<Load<G>>,
328 shown: bool,
330 },
331 Running(Box<Game<G>>),
333 Failed(String),
335}
336
337#[derive(Debug, Clone, Copy)]
339struct Elapsed {
340 now: Millis,
342 dt: f32,
344 ticks: u32,
346}
347
348#[derive(Debug, Default)]
351struct Clock {
352 last: Option<Millis>,
355 driven: Duration,
357 ticks: TickClock,
358}
359
360impl Clock {
361 fn at(&mut self, now: Millis) -> Elapsed {
363 let elapsed = self.last.map_or(Duration::ZERO, |last| now.since(last));
364 self.last = Some(now);
365 Elapsed {
366 now,
367 dt: elapsed.as_secs_f32(),
368 ticks: self.ticks.advance(elapsed),
369 }
370 }
371
372 fn after(&mut self, seconds: f32) -> Elapsed {
374 let dt = frame_seconds(seconds);
375 let elapsed = Duration::try_from_secs_f32(dt).unwrap_or(Duration::ZERO);
376 self.driven = self.driven.saturating_add(elapsed);
377 Elapsed {
378 now: Millis(0).after(self.driven),
379 dt,
380 ticks: self.ticks.advance(elapsed),
381 }
382 }
383
384 fn restart(&mut self) {
387 self.last = None;
388 self.ticks.reset();
389 }
390}
391
392struct Perf {
396 asked: bool,
398 clock: Option<PerfClock>,
400 scopes: Option<MemoryScopes>,
403 app_tag: Entered,
405 load_us: Option<u32>,
407 start: Option<u64>,
409 paint_us: Option<u32>,
411 last_end: Option<u64>,
414 records: PerfRing,
415 read: u64,
418}
419
420impl Perf {
421 fn new() -> Self {
422 Self {
423 asked: false,
424 clock: None,
425 scopes: None,
426 app_tag: None,
427 load_us: None,
428 start: None,
429 paint_us: None,
430 last_end: None,
431 records: PerfRing::new(PERF_FRAMES),
432 read: 0,
433 }
434 }
435
436 fn now<G: GameData>(&self, game: &mut Game<G>) -> Option<u64> {
438 self.start?;
439 game.ctx_mut().perf_now()
440 }
441}
442
443type Entered = Option<(MemoryScopes, u16)>;
445
446fn enter(scopes: Option<MemoryScopes>, name: &'static str) -> Entered {
448 scopes.map(|scopes| (scopes, scopes.enter(TagClass::Phase, name)))
449}
450
451fn leave(entered: Entered) {
453 if let Some((scopes, before)) = entered {
454 scopes.leave(before);
455 }
456}
457
458fn micros(from: u64, to: u64) -> u32 {
461 u32::try_from(to.saturating_sub(from)).unwrap_or(u32::MAX)
462}
463
464#[derive(Serialize)]
467struct FrameFacts<'a> {
468 phase: PhaseFact<'a>,
469 screens: Vec<&'static str>,
471 quit: bool,
472 sounds: &'a [SoundRequest],
473 music: &'a [MusicCommand],
474 game: Option<u64>,
476}
477
478#[derive(Serialize)]
480enum PhaseFact<'a> {
481 Loading,
482 Failed(&'a str),
483 Running,
484}
485
486struct Lists<V> {
488 input: Vec<RawInputEvent>,
489 system: Vec<SystemEvent>,
490 sounds: Vec<SoundRequest>,
491 audio: Vec<AudioRequest<V>>,
492 music: Vec<MusicCommand>,
493 changed: Vec<String>,
494 voice: Option<(LineId, String)>,
497}
498
499#[derive(Debug, Clone, Copy, Default)]
501struct FrameHashes {
502 on: bool,
505 last: Option<StateHash>,
507}
508
509pub struct App<G: GameData> {
511 phase: Phase<G>,
512 look: Option<LookOf<G>>,
513 on_system: Option<SystemHook<G>>,
514 clock: Clock,
515 suspended: bool,
516 focused: bool,
517 background: Background,
520 quit: bool,
521 lists: Lists<G::Voice>,
522 frames: u32,
524 log: LogRing,
526 log_filter: LogFilter,
528 log_read: u64,
533 perf: Perf,
534 pacing: Option<Pacing>,
537 made: Option<Made>,
539 game_hash: Option<GameHash<G>>,
541 crash_record: Option<CrashRecord<G>>,
543 recorder: Option<Box<Recorder>>,
545 frame_hashes: FrameHashes,
546 ticks: u32,
548}
549
550impl<G: GameData> App<G> {
551 pub fn new(start: Start<G>) -> Self {
553 let Start {
554 card,
555 load,
556 look,
557 on_system,
558 made,
559 state_hash,
560 crash_record,
561 } = start;
562 let mut app = Self::in_phase(
563 Phase::Loading {
564 card,
565 load: Some(load),
566 shown: false,
567 },
568 Some(look),
569 on_system,
570 );
571 app.made = made;
572 app.game_hash = state_hash;
573 app.crash_record = crash_record;
574 app
575 }
576
577 pub fn running(game: Game<G>) -> Self {
581 let mut app = Self::in_phase(Phase::Running(Box::new(game)), None, None);
582 app.take_game_log();
583 app
584 }
585
586 fn in_phase(
587 phase: Phase<G>,
588 look: Option<LookOf<G>>,
589 on_system: Option<SystemHook<G>>,
590 ) -> Self {
591 Self {
592 phase,
593 look,
594 on_system,
595 clock: Clock::default(),
596 suspended: false,
597 focused: true,
598 background: <G::Settings as GameSettings>::BACKGROUND,
599 quit: false,
600 lists: Lists {
601 input: Vec::new(),
602 system: Vec::new(),
603 sounds: Vec::new(),
604 audio: Vec::new(),
605 music: Vec::new(),
606 changed: Vec::new(),
607 voice: None,
608 },
609 frames: 0,
610 log: LogRing::new(LOG_LINES),
611 log_filter: LogFilter::default(),
612 log_read: 0,
613 perf: Perf::new(),
614 pacing: None,
615 made: None,
616 game_hash: None,
617 crash_record: None,
618 recorder: None,
619 frame_hashes: FrameHashes::default(),
620 ticks: 0,
621 }
622 }
623
624 pub fn set_system_hook(&mut self, hook: SystemHook<G>) {
627 self.on_system = Some(hook);
628 }
629
630 pub fn frame(&mut self, platform: &mut impl Platform, now: Millis) -> FrameOutcome {
636 if let Some(recorder) = &mut self.recorder {
637 recorder.begin(now.0);
638 }
639 self.begin(platform);
640 let elapsed = self.clock.at(now);
641 self.end(platform, elapsed)
642 }
643
644 pub fn set_pacing(&mut self, pacing: Pacing) {
652 self.pacing = Some(pacing);
653 }
654
655 pub fn present_again(&mut self, platform: &mut impl Platform) {
661 match &self.phase {
662 Phase::Loading {
663 card, shown: true, ..
664 } => platform.present(&Frame::new(Show::Card(card))),
665 Phase::Loading { .. } => {}
666 Phase::Running(game) => present_tagged(platform, game, self.look),
667 Phase::Failed(errors) => platform.present(&Frame::new(Show::Errors(errors))),
668 }
669 }
670
671 pub fn frame_after(&mut self, platform: &mut impl Platform, seconds: f32) -> FrameOutcome {
679 if let Some(recorder) = &mut self.recorder {
680 recorder.stop();
681 }
682 self.begin(platform);
683 let elapsed = self.clock.after(seconds);
684 self.end(platform, elapsed)
685 }
686
687 fn begin(&mut self, platform: &mut impl Platform) {
690 self.frames = self.frames.saturating_add(1);
691 self.take_clock(platform);
692 self.perf.app_tag = enter(self.memory_scopes(), "app");
693 self.begin_perf();
694 self.system_events(platform);
695 }
696
697 fn end(&mut self, platform: &mut impl Platform, elapsed: Elapsed) -> FrameOutcome {
700 let outcome = self.run(platform, elapsed);
701 self.take_game_log();
702 let tagged = enter(self.memory_scopes(), "perf");
703 self.end_perf(platform);
704 leave(tagged);
705 leave(self.perf.app_tag.take());
706 self.end_session_frame();
707 outcome
708 }
709
710 fn end_session_frame(&mut self) {
713 let recording = self.recorder.as_ref().is_some_and(|r| r.is_on());
714 if !(recording || self.frame_hashes.on) {
715 self.frame_hashes.last = None;
716 return;
717 }
718 let (phase, screens, game) = match &self.phase {
719 Phase::Loading { .. } => (PhaseFact::Loading, Vec::new(), None),
720 Phase::Failed(errors) => (PhaseFact::Failed(errors), Vec::new(), None),
721 Phase::Running(game) => (
722 PhaseFact::Running,
723 game.screens(),
724 self.game_hash
725 .and_then(|hash| hash(game))
726 .map(|hash| hash.0),
727 ),
728 };
729 let facts = FrameFacts {
730 phase,
731 screens,
732 quit: self.quit,
733 sounds: &self.lists.sounds,
734 music: &self.lists.music,
735 game,
736 };
737 let hash = state_hash(&facts).unwrap_or_default();
739 self.frame_hashes.last = Some(hash);
740 if let Some(recorder) = &mut self.recorder {
741 recorder.end(&self.lists.input, hash);
742 }
743 }
744
745 fn take_clock(&mut self, platform: &impl Platform) {
748 if core::mem::replace(&mut self.perf.asked, true) {
749 return;
750 }
751 if shuyi_ui::DEBUG_TOOLS {
752 self.perf.clock = platform.clock();
753 self.perf.scopes = platform.memory_scopes();
754 }
755 self.give_clock();
756 }
757
758 fn give_clock(&mut self) {
761 let Phase::Running(game) = &mut self.phase else {
762 return;
763 };
764 let ctx = game.ctx_mut();
765 if let Some(clock) = self.perf.clock.take()
766 && ctx.perf_clock().is_none()
767 {
768 ctx.set_perf_clock(Some(clock));
769 }
770 if let Some(scopes) = self.perf.scopes.take()
771 && ctx.memory_scopes().is_none()
772 {
773 ctx.set_memory_scopes(Some(scopes));
774 }
775 }
776
777 fn memory_scopes(&self) -> Option<MemoryScopes> {
780 match &self.phase {
781 Phase::Running(game) => game.ctx().memory_scopes(),
782 Phase::Loading { .. } | Phase::Failed(_) => self.perf.scopes,
783 }
784 }
785
786 fn begin_perf(&mut self) {
789 self.perf.start = None;
790 self.perf.paint_us = None;
791 if self.suspended || self.quit || self.is_paused() {
792 return;
793 }
794 if let Phase::Running(game) = &mut self.phase
795 && game.ctx().debug_tools
796 {
797 self.perf.start = game.ctx_mut().begin_perf_frame();
798 }
799 }
800
801 fn end_perf(&mut self, platform: &mut impl Platform) {
804 let Some(start) = self.perf.start.take() else {
805 self.perf.last_end = None;
806 return;
807 };
808 let Phase::Running(game) = &mut self.phase else {
809 return;
810 };
811 let ctx = game.ctx_mut();
812 let end = ctx.perf_now();
813 let measured = ctx.end_perf_frame();
814 let (Some(end), Some(mut record), Some(paint_us)) =
815 (end, measured, self.perf.paint_us.take())
816 else {
817 self.perf.last_end = None;
820 return;
821 };
822 let present_us = self.perf.last_end.map_or(0, |last| micros(last, start));
823 record.frame = self.frames;
824 record.times.paint_us = paint_us;
825 record.times.present_us = present_us;
826 record.times.frame_us = present_us.saturating_add(micros(start, end));
827 record.heap = platform.memory().map(|memory| memory.heap);
828 self.perf.last_end = Some(end);
829 self.perf.records.push(record);
830 }
831
832 fn take_game_log(&mut self) {
835 let Phase::Running(game) = &mut self.phase else {
836 return;
837 };
838 let (lines, dropped) = game.ctx_mut().take_log();
839 self.log.add_dropped(dropped);
840 for line in lines {
841 self.log_line(line.level, line.category, line.text);
842 }
843 }
844
845 fn log_line(&mut self, level: LogLevel, category: LogCategory, text: String) {
847 if self.log_filter.allows(category, level) {
848 self.log
849 .push(LogLine::new(self.frames, level, category, text));
850 }
851 }
852
853 fn system_events(&mut self, platform: &mut impl Platform) {
856 let mut events = core::mem::take(&mut self.lists.system);
857 events.clear();
858 platform.system_events(&mut events);
859 if let Some(recorder) = &mut self.recorder {
860 recorder.system(&events);
861 }
862 for &event in &events {
863 self.on_system_event(platform, event);
864 }
865 self.lists.system = events;
866 }
867
868 fn on_system_event(&mut self, platform: &mut impl Platform, event: SystemEvent) {
869 if self.quit {
870 return;
871 }
872 match event {
873 SystemEvent::Suspend => {
874 self.suspended = true;
875 platform.audio().suspend();
876 }
877 SystemEvent::Resume => {
878 self.suspended = false;
879 self.clock.restart();
880 platform.audio().resume();
881 if let Some(game) = self.game_mut() {
884 game.set_resumed();
885 }
886 }
887 SystemEvent::FocusLost => {
888 self.focused = false;
889 if let Some(game) = self.game_mut() {
892 game.forget_held_input();
893 }
894 }
895 SystemEvent::FocusGained => {
896 if self.is_paused() {
897 self.clock.restart();
899 }
900 self.focused = true;
901 }
902 SystemEvent::QuitRequested => self.quit = true,
903 SystemEvent::ToggleFullscreen => {
904 if let Some(game) = self.game_mut() {
905 game.toggle_fullscreen();
906 }
907 }
908 SystemEvent::LowBattery => {}
909 SystemEvent::CrankDocked | SystemEvent::CrankUndocked => {
910 if let Some(game) = self.game_mut() {
911 game.ctx_mut().crank_docked = event == SystemEvent::CrankDocked;
912 }
913 }
914 }
915 if let (Some(hook), Phase::Running(game)) = (self.on_system, &mut self.phase) {
916 hook(game, event);
917 }
918 }
919
920 fn run(&mut self, platform: &mut impl Platform, elapsed: Elapsed) -> FrameOutcome {
921 self.lists.audio.clear();
922 self.lists.music.clear();
923 self.lists.sounds.clear();
924 self.ticks = 0;
925 if self.suspended && !self.quit {
926 drain(platform, &mut self.lists.input);
928 return FrameOutcome::Continue;
929 }
930 let mut loading = None;
931 let paused = self.is_paused();
932 let muted = self.is_muted();
933 match &mut self.phase {
934 Phase::Loading { card, load, shown } => {
935 if *shown && !self.quit {
936 loading = load.take();
937 } else {
938 *shown = true;
939 platform.present(&Frame::new(Show::Card(card)));
940 drain(platform, &mut self.lists.input);
941 }
942 }
943 Phase::Running(game) if paused => {
944 drain(platform, &mut self.lists.input);
947 if muted {
948 platform.audio().play(&silent(&[], &[]), elapsed.now);
949 }
950 present_tagged(platform, game, self.look);
951 }
952 Phase::Running(game) if !self.quit => {
953 let recording = self.recorder.as_ref().is_some_and(|r| r.is_on());
954 game.ctx_mut()
955 .set_session_recording(recording || self.frame_hashes.on);
956 let mut recorder = self.recorder.as_deref_mut();
957 if let Some(pacing) = self.pacing.take() {
958 game.set_pacing(pacing);
959 if let Some(recorder) = recorder.as_deref_mut() {
960 recorder.handed_pacing(pacing);
961 }
962 }
963 let told = tell_changed(platform, game, &mut self.lists.changed);
964 if told {
965 use_look_widths(game, self.look);
967 }
968 if let Some(recorder) = recorder.as_deref_mut().filter(|_| told) {
969 recorder.reloaded(self.frames);
970 }
971 step(platform, game, &mut self.lists, elapsed, muted, recorder);
972 self.ticks = elapsed.ticks;
973 if game.quit_requested() {
974 platform.audio().stop_all();
975 self.quit = true;
976 }
977 let painting = self.perf.now(game);
978 present_tagged(platform, game, self.look);
979 let painted = self.perf.now(game);
980 self.perf.paint_us = painting.zip(painted).map(|(from, to)| micros(from, to));
981 }
982 Phase::Running(game) => {
983 drain(platform, &mut self.lists.input);
985 present_tagged(platform, game, self.look);
986 }
987 Phase::Failed(errors) => {
988 platform.present(&Frame::new(Show::Errors(errors)));
989 drain(platform, &mut self.lists.input);
990 }
991 }
992 if let Some(load) = loading {
993 self.load(platform, load, elapsed.now);
994 }
995 if self.quit {
996 FrameOutcome::Quit
997 } else {
998 FrameOutcome::Continue
999 }
1000 }
1001
1002 fn load(&mut self, platform: &mut impl Platform, load: Load<G>, now: Millis) {
1006 let mut setup = Setup {
1007 display: platform.display(),
1008 storage: platform.storage(),
1009 seed: mix(now.0),
1010 voice_manifest: None,
1011 platform: platform.facts(),
1012 key_prompt: KeyPrompt::Waiting,
1013 last_crash: None,
1014 };
1015 if let Some(recorder) = &mut self.recorder {
1016 recorder.handed_over(setup.display, setup.storage.as_ref(), &setup.platform);
1017 }
1018 setup.last_crash = crash::offer_last_crash(setup.storage.as_mut());
1020 drain(platform, &mut self.lists.input);
1021 let tagged = enter(self.perf.scopes, "load");
1022 let loading = self.perf.clock.as_mut().map(PerfClock::read);
1023 let loaded = load(setup);
1024 let done = self.perf.clock.as_mut().map(PerfClock::read);
1025 leave(tagged);
1026 self.perf.load_us = loading.zip(done).map(|(from, to)| micros(from, to));
1027 match loaded {
1028 Ok(mut game) => {
1029 use_look_widths(&mut game, self.look);
1030 present_game(platform, &game, self.look);
1031 self.phase = Phase::Running(Box::new(game));
1032 self.give_clock();
1033 }
1034 Err(errors) => {
1035 let (level, category) = (LogLevel::Error, LogCategory::Platform);
1036 self.log_line(level, category, format!("the game didn't load: {errors}"));
1037 platform.present(&Frame::new(Show::Errors(&errors)));
1038 self.phase = Phase::Failed(errors);
1039 }
1040 }
1041 self.clock.restart();
1043 }
1044
1045 pub fn record(&mut self) -> bool {
1051 if self.frames > 0 {
1052 return false;
1053 }
1054 let made = self.made.clone();
1055 self.recorder
1056 .get_or_insert_with(|| Box::new(Recorder::new(made)));
1057 true
1058 }
1059
1060 pub fn session(&self) -> Option<&SessionRecord> {
1062 self.recorder.as_ref().map(|recorder| &recorder.record)
1063 }
1064
1065 pub fn session_mut(&mut self) -> Option<&mut SessionRecord> {
1068 self.recorder.as_mut().map(|recorder| &mut recorder.record)
1069 }
1070
1071 pub fn take_session(&mut self) -> Option<SessionRecord> {
1073 self.recorder.take().map(|recorder| recorder.record)
1074 }
1075
1076 pub fn take_session_save(&mut self) -> bool {
1080 self.game_mut()
1081 .is_some_and(|game| game.ctx_mut().take_session_save())
1082 }
1083
1084 pub fn frame_hash(&self) -> Option<StateHash> {
1088 self.frame_hashes.last
1089 }
1090
1091 pub fn hash_frames(&mut self, on: bool) {
1094 self.frame_hashes.on = on;
1095 }
1096
1097 pub fn game(&self) -> Option<&Game<G>> {
1099 match &self.phase {
1100 Phase::Running(game) => Some(game),
1101 Phase::Loading { .. } | Phase::Failed(_) => None,
1102 }
1103 }
1104
1105 pub fn game_mut(&mut self) -> Option<&mut Game<G>> {
1108 match &mut self.phase {
1109 Phase::Running(game) => Some(game),
1110 Phase::Loading { .. } | Phase::Failed(_) => None,
1111 }
1112 }
1113
1114 pub fn into_game(self) -> Option<Game<G>> {
1116 match self.phase {
1117 Phase::Running(game) => Some(*game),
1118 Phase::Loading { .. } | Phase::Failed(_) => None,
1119 }
1120 }
1121
1122 pub fn crash_record(&self) -> Option<String> {
1126 (self.crash_record?)(self.game()?)
1127 }
1128
1129 pub fn is_loading(&self) -> bool {
1131 matches!(self.phase, Phase::Loading { .. })
1132 }
1133
1134 pub fn errors(&self) -> Option<&str> {
1136 match &self.phase {
1137 Phase::Failed(errors) => Some(errors),
1138 Phase::Loading { .. } | Phase::Running(_) => None,
1139 }
1140 }
1141
1142 pub fn is_suspended(&self) -> bool {
1144 self.suspended
1145 }
1146
1147 pub fn ticks(&self) -> u32 {
1151 self.ticks
1152 }
1153
1154 pub fn is_focused(&self) -> bool {
1157 self.focused
1158 }
1159
1160 pub fn is_paused(&self) -> bool {
1164 !self.focused
1165 && self.background.pause
1166 && !self.quit
1167 && matches!(self.phase, Phase::Running(_))
1168 }
1169
1170 fn is_muted(&self) -> bool {
1173 !self.focused && self.background.audio == BackgroundAudio::Mute
1174 }
1175
1176 pub fn has_quit(&self) -> bool {
1178 self.quit
1179 }
1180
1181 pub fn frame_number(&self) -> u32 {
1185 self.frames
1186 }
1187
1188 pub fn set_log_filter(&mut self, filter: LogFilter) {
1191 self.log_filter = filter;
1192 }
1193
1194 pub fn log(&self) -> &LogRing {
1197 &self.log
1198 }
1199
1200 pub fn take_new_log(&mut self) -> Vec<LogLine> {
1203 let lines = self.log.since(self.log_read).cloned().collect();
1204 self.log_read = self.log.next_seq();
1205 lines
1206 }
1207
1208 pub fn perf(&self) -> &PerfRing {
1214 &self.perf.records
1215 }
1216
1217 pub fn take_new_perf(&mut self) -> Vec<FrameRecord> {
1220 let records = self.perf.records.since(self.perf.read).cloned().collect();
1221 self.perf.read = self.perf.records.next_seq();
1222 records
1223 }
1224
1225 pub fn load_us(&self) -> Option<u32> {
1228 self.perf.load_us
1229 }
1230
1231 pub fn heap_snapshot(&self, platform: &mut impl Platform) -> Option<HeapSnapshot> {
1237 let on = match &self.phase {
1238 Phase::Running(game) => game.ctx().debug_tools,
1239 Phase::Loading { .. } | Phase::Failed(_) => true,
1240 };
1241 if !(shuyi_ui::DEBUG_TOOLS && on) {
1242 return None;
1243 }
1244 let mut snapshot = platform.heap_snapshot()?;
1245 snapshot.frame = self.frames;
1246 Some(snapshot)
1247 }
1248
1249 pub fn audio(&self) -> &[AudioRequest<G::Voice>] {
1252 &self.lists.audio
1253 }
1254
1255 pub fn music(&self) -> &[MusicCommand] {
1257 &self.lists.music
1258 }
1259}
1260
1261fn drain(platform: &mut impl Platform, input: &mut Vec<RawInputEvent>) {
1264 input.clear();
1265 platform.poll_input(input);
1266}
1267
1268fn use_look_widths<G: GameData>(game: &mut Game<G>, look: Option<LookOf<G>>) {
1272 if let Some(look) = look {
1273 let widths = CellWidths::of(look(&game.ctx().game).font);
1274 game.set_widths(widths);
1275 }
1276}
1277
1278fn tell_changed<G: GameData>(
1282 platform: &mut impl Platform,
1283 game: &mut Game<G>,
1284 changed: &mut Vec<String>,
1285) -> bool {
1286 if !(shuyi_ui::DEBUG_TOOLS && game.ctx().debug_tools) {
1287 return false;
1288 }
1289 changed.clear();
1290 platform.changed_files(changed);
1291 let told = !changed.is_empty();
1292 if told {
1293 game.reloaded(changed);
1294 }
1295 told
1296}
1297
1298fn silent<'a>(requests: &'a [SoundRequest], music: &'a [MusicCommand]) -> AudioFrame<'a> {
1300 AudioFrame {
1301 requests,
1302 music,
1303 music_volume: 0.0,
1304 sound_volume: 0.0,
1305 voice_volume: 0.0,
1306 }
1307}
1308
1309fn step<G: GameData>(
1314 platform: &mut impl Platform,
1315 game: &mut Game<G>,
1316 lists: &mut Lists<G::Voice>,
1317 elapsed: Elapsed,
1318 muted: bool,
1319 mut recorder: Option<&mut Recorder>,
1320) {
1321 lists.input.clear();
1322 platform.poll_input(&mut lists.input);
1323 let keyboard = &mut game.ctx_mut().system_keyboard;
1324 let text_input = platform.text_input();
1325 let present = text_input.is_some();
1326 keyboard.set_present(present);
1327 if let Some(recorder) = recorder.as_deref_mut() {
1328 recorder.keyboard_present(present);
1329 }
1330 if keyboard.is_waiting()
1331 && let Some(answer) = text_input.and_then(TextInput::poll)
1332 {
1333 if let Some(recorder) = recorder.as_deref_mut() {
1334 recorder.keyboard_answer(&answer);
1335 }
1336 keyboard.answer(answer);
1337 }
1338 let playing = platform.audio().music_playing(elapsed.now);
1339 if let Some(recorder) = recorder.as_deref_mut() {
1340 recorder.music(playing);
1341 }
1342 game.set_music_playing(playing);
1343 let voice = platform.audio().voice_playing(elapsed.now);
1344 if let Some(recorder) = recorder.as_deref_mut() {
1345 recorder.voice(voice);
1346 }
1347 let asked = lists.voice.as_ref();
1348 let line = asked.filter(|(_, file)| voice == Some(file.as_str()));
1349 game.set_voice_playing(line.map(|(line, _)| line));
1350 game.set_ticks(elapsed.ticks);
1351 let out = game.frame(&lists.input, elapsed.dt);
1352 lists.audio.extend_from_slice(out.audio);
1353 for request in out.audio {
1354 if let AudioRequest::PlayVoice { line, variant } = request {
1355 lists.voice = Some((line.clone(), clip_file(line, *variant)));
1356 }
1357 }
1358 lists.music.extend_from_slice(out.music);
1359 let (music_volume, sound_volume, voice_volume) =
1360 (out.music_volume, out.sound_volume, out.voice_volume);
1361 lists.sounds.clear();
1362 let sounds = lists.audio.iter().filter_map(SoundRequest::from_request);
1363 lists.sounds.extend(sounds);
1364 let audio = if muted {
1365 silent(&lists.sounds, &lists.music)
1366 } else {
1367 AudioFrame {
1368 requests: &lists.sounds,
1369 music: &lists.music,
1370 music_volume,
1371 sound_volume,
1372 voice_volume,
1373 }
1374 };
1375 platform.audio().play(&audio, elapsed.now);
1376 let keyboard = &mut game.ctx_mut().system_keyboard;
1377 if let Some(ask) = keyboard.take_ask() {
1378 let opened = platform.text_input().is_some_and(|input| input.open(&ask));
1379 if let Some(recorder) = recorder {
1380 recorder.keyboard_opened(opened);
1381 }
1382 if !opened {
1383 keyboard.answer(TextAnswer::Cancelled);
1384 }
1385 }
1386}
1387
1388fn present_game<G: GameData>(
1391 platform: &mut impl Platform,
1392 game: &Game<G>,
1393 look: Option<LookOf<G>>,
1394) {
1395 let ctx = game.ctx();
1396 let show = Show::Game {
1397 buffer: game.buffer(),
1398 background: ctx.game.blank_cell(&ctx.palette).bg,
1399 look: look.map(|look| look(&ctx.game)),
1400 };
1401 let mut frame = Frame::new(show);
1402 frame.fullscreen = ctx.settings().fullscreen;
1403 frame.frame_cap = ctx.settings().frame_cap;
1404 frame.frame_cap_serial = ctx.frame_cap_serial();
1405 frame.reload_errors = ctx.reload_errors();
1406 platform.present(&frame);
1407}
1408
1409fn present_tagged<G: GameData>(
1411 platform: &mut impl Platform,
1412 game: &Game<G>,
1413 look: Option<LookOf<G>>,
1414) {
1415 let tagged = enter(game.ctx().memory_scopes(), "paint");
1416 present_game(platform, game, look);
1417 leave(tagged);
1418}
1419
1420fn mix(x: u64) -> u64 {
1423 let mut z = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
1424 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1425 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
1426 z ^ (z >> 31)
1427}
1428
1429#[cfg(test)]
1430mod tests;