Skip to main content

botstadia_interface/
player.rs

1//! Messages between the site and an arena's player.
2//!
3//! The player runs in a frame on the CDN's domain. The site loads its
4//! `index.html`, waits for [`PlayerMessage::Ready`], sends [`SiteMessage::Init`],
5//! and from then on drives playback while the player reports the turn it's
6//! showing. Messages are JSON strings in `postMessage`, tagged by `type`.
7//! Unknown messages are ignored, so either side can be newer.
8//!
9//! The site draws charts, highlight marks, stats and the result from the
10//! metrics file. The player draws only the stage.
11
12use serde::{Deserialize, Serialize};
13
14/// Bumped when a message changes shape.
15pub const PROTOCOL: u32 = 1;
16
17/// Site → player.
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19#[serde(tag = "type", rename_all = "snake_case")]
20pub enum SiteMessage {
21    Init {
22        protocol: u32,
23        /// Where to fetch the replay archive. `None` opens the stage alone,
24        /// for authoring: authored cameras, no playback.
25        replay_url: Option<String>,
26        /// Seat names, in seat order.
27        seats: Vec<String>,
28        control_rate: u32,
29        /// The last turn, so the transport knows the length before loading.
30        turns: u32,
31    },
32    Play,
33    Pause,
34    /// Show this turn. `seq` is echoed back so a stale report can't move a
35    /// scrubber that's being dragged.
36    Seek {
37        turn: u32,
38        seq: u32,
39    },
40    Speed {
41        multiplier: f64,
42    },
43    View {
44        id: String,
45    },
46}
47
48/// Player → site.
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50#[serde(tag = "type", rename_all = "snake_case")]
51pub enum PlayerMessage {
52    Ready {
53        protocol: u32,
54    },
55    Progress {
56        fraction: f32,
57        label: String,
58    },
59    /// Everything is loaded and turn 0 is showing.
60    Loaded {
61        turns: u32,
62        views: Vec<View>,
63        view: String,
64    },
65    /// The turn on screen, for the site's playhead.
66    Turn {
67        turn: u32,
68        seq: Option<u32>,
69    },
70    Playing {
71        playing: bool,
72        speed: f64,
73    },
74    Finished,
75    /// A failure, worded to show as is.
76    Error {
77        message: String,
78    },
79}
80
81/// A camera viewers can pick.
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct View {
84    pub id: String,
85    pub label: String,
86}
87
88macro_rules! json {
89    ($type:ty) => {
90        impl $type {
91            pub fn to_json(&self) -> String {
92                serde_json::to_string(self).expect("messages always serialise")
93            }
94
95            /// `None` for anything that isn't a message of this kind, which is
96            /// ignored.
97            pub fn from_json(text: &str) -> Option<Self> {
98                serde_json::from_str(text).ok()
99            }
100        }
101    };
102}
103
104json!(SiteMessage);
105json!(PlayerMessage);
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn tagged_json_round_trips_and_strangers_are_ignored() {
113        let init = SiteMessage::Init {
114            protocol: PROTOCOL,
115            replay_url: None,
116            seats: vec!["a".into()],
117            control_rate: 50,
118            turns: 10,
119        };
120        assert!(init.to_json().contains(r#""type":"init""#));
121        assert_eq!(SiteMessage::from_json(&init.to_json()), Some(init));
122        let turn = PlayerMessage::Turn {
123            turn: 3,
124            seq: Some(1),
125        };
126        assert_eq!(PlayerMessage::from_json(&turn.to_json()), Some(turn));
127        assert_eq!(PlayerMessage::from_json(r#"{"type":"future_thing"}"#), None);
128    }
129}