Skip to main content

shuyi_platform/
link.rs

1//! The live link (ADR-0047): how a tool reaches a build that is already
2//! running, in a debug build with the backend's `link` feature. The wire is
3//! JSON lines, one UTF-8 JSON object per line: the client sends a
4//! [`Request`] per line, and the build answers each with an [`Answer`], in
5//! order. The first line must be a [`Request::Hello`] with the build's
6//! token, or the connection is closed.
7//!
8//! This module touches no device. A backend carries the lines (TCP on
9//! loopback in `shuyi-app`) and the [`Server`] turns each into its answer
10//! over the [`App`] it runs: it reads the screens, the frame as text, the
11//! window's picture (taken by the backend), the log, and the frames'
12//! records with a heap snapshot (taken by the backend), and its only input
13//! is raw device events the backend adds after the devices' own in
14//! [`Platform::poll_input`](crate::Platform::poll_input), so they go where a
15//! player's go. It changes nothing any other way.
16//!
17//! `shuyi attach` is the client, and shares these types with the build, so
18//! the two ends can't drift; [`VERSION`] says which wire they speak.
19
20pub mod base64;
21
22use crate::alloc_prelude::*;
23use alloc::collections::VecDeque;
24use alloc::format;
25
26use serde::{Deserialize, Serialize};
27use shuyi_ui::input::PadKind;
28use shuyi_ui::log::LogRing;
29use shuyi_ui::perf::{FrameRecord, HeapSnapshot, PERF_FRAMES, PerfRing};
30use shuyi_ui::{GameData, RawInputEvent};
31
32use crate::app::App;
33use crate::script::{self, Planned, Step};
34
35/// The link's version: a client and a build speak the same, or the build
36/// closes the link after its hello.
37pub const VERSION: u32 = 3;
38
39/// The longest request line, in bytes: a longer one closes the link.
40pub const MAX_LINE: usize = 64 * 1024;
41
42/// The most bytes of answers a client may leave unread: past it, the
43/// backend drops the client.
44pub const MAX_UNSENT: usize = 64 * 1024 * 1024;
45
46/// The longest [`Step::Wait`] of a play, in seconds: half an hour, played
47/// as the build's frames at 60 a second ([`script::wait_frames`]).
48pub const MAX_WAIT: f32 = 1_800.0;
49
50/// How many records a [`Request::Perf`] answers when it doesn't say.
51pub const PERF_MAX: u32 = 120;
52
53/// A line a client sends, tagged by `op`.
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55#[serde(tag = "op", rename_all = "snake_case", deny_unknown_fields)]
56#[non_exhaustive]
57pub enum Request {
58    /// The first line: the build's token, and the link's version the
59    /// client speaks.
60    Hello {
61        /// The token the build logged when it started listening.
62        token: String,
63        /// The client's [`VERSION`].
64        link: u32,
65    },
66    /// Where the build is: [`Status`].
67    Status,
68    /// The frame as text, in the snapshot format.
69    Frame,
70    /// The window's picture, as a PNG, after the next frame is shown.
71    Screenshot,
72    /// Steps played as a player's input, frame by frame: answered when
73    /// the last has run. A wait's seconds are the build's frames at 60 a
74    /// second ([`script::wait_frames`]); a crank isn't played over the
75    /// link yet.
76    Play {
77        /// The steps, in order.
78        steps: Vec<Step>,
79    },
80    /// The log's kept lines numbered `since` or more.
81    Log {
82        /// The first line's number: 0, the default, for every kept line;
83        /// the last answer's `next` for the lines since.
84        #[serde(default)]
85        since: u64,
86    },
87    /// The frames' records numbered `since` or more ([`App::perf`]), the
88    /// first `max` of them, and a heap snapshot after this frame if asked.
89    Perf {
90        /// The first record's number: 0, the default, for every kept
91        /// record; the last answer's `next` for the records since.
92        #[serde(default)]
93        since: u64,
94        /// The most records to answer, 1 to [`PERF_FRAMES`];
95        /// [`PERF_MAX`] by default.
96        #[serde(default = "perf_max")]
97        max: u32,
98        /// Whether the backend takes a heap snapshot after this frame
99        /// ([`App::heap_snapshot`]) and answers it with the records.
100        #[serde(default)]
101        snapshot: bool,
102    },
103}
104
105/// [`Request::Perf`]'s `max` when it doesn't say.
106fn perf_max() -> u32 {
107    PERF_MAX
108}
109
110impl Request {
111    /// The request as one line, with no line break.
112    pub fn to_line(&self) -> String {
113        // An enum of strings and numbers: serializing can't fail.
114        serde_json::to_string(self).unwrap_or_default()
115    }
116}
117
118/// Which part of its life the game is in.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "snake_case")]
121#[non_exhaustive]
122pub enum Phase {
123    /// Its loading card shows.
124    Loading,
125    /// It runs.
126    Running,
127    /// Its load failed: the errors show.
128    Failed,
129}
130
131/// Where a running build is.
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(deny_unknown_fields)]
134// An answer, read field by field: it may grow (ADR-0039).
135#[non_exhaustive]
136pub struct Status {
137    /// The frames run: [`App::frame_number`].
138    pub frames: u32,
139    /// Loading, running or failed.
140    pub phase: Phase,
141    /// Why the game didn't load, if it failed.
142    pub errors: Option<String>,
143    /// The open screens, bottom first.
144    pub screens: Vec<String>,
145    /// The top screen's name; empty with none open.
146    pub top_screen: String,
147    /// Whether the system has suspended the game.
148    pub suspended: bool,
149    /// Whether its window has the player's attention.
150    pub focused: bool,
151    /// Whether the game has quit.
152    pub quit: bool,
153}
154
155impl Status {
156    /// Where `app` is now.
157    pub fn of<G: GameData>(app: &App<G>) -> Self {
158        let phase = if app.is_loading() {
159            Phase::Loading
160        } else if app.errors().is_some() {
161            Phase::Failed
162        } else {
163            Phase::Running
164        };
165        let screens: Vec<String> = app
166            .game()
167            .map(|game| game.screens().into_iter().map(str::to_owned).collect())
168            .unwrap_or_default();
169        Self {
170            frames: app.frame_number(),
171            phase,
172            errors: app.errors().map(str::to_owned),
173            top_screen: screens.last().cloned().unwrap_or_default(),
174            screens,
175            suspended: app.is_suspended(),
176            focused: app.is_focused(),
177            quit: app.has_quit(),
178        }
179    }
180}
181
182/// The window's picture.
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(deny_unknown_fields)]
185#[non_exhaustive]
186pub struct Screenshot {
187    /// The PNG file, in [`base64`].
188    pub png: String,
189    /// Its width in pixels.
190    pub width: u32,
191    /// Its height in pixels.
192    pub height: u32,
193    /// What it doesn't show: the loading card or the errors instead of
194    /// the game.
195    pub limit: Option<String>,
196}
197
198/// What a [`Request::Play`] did.
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200#[serde(deny_unknown_fields)]
201#[non_exhaustive]
202pub struct Played {
203    /// The frames its steps took.
204    pub frames: u32,
205    /// Whether the steps waited for the game to load.
206    pub waited_loading: bool,
207    /// Whether the steps waited while the game was suspended.
208    pub waited_suspended: bool,
209}
210
211/// One line of the log, with its number in the ring.
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213#[serde(deny_unknown_fields)]
214#[non_exhaustive]
215pub struct LogEntry {
216    /// Its number: lines are numbered from 0 as they are logged.
217    pub seq: u64,
218    /// The frame it was given in.
219    pub frame: u32,
220    /// How serious it is: `error`, `warn`, `info` or `debug`.
221    pub level: String,
222    /// What it is about (`assets`, `platform`, `game`, …).
223    pub category: String,
224    /// What it says.
225    pub text: String,
226}
227
228/// The log's lines a [`Request::Log`] asked for.
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
230#[serde(deny_unknown_fields)]
231#[non_exhaustive]
232pub struct LogAnswer {
233    /// The kept lines numbered `since` or more, oldest first.
234    pub lines: Vec<LogEntry>,
235    /// The number the next line gets: the next request's `since`.
236    pub next: u64,
237    /// How many lines numbered `since` or more the ring no longer keeps.
238    pub dropped: u64,
239}
240
241/// The frames' records a [`Request::Perf`] asked for, counted as the
242/// contract's `PerfRead` counts them.
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(deny_unknown_fields)]
245#[non_exhaustive]
246pub struct PerfAnswer {
247    /// The kept records numbered `since` or more, oldest first, the first
248    /// `max` of them.
249    pub records: Vec<FrameRecord>,
250    /// The number to read from next time: the one after the last record
251    /// answered; 0 for a `since` past the ring's next number.
252    pub next: u64,
253    /// How many records numbered `since` or more the ring no longer keeps.
254    pub dropped: u64,
255    /// Whether any frame was measured.
256    pub measured: bool,
257    /// The frames run: [`App::frame_number`].
258    pub frames: u32,
259    /// The heap snapshot taken after the frame, if one was asked for and
260    /// the build takes one (a debug build that counts the heap by tag).
261    pub snapshot: Option<HeapSnapshot>,
262}
263
264/// A line the build sends, tagged by `answer`.
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266#[serde(tag = "answer", rename_all = "snake_case", deny_unknown_fields)]
267#[non_exhaustive]
268pub enum Answer {
269    /// The hello's answer: the build's link version (a client of another
270    /// is closed after it) and its game, the window's title.
271    Hello {
272        /// The build's [`VERSION`].
273        link: u32,
274        /// The game: the window's title.
275        game: String,
276    },
277    /// [`Request::Status`]'s.
278    Status(Status),
279    /// [`Request::Frame`]'s: the frame in the snapshot format, empty
280    /// while the game isn't running, with a limit saying why.
281    Frame {
282        /// The frame as text.
283        frame: String,
284        /// Why there is none.
285        limit: Option<String>,
286    },
287    /// [`Request::Screenshot`]'s.
288    Screenshot(Screenshot),
289    /// [`Request::Play`]'s, once its last frame has run.
290    Played(Played),
291    /// [`Request::Log`]'s.
292    Log(LogAnswer),
293    /// [`Request::Perf`]'s.
294    Perf(PerfAnswer),
295    /// A request that couldn't be answered: why.
296    Error {
297        /// What was wrong.
298        error: String,
299    },
300    /// Another client holds the link: this one is closed after this line.
301    Busy,
302}
303
304impl Answer {
305    /// The answer as one line, with no line break.
306    pub fn to_line(&self) -> String {
307        // Strings, numbers and lists: serializing can't fail.
308        serde_json::to_string(self).unwrap_or_default()
309    }
310
311    /// The answer `line` holds.
312    pub fn from_line(line: &str) -> Result<Self, String> {
313        serde_json::from_str(line.trim_end()).map_err(|e| format!("not a link answer: {e}"))
314    }
315
316    fn error(error: impl Into<String>) -> String {
317        Self::Error {
318            error: error.into(),
319        }
320        .to_line()
321    }
322}
323
324/// What the [`Server`] made of a line.
325#[derive(Debug, Clone, PartialEq, Eq)]
326#[non_exhaustive]
327pub enum Handled {
328    /// Its answer, to send now.
329    Answer(String),
330    /// Its answer comes from [`Server::after_frame`]: a play's when its
331    /// last frame has run, or any request's behind a play or a picture.
332    Later,
333    /// The backend answers it after this frame is shown, with
334    /// [`Server::screenshot`].
335    Screenshot,
336    /// The backend answers it after this frame, with a heap snapshot
337    /// ([`App::heap_snapshot`]) given to [`Server::snapshot`].
338    Snapshot,
339    /// Close the connection, after sending this line if there is one,
340    /// and read nothing more: the first line wasn't a hello with the
341    /// token, a line was longer than [`MAX_LINE`], or the client speaks
342    /// another link version (the line is the hello's answer, saying the
343    /// build's).
344    Close(Option<String>),
345}
346
347/// A play being played.
348#[derive(Debug)]
349struct Play {
350    steps: Vec<Step>,
351    /// Its frames, once its actions are looked up (the game has loaded).
352    frames: Option<VecDeque<Planned>>,
353    /// The frames given so far.
354    run: u32,
355    waited_loading: bool,
356    waited_suspended: bool,
357}
358
359/// What the server is busy with: later requests wait for it.
360#[derive(Debug)]
361enum Busy {
362    Idle,
363    Play(Play),
364    /// A picture the backend takes after this frame is shown.
365    Screenshot {
366        limit: Option<String>,
367    },
368    /// A heap snapshot the backend takes after this frame, answered with
369    /// the records this asks for.
370    Snapshot {
371        since: u64,
372        max: u32,
373    },
374}
375
376/// The build's end of the link, for one client: it turns the client's
377/// lines into answers over the [`App`] the backend runs, and gives the
378/// backend a play's input frame by frame. The backend makes one per
379/// connection.
380///
381/// Each frame the backend calls [`handle`](Self::handle) for each line
382/// read and sends the answers; [`input`](Self::input) before
383/// [`App::frame`], adding what it gives after the devices' input; then
384/// [`after_frame`](Self::after_frame), sending what it gives; if
385/// [`wants_screenshot`](Self::wants_screenshot), reads the window and
386/// sends [`screenshot`](Self::screenshot)'s line; and if
387/// [`wants_snapshot`](Self::wants_snapshot), takes a heap snapshot and
388/// sends [`snapshot`](Self::snapshot)'s.
389#[derive(Debug)]
390pub struct Server {
391    token: String,
392    game: String,
393    hello: bool,
394    busy: Busy,
395    /// The request lines behind a play or a picture, in order.
396    waiting: VecDeque<String>,
397    /// The answers [`after_frame`](Self::after_frame) gives.
398    due: Vec<String>,
399}
400
401impl Server {
402    /// The server of a build whose token is `token`, of the game `game`
403    /// (the window's title).
404    pub fn new(token: String, game: String) -> Self {
405        Self {
406            token,
407            game,
408            hello: false,
409            busy: Busy::Idle,
410            waiting: VecDeque::new(),
411            due: Vec::new(),
412        }
413    }
414
415    /// What `line` (without its line break) asks of `app`.
416    pub fn handle<G: GameData>(&mut self, line: &str, app: &App<G>) -> Handled {
417        if line.len() > MAX_LINE {
418            return Handled::Close(None);
419        }
420        if !self.hello {
421            return self.hello(line);
422        }
423        if !matches!(self.busy, Busy::Idle) || !self.waiting.is_empty() {
424            self.waiting.push_back(line.to_owned());
425            return Handled::Later;
426        }
427        self.request(line, app)
428    }
429
430    /// The first line: a hello with the token and this link's version.
431    fn hello(&mut self, line: &str) -> Handled {
432        let Ok(Request::Hello { token, link }) = serde_json::from_str::<Request>(line) else {
433            return Handled::Close(None);
434        };
435        if token != self.token {
436            return Handled::Close(None);
437        }
438        let answer = Answer::Hello {
439            link: VERSION,
440            game: self.game.clone(),
441        }
442        .to_line();
443        if link != VERSION {
444            return Handled::Close(Some(answer));
445        }
446        self.hello = true;
447        Handled::Answer(answer)
448    }
449
450    /// A request after the hello, with nothing before it waiting.
451    fn request<G: GameData>(&mut self, line: &str, app: &App<G>) -> Handled {
452        let request = match serde_json::from_str::<Request>(line) {
453            Ok(request) => request,
454            Err(e) => return Handled::Answer(Answer::error(format!("not a request: {e}"))),
455        };
456        let answer = match request {
457            Request::Hello { .. } => Answer::error("the link has said hello already"),
458            Request::Status => Answer::Status(Status::of(app)).to_line(),
459            Request::Frame => frame(app).to_line(),
460            Request::Log { since } => Answer::Log(log(app.log(), since)).to_line(),
461            Request::Perf {
462                since,
463                max,
464                snapshot,
465            } => {
466                let most = u32::try_from(PERF_FRAMES).unwrap_or(u32::MAX);
467                if !(1..=most).contains(&max) {
468                    return Handled::Answer(Answer::error(format!("max: 1 to {most}, not {max}")));
469                }
470                if snapshot {
471                    self.busy = Busy::Snapshot { since, max };
472                    return Handled::Snapshot;
473                }
474                Answer::Perf(perf(app, since, max, None)).to_line()
475            }
476            Request::Screenshot => {
477                self.busy = Busy::Screenshot {
478                    limit: shot_limit(app),
479                };
480                return Handled::Screenshot;
481            }
482            Request::Play { steps } => {
483                if let Err(e) = check_play(&steps) {
484                    return Handled::Answer(Answer::error(e));
485                }
486                // A play of no frames (nothing, or waits of none) is done.
487                let none =
488                    |step: &Step| matches!(step, Step::Wait(s) if script::wait_frames(*s) == 0);
489                if steps.iter().all(none) {
490                    Answer::Played(Played {
491                        frames: 0,
492                        waited_loading: false,
493                        waited_suspended: false,
494                    })
495                    .to_line()
496                } else {
497                    self.busy = Busy::Play(Play {
498                        steps,
499                        frames: None,
500                        run: 0,
501                        waited_loading: false,
502                        waited_suspended: false,
503                    });
504                    return Handled::Later;
505                }
506            }
507        };
508        Handled::Answer(answer)
509    }
510
511    /// This frame's input from the play being played, if one is: the next
512    /// frame of its steps, put in `out` after what is there. While the
513    /// game loads or is suspended the steps wait. A play whose game failed
514    /// to load, or one of whose actions can't be pressed, ends with an
515    /// error, having played nothing more.
516    pub fn input<G: GameData>(&mut self, app: &App<G>, out: &mut Vec<RawInputEvent>) {
517        let Busy::Play(play) = &mut self.busy else {
518            return;
519        };
520        if let Some(errors) = app.errors() {
521            let error = format!("the game didn't load, so the steps can't be played: {errors}");
522            self.end_play(Answer::error(error));
523            return;
524        }
525        if app.is_loading() {
526            play.waited_loading = true;
527            return;
528        }
529        if app.is_suspended() {
530            play.waited_suspended = true;
531            return;
532        }
533        let Some(game) = app.game() else {
534            return;
535        };
536        if play.frames.is_none() {
537            match script::frames(&play.steps, game.input().keymap(), PadKind::default()) {
538                Ok(frames) if frames.is_empty() => {
539                    let played = Played {
540                        frames: 0,
541                        waited_loading: play.waited_loading,
542                        waited_suspended: play.waited_suspended,
543                    };
544                    self.end_play(Answer::Played(played).to_line());
545                    return;
546                }
547                Ok(frames) => play.frames = Some(frames.into()),
548                Err(e) => {
549                    self.end_play(Answer::error(e));
550                    return;
551                }
552            }
553        }
554        let frames = play.frames.get_or_insert_with(VecDeque::new);
555        match frames.front_mut() {
556            Some(Planned::Events(events)) => {
557                out.append(events);
558                frames.pop_front();
559            }
560            Some(Planned::Idle(n)) => {
561                *n = n.saturating_sub(1);
562                if *n == 0 {
563                    frames.pop_front();
564                }
565            }
566            None => {}
567        }
568        play.run = play.run.saturating_add(1);
569    }
570
571    /// Ends the play with `answer`, due after this frame.
572    fn end_play(&mut self, answer: String) {
573        self.busy = Busy::Idle;
574        self.due.push(answer);
575    }
576
577    /// The answers due now that `app`'s frame has run: a play's whose last
578    /// frame it was, then those of the requests that waited behind it, up
579    /// to the next play or picture.
580    pub fn after_frame<G: GameData>(&mut self, app: &App<G>) -> Vec<String> {
581        match &mut self.busy {
582            Busy::Play(play) if play.frames.as_ref().is_some_and(VecDeque::is_empty) => {
583                let answer = Answer::Played(Played {
584                    frames: play.run,
585                    waited_loading: play.waited_loading,
586                    waited_suspended: play.waited_suspended,
587                });
588                self.end_play(answer.to_line());
589            }
590            Busy::Screenshot { limit } => *limit = shot_limit(app),
591            Busy::Idle | Busy::Play(_) | Busy::Snapshot { .. } => {}
592        }
593        while matches!(self.busy, Busy::Idle)
594            && let Some(line) = self.waiting.pop_front()
595        {
596            match self.request(&line, app) {
597                Handled::Answer(answer) => self.due.push(answer),
598                Handled::Later | Handled::Screenshot | Handled::Snapshot | Handled::Close(_) => {}
599            }
600        }
601        core::mem::take(&mut self.due)
602    }
603
604    /// Whether the backend should read the window after this frame and
605    /// answer with [`screenshot`](Self::screenshot).
606    pub fn wants_screenshot(&self) -> bool {
607        matches!(self.busy, Busy::Screenshot { .. })
608    }
609
610    /// The answer to a [`Request::Screenshot`]: `png`, the window's
611    /// picture of `width` × `height` pixels. Requests behind it are
612    /// answered from the next [`after_frame`](Self::after_frame).
613    pub fn screenshot(&mut self, png: &[u8], width: u32, height: u32) -> String {
614        let limit = match core::mem::replace(&mut self.busy, Busy::Idle) {
615            Busy::Screenshot { limit } => limit,
616            other => {
617                self.busy = other;
618                None
619            }
620        };
621        Answer::Screenshot(Screenshot {
622            png: base64::encode(png),
623            width,
624            height,
625            limit,
626        })
627        .to_line()
628    }
629
630    /// Whether the backend should take a heap snapshot after this frame
631    /// and answer with [`snapshot`](Self::snapshot).
632    pub fn wants_snapshot(&self) -> bool {
633        matches!(self.busy, Busy::Snapshot { .. })
634    }
635
636    /// The answer to a [`Request::Perf`] that asked for a snapshot:
637    /// `app`'s records it asked for, and `snapshot`, the backend's
638    /// ([`App::heap_snapshot`]; `None` where it takes none). Requests
639    /// behind it are answered from the next
640    /// [`after_frame`](Self::after_frame).
641    pub fn snapshot<G: GameData>(
642        &mut self,
643        app: &App<G>,
644        snapshot: Option<HeapSnapshot>,
645    ) -> String {
646        let (since, max) = match core::mem::replace(&mut self.busy, Busy::Idle) {
647            Busy::Snapshot { since, max } => (since, max),
648            other => {
649                self.busy = other;
650                (app.perf().next_seq(), PERF_MAX)
651            }
652        };
653        Answer::Perf(perf(app, since, max, snapshot)).to_line()
654    }
655}
656
657/// The frame `app` shows, as text.
658fn frame<G: GameData>(app: &App<G>) -> Answer {
659    match app.game() {
660        Some(game) => Answer::Frame {
661            frame: game.buffer().to_snapshot(&game.ctx().palette),
662            limit: None,
663        },
664        None => Answer::Frame {
665            frame: String::new(),
666            limit: Some(not_running(app)),
667        },
668    }
669}
670
671/// Why `app` shows no game: its loading card, or the errors of its load.
672fn not_running<G: GameData>(app: &App<G>) -> String {
673    match app.errors() {
674        Some(errors) => format!("the build's game didn't load: {errors}"),
675        None => "the build shows its loading card".to_owned(),
676    }
677}
678
679/// What a picture of `app`'s window doesn't show.
680fn shot_limit<G: GameData>(app: &App<G>) -> Option<String> {
681    app.game()
682        .is_none()
683        .then(|| format!("{}: the picture shows that, not the game", not_running(app)))
684}
685
686/// `ring`'s kept lines numbered `since` or more, each with its number.
687fn log(ring: &LogRing, since: u64) -> LogAnswer {
688    let next = ring.next_seq();
689    let kept = u64::try_from(ring.lines().len()).unwrap_or(u64::MAX);
690    let first = next.saturating_sub(kept);
691    let lines = (since.max(first)..)
692        .zip(ring.since(since))
693        .map(|(seq, line)| LogEntry {
694            seq,
695            frame: line.frame,
696            level: line.level.name().to_owned(),
697            category: line.category.name().to_owned(),
698            text: line.text.clone(),
699        })
700        .collect();
701    LogAnswer {
702        lines,
703        next,
704        dropped: first.saturating_sub(since),
705    }
706}
707
708/// `app`'s records numbered `since` or more, the first `max`, with
709/// `snapshot`.
710fn perf<G: GameData>(
711    app: &App<G>,
712    since: u64,
713    max: u32,
714    snapshot: Option<HeapSnapshot>,
715) -> PerfAnswer {
716    let mut answer = perf_read(app.perf(), since, max);
717    answer.frames = app.frame_number();
718    answer.snapshot = snapshot;
719    answer
720}
721
722/// The first `max` records of `ring` numbered `since` or more, as the
723/// contract's `PerfRead::of` reads them: a `since` past the ring's next
724/// number (a build started again) reads nothing, with `next` 0. No frames
725/// and no snapshot.
726fn perf_read(ring: &PerfRing, since: u64, max: u32) -> PerfAnswer {
727    let next_seq = ring.next_seq();
728    let kept = u64::try_from(ring.records().len()).unwrap_or(u64::MAX);
729    let first = next_seq.saturating_sub(kept);
730    let mut answer = PerfAnswer {
731        records: Vec::new(),
732        next: 0,
733        dropped: 0,
734        measured: next_seq > 0,
735        frames: 0,
736        snapshot: None,
737    };
738    if since > next_seq {
739        return answer;
740    }
741    let max = usize::try_from(max).unwrap_or(usize::MAX);
742    answer.records = ring.since(since).take(max).cloned().collect();
743    answer.next = since.max(first) + u64::try_from(answer.records.len()).unwrap_or(u64::MAX);
744    answer.dropped = first.saturating_sub(since);
745    answer
746}
747
748/// Whether a play's `steps` can be played: each step
749/// ([`Step::check`](script::Step::check)), waits of at most [`MAX_WAIT`]
750/// seconds, and no crank or debug step; an error names the step. An action's name is
751/// looked up when the play starts.
752pub fn check_play(steps: &[Step]) -> Result<(), String> {
753    for (i, step) in steps.iter().enumerate() {
754        step.check().map_err(|e| format!("step {}: {e}", i + 1))?;
755        if matches!(step, Step::Crank(_)) {
756            return Err(format!("step {}: crank: not over the live link yet", i + 1));
757        }
758        if matches!(step, Step::Debug(_)) {
759            return Err(format!("step {}: debug: not over the live link yet", i + 1));
760        }
761    }
762    script::check_waits(steps, MAX_WAIT)
763}
764
765#[cfg(test)]
766mod tests;