1use crate::alloc_prelude::*;
26use alloc::collections::BTreeMap;
27use alloc::format;
28use core::fmt;
29
30use ron::ser::PrettyConfig;
31use serde::{Deserialize, Serialize};
32use shuyi_core::StateHash;
33use shuyi_core::made::{Made, Mismatch};
34use shuyi_ui::frame_cap::Pacing;
35use shuyi_ui::storage::Storage;
36use shuyi_ui::system_keyboard::TextAnswer;
37use shuyi_ui::{GameData, PlatformFacts, RawInputEvent};
38
39use crate::app::{App, Start};
40use crate::headless::HeadlessPlatform;
41use crate::platform::{DisplayInfo, Millis, SystemEvent};
42
43pub const FORMAT_VERSION: u32 = 1;
45
46pub const MAX_SESSION_FRAMES: usize = 216_000;
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54#[serde(deny_unknown_fields)]
55#[non_exhaustive]
56pub struct SessionRecord {
57 pub format_version: u32,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub made: Option<Made>,
62 pub display: DisplayInfo,
64 pub platform: PlatformFacts,
67 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
69 pub storage: BTreeMap<String, String>,
70 pub storage_complete: bool,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub voice_manifest: Option<String>,
78 pub start: u64,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub reloaded_at: Option<u32>,
85 #[serde(default, skip_serializing_if = "core::ops::Not::not")]
88 pub truncated: bool,
89 pub frames: Vec<FrameRecord>,
91}
92
93impl SessionRecord {
94 pub fn new() -> Self {
96 Self {
97 format_version: FORMAT_VERSION,
98 made: None,
99 display: DisplayInfo::new(0, 0),
100 platform: PlatformFacts::default(),
101 storage: BTreeMap::new(),
102 storage_complete: false,
103 voice_manifest: None,
104 start: 0,
105 reloaded_at: None,
106 truncated: false,
107 frames: Vec::new(),
108 }
109 }
110
111 pub fn to_ron(&self) -> String {
113 let config = PrettyConfig::new()
117 .depth_limit(2)
118 .indentor(" ")
119 .new_line("\n");
120 ron::ser::to_string_pretty(self, config)
121 .unwrap_or_else(|error| panic!("a session record has no value RON refuses: {error}"))
122 }
123
124 pub fn from_ron(text: &str) -> Result<Self, SessionError> {
127 #[derive(Deserialize)]
129 struct Head {
130 format_version: u32,
131 }
132
133 let refused = |version| Err(SessionError::Version(version));
134 match ron::from_str::<Self>(text) {
135 Ok(record) if record.format_version == FORMAT_VERSION => Ok(record),
136 Ok(record) => refused(record.format_version),
137 Err(error) => match ron::from_str::<Head>(text) {
138 Ok(head) if head.format_version != FORMAT_VERSION => refused(head.format_version),
139 _ => Err(SessionError::Text(error.to_string())),
140 },
141 }
142 }
143
144 pub fn limits(&self) -> Vec<String> {
148 let mut limits = Vec::new();
149 if self.truncated {
150 limits.push("the record stopped early: it doesn't hold the run's end".to_owned());
151 }
152 if !self.storage_complete {
153 limits.push("the record doesn't hold all of the store".to_owned());
154 }
155 if let Some(frame) = self.reloaded_at {
156 limits.push(format!(
157 "files changed at frame {frame}: their new bytes aren't in the record, so frames after it may differ"
158 ));
159 }
160 limits
161 }
162}
163
164impl Default for SessionRecord {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
174#[serde(deny_unknown_fields)]
175#[non_exhaustive]
176pub struct FrameRecord {
177 pub dt: i64,
181 #[serde(default, skip_serializing_if = "Vec::is_empty")]
183 pub system: Vec<SystemEvent>,
184 #[serde(default, skip_serializing_if = "Vec::is_empty")]
186 pub input: Vec<RawInputEvent>,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub keyboard: Option<KeyboardFrame>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub music: Option<f64>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub track: Option<String>,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub voice: Option<String>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub pacing: Option<Pacing>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub hash: Option<u64>,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
214#[serde(deny_unknown_fields)]
215#[non_exhaustive]
216pub struct KeyboardFrame {
217 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub present: Option<bool>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub answer: Option<TextAnswer>,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub opened: Option<bool>,
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
231#[non_exhaustive]
232pub enum SessionError {
233 Text(String),
235 Version(u32),
237 Made(Mismatch),
239}
240
241impl fmt::Display for SessionError {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 match self {
244 Self::Text(text) => write!(f, "a session record that can't be read: {text}"),
245 Self::Version(version) => write!(
246 f,
247 "a session record of version {version}; this engine reads version {FORMAT_VERSION}"
248 ),
249 Self::Made(mismatch) => write!(f, "{mismatch}"),
250 }
251 }
252}
253
254impl core::error::Error for SessionError {}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
258pub enum Stamps {
259 #[default]
261 Check,
262 Ignore,
264}
265
266pub fn prepare<G: GameData>(
271 start: Start<G>,
272 record: &SessionRecord,
273 stamps: Stamps,
274) -> Result<(Start<G>, Vec<String>), SessionError> {
275 if record.format_version != FORMAT_VERSION {
276 return Err(SessionError::Version(record.format_version));
277 }
278 let mut limits = record.limits();
279 match (&record.made, &start.made) {
280 (Some(made), Some(now)) => {
281 if stamps == Stamps::Check {
282 made.check(now).map_err(SessionError::Made)?;
283 }
284 }
285 (None, _) => limits.push("the record has no stamp: its build isn't checked".to_owned()),
286 (_, None) => limits.push("the start has no stamp: the build isn't checked".to_owned()),
287 }
288 if stamps == Stamps::Ignore {
289 limits.push("the stamps were ignored: another build may play differently".to_owned());
290 }
291 let start = match record.voice_manifest.clone() {
292 Some(manifest) => start.before_load(move |setup| setup.voice_manifest = Some(manifest)),
293 None => start,
294 };
295 Ok((start, limits))
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300#[non_exhaustive]
301pub struct FrameDifference {
302 pub frame: u32,
304 pub recorded: StateHash,
306 pub played: StateHash,
308}
309
310#[derive(Debug, Clone, PartialEq, Eq, Default)]
312#[non_exhaustive]
313pub struct Played {
314 pub frames: u32,
316 pub of: u32,
318 pub first_difference: Option<FrameDifference>,
320 pub limits: Vec<String>,
323}
324
325#[derive(Debug, Clone)]
330pub struct Playback {
331 frames: alloc::vec::IntoIter<FrameRecord>,
333 now: u64,
335 hash: Option<u64>,
337}
338
339impl Playback {
340 pub fn new(record: &SessionRecord) -> Self {
342 Self {
343 frames: record.frames.clone().into_iter(),
344 now: record.start,
345 hash: None,
346 }
347 }
348
349 pub fn next<G: GameData>(
354 &mut self,
355 app: &mut App<G>,
356 platform: &mut HeadlessPlatform,
357 ) -> Option<Result<u32, FrameDifference>> {
358 let frame = self.frames.next()?;
359 app.hash_frames(true);
360 platform.push_frame(&frame);
361 if let Some(pacing) = frame.pacing {
362 app.set_pacing(pacing);
363 }
364 self.now = self.now.wrapping_add_signed(frame.dt);
365 self.hash = frame.hash.or(self.hash);
366 let _ = app.frame(platform, Millis(self.now));
367 if self.is_done() {
368 platform.end_session();
369 }
370 let number = app.frame_number();
371 let (Some(recorded), Some(played)) = (self.hash.map(StateHash), app.frame_hash()) else {
372 return Some(Ok(number));
373 };
374 Some(if recorded == played {
375 Ok(number)
376 } else {
377 Err(FrameDifference {
378 frame: number,
379 recorded,
380 played,
381 })
382 })
383 }
384
385 pub fn is_done(&self) -> bool {
387 self.frames.as_slice().is_empty()
388 }
389
390 pub fn now(&self) -> Millis {
393 Millis(self.now)
394 }
395}
396
397#[derive(Debug)]
400pub(crate) struct Recorder {
401 pub(crate) record: SessionRecord,
402 on: bool,
404 frame: FrameRecord,
405 now: u64,
407 present: bool,
408 track: String,
409 voice: String,
410 pacing: Option<Pacing>,
411 hash: Option<u64>,
412}
413
414impl Recorder {
415 pub(crate) fn new(made: Option<Made>) -> Self {
416 let mut record = SessionRecord::new();
417 record.made = made;
418 Self {
419 record,
420 on: true,
421 frame: FrameRecord::default(),
422 now: 0,
423 present: false,
424 track: String::new(),
425 voice: String::new(),
426 pacing: None,
427 hash: None,
428 }
429 }
430
431 pub(crate) fn is_on(&self) -> bool {
432 self.on
433 }
434
435 pub(crate) fn stop(&mut self) {
437 if self.on {
438 self.on = false;
439 self.record.truncated = true;
440 }
441 }
442
443 pub(crate) fn begin(&mut self, now: u64) {
445 if self.record.frames.len() >= MAX_SESSION_FRAMES {
446 self.stop();
447 }
448 if !self.on {
449 return;
450 }
451 self.frame = FrameRecord::default();
452 if self.record.frames.is_empty() {
453 self.record.start = now;
454 } else {
455 self.frame.dt = now.wrapping_sub(self.now).cast_signed();
456 }
457 self.now = now;
458 }
459
460 pub(crate) fn system(&mut self, events: &[SystemEvent]) {
462 if self.on {
463 self.frame.system.extend_from_slice(events);
464 }
465 }
466
467 pub(crate) fn handed_over(
469 &mut self,
470 display: DisplayInfo,
471 storage: &dyn Storage,
472 facts: &PlatformFacts,
473 ) {
474 if self.on {
475 self.record.display = display;
476 self.record.platform = facts.clone();
477 self.record.storage_complete = read_store(storage, &mut self.record.storage);
478 }
479 }
480
481 pub(crate) fn reloaded(&mut self, frame: u32) {
483 if self.on {
484 self.record.reloaded_at.get_or_insert(frame);
485 }
486 }
487
488 pub(crate) fn handed_pacing(&mut self, pacing: Pacing) {
490 if self.on && self.pacing != Some(pacing) {
491 self.pacing = Some(pacing);
492 self.frame.pacing = Some(pacing);
493 }
494 }
495
496 fn keyboard(&mut self) -> &mut KeyboardFrame {
497 self.frame.keyboard.get_or_insert_default()
498 }
499
500 pub(crate) fn keyboard_present(&mut self, present: bool) {
502 if self.on && self.present != present {
503 self.present = present;
504 self.keyboard().present = Some(present);
505 }
506 }
507
508 pub(crate) fn keyboard_answer(&mut self, answer: &TextAnswer) {
510 if self.on {
511 self.keyboard().answer = Some(answer.clone());
512 }
513 }
514
515 pub(crate) fn keyboard_opened(&mut self, opened: bool) {
517 if self.on {
518 self.keyboard().opened = Some(opened);
519 }
520 }
521
522 pub(crate) fn music(&mut self, playing: Option<(&str, f64)>) {
524 if !self.on {
525 return;
526 }
527 let cue = playing.map_or("", |(cue, _)| cue);
528 if cue != self.track {
529 cue.clone_into(&mut self.track);
530 self.frame.track = Some(cue.to_owned());
531 }
532 self.frame.music = playing.map(|(_, position)| position);
533 }
534
535 pub(crate) fn voice(&mut self, playing: Option<&str>) {
537 let file = playing.unwrap_or("");
538 if self.on && file != self.voice {
539 file.clone_into(&mut self.voice);
540 self.frame.voice = Some(file.to_owned());
541 }
542 }
543
544 pub(crate) fn end(&mut self, input: &[RawInputEvent], hash: StateHash) {
546 if !self.on {
547 return;
548 }
549 let mut frame = core::mem::take(&mut self.frame);
550 frame.input.extend_from_slice(input);
551 if self.hash != Some(hash.0) {
552 self.hash = Some(hash.0);
553 frame.hash = Some(hash.0);
554 }
555 self.record.frames.push(frame);
556 }
557}
558
559fn read_store(storage: &dyn Storage, into: &mut BTreeMap<String, String>) -> bool {
561 let Ok(keys) = storage.list() else {
562 return false;
563 };
564 let mut complete = true;
565 for key in keys {
566 if let Ok(Some(value)) = storage.read(&key) {
567 into.insert(key, value);
568 } else {
569 complete = false;
570 }
571 }
572 complete
573}
574
575#[cfg(test)]
576mod tests;