Skip to main content

mlb_api/requests/game/
mod.rs

1//! The thing you're most likely here for.
2//!
3//! This module itself acts like [`crate::types`] but for misc game-specific types as there are many.
4
5#![allow(unused_imports, reason = "usage of children modules")]
6
7use std::fmt::{Display, Formatter};
8use std::marker::PhantomData;
9use std::ops::{ControlFlow, Sub};
10use std::time::{Duration, Instant};
11use bon::Builder;
12use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, Utc};
13use derive_more::{Deref, DerefMut, Display, From, Not};
14use fxhash::FxHashMap;
15use serde::{Deserialize, Deserializer};
16use serde::de::{DeserializeOwned, Error, IgnoredAny, MapAccess, Visitor};
17use serde_with::{serde_as, DisplayFromStr};
18use crate::person::{Ballplayer, JerseyNumber, NamedPerson, PersonId};
19use crate::meta::{DayNight, NamedPosition};
20use crate::request::RequestURLBuilderExt;
21use crate::team::TeamId;
22use crate::team::roster::RosterStatus;
23use crate::{DayHalf, HomeAway, ResourceUsage, ResultHoldingResourceUsage, TeamSide};
24use crate::meta::WindDirectionId;
25use crate::request;
26
27mod boxscore; // done
28mod changes;
29mod content;
30mod context_metrics;
31mod diff;
32mod linescore; // done
33mod pace; // done
34mod plays; // done
35mod timestamps; // done
36mod uniforms;
37mod win_probability;
38mod live_feed; // done
39
40pub use boxscore::*;
41pub use changes::*;
42pub use content::*;
43pub use context_metrics::*;
44pub use diff::*;
45pub use linescore::*;
46pub use pace::*;
47pub use plays::*;
48pub use timestamps::*;
49pub use uniforms::*;
50pub use win_probability::*;
51pub use live_feed::*;
52
53id!(#[doc = "A [`u32`] representing a baseball game. [Sport](crate::sport)-independent"] GameId { gamePk: u32 });
54
55/// Date & Time of the game. 
56#[derive(Debug, Deserialize, PartialEq, Clone)]
57#[serde(rename_all = "camelCase")]
58#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
59pub struct GameDateTime {
60	/// The date and time of the game. Note that the time is typically rounded to the hour and the :07, :05 on the hour is for the first pitch, which is a different timestamp.
61	#[serde(rename = "dateTime", deserialize_with = "crate::deserialize_datetime")]
62	pub datetime: DateTime<Utc>,
63	/// The original planned date of the game
64	pub original_date: NaiveDate,
65	/// The currently set "date" of the game.
66	pub official_date: NaiveDate,
67	/// Data regarding the game's paused resumption
68	#[serde(flatten, default)]
69	pub resumed: Option<GameResumedDateTime>,
70	
71	/// Day or night
72	#[serde(rename = "dayNight")]
73	pub sky: DayNight,
74	/// The local 12-hour time of the game
75	pub time: NaiveTime,
76	/// AM or PM; use [`DayHalf::into_24_hour_time`] to convert to 24-hour local time.
77	pub ampm: DayHalf,
78}
79
80/// Optional resumed data regarding a paused game
81#[derive(Debug, Deserialize, PartialEq, Clone)]
82#[serde(rename_all = "camelCase")]
83#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
84pub struct GameResumedDateTime {
85	#[serde(rename = "resumeDateTime", deserialize_with = "crate::deserialize_datetime")]
86	pub resumed_datetime: DateTime<Utc>,
87	#[serde(rename = "resumedFromDateTime", deserialize_with = "crate::deserialize_datetime")]
88	pub resumed_from_datetime: DateTime<Utc>,
89
90	#[serde(rename = "resumeDate", default)]
91	pub __resume_date: IgnoredAny,
92	#[serde(rename = "resumeFromDate", default)]
93	pub __resume_from_date: IgnoredAny,
94}
95
96/// General weather conditions, temperature, wind, etc.
97#[derive(Debug, Deserialize, PartialEq, Clone, Default)]
98#[serde(try_from = "__WeatherConditionsStruct")]
99pub struct WeatherConditions {
100	pub condition: Option<String>,
101	pub temp: Option<uom::si::f64::ThermodynamicTemperature>,
102	pub wind: Option<(uom::si::f64::Velocity, WindDirectionId)>,
103}
104
105#[derive(Deserialize)]
106#[doc(hidden)]
107#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
108struct __WeatherConditionsStruct {
109	condition: Option<String>,
110	temp: Option<String>,
111	wind: Option<String>,
112}
113
114impl TryFrom<__WeatherConditionsStruct> for WeatherConditions {
115	type Error = &'static str;
116
117	fn try_from(value: __WeatherConditionsStruct) -> Result<Self, Self::Error> {
118		Ok(Self {
119			condition: value.condition,
120			temp: value.temp.and_then(|temp| temp.parse::<i32>().ok()).map(|temp| uom::si::f64::ThermodynamicTemperature::new::<uom::si::thermodynamic_temperature::degree_fahrenheit>(temp as f64)),
121			wind: if let Some(wind) = value.wind {
122				let (speed, direction) = wind.split_once(" mph, ").ok_or("invalid wind format")?;
123				let speed = speed.parse::<i32>().map_err(|_| "invalid wind speed")?;
124				Some((uom::si::f64::Velocity::new::<uom::si::velocity::mile_per_hour>(speed as f64), WindDirectionId::new(direction)))
125			} else { None },
126		})
127	}
128}
129
130/// Misc
131#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
132#[serde(rename_all = "camelCase")]
133#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
134pub struct GameInfo {
135	pub attendance: Option<u32>,
136	#[serde(deserialize_with = "crate::try_deserialize_datetime", default)]
137	pub first_pitch: Option<DateTime<Utc>>,
138	/// Measured in minutes,
139	#[serde(rename = "gameDurationMinutes")]
140	pub game_duration: Option<u32>,
141	/// Durationg of the game delay; measured in minutes.
142	#[serde(rename = "delayDurationMinutes")]
143	pub delay_duration: Option<u32>,
144}
145
146/// Review usage for each team and if the game supports challenges.
147#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
148#[serde(rename_all = "camelCase")]
149#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
150pub struct TeamReviewData {
151	pub has_challenges: bool,
152	#[serde(flatten)]
153	pub teams: HomeAway<ResourceUsage>,
154}
155
156#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
157#[serde(rename_all = "camelCase")]
158#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
159pub struct TeamChallengeData {
160	pub has_challenges: bool,
161	#[serde(flatten)]
162	pub teams: HomeAway<ResultHoldingResourceUsage>,
163}
164
165/// Tags about a game, such as a perfect game in progress, no-hitter, etc.
166#[allow(clippy::struct_excessive_bools, reason = "no")]
167#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
168#[serde(rename_all = "camelCase")]
169#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
170pub struct GameTags {
171	pub no_hitter: bool,
172	pub perfect_game: bool,
173
174	pub away_team_no_hitter: bool,
175	pub away_team_perfect_game: bool,
176
177	pub home_team_no_hitter: bool,
178	pub home_team_perfect_game: bool,
179}
180
181/// Double-header information.
182#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
183pub enum DoubleHeaderKind {
184	#[serde(rename = "N")]
185	/// Not a doubleheader
186	Not,
187
188	#[serde(rename = "Y")]
189	/// First game in a double-header
190	FirstGame,
191
192	#[serde(rename = "S")]
193	/// Second game in a double-header.
194	SecondGame,
195}
196
197impl DoubleHeaderKind {
198	#[must_use]
199	pub const fn is_double_header(self) -> bool {
200		matches!(self, Self::FirstGame | Self::SecondGame)
201	}
202}
203
204#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq, Deref, DerefMut, From)]
205pub struct Inning(usize);
206
207impl Inning {
208	/// Starting inning of a game
209	#[must_use]
210	pub const fn starting() -> Self {
211		Self(1)
212	}
213}
214
215impl Display for Inning {
216	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
217		crate::write_nth(self.0, f)
218	}
219}
220
221/// Half of the inning.
222#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq, Not)]
223pub enum InningHalf {
224	#[serde(rename = "Top", alias = "top")]
225	Top,
226	#[serde(rename = "Bottom", alias = "bottom")]
227	Bottom,
228}
229
230impl InningHalf {
231	/// Starting inning half of a game
232	#[must_use]
233	pub const fn starting() -> Self {
234		Self::Top
235	}
236
237	pub(crate) fn deserialize_from_is_top_inning<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
238		Ok(match bool::deserialize(deserializer)? {
239			true => Self::Top,
240			false => Self::Bottom,
241		})
242	}
243}
244
245impl InningHalf {
246	/// A unicode character representing an up or down arrow.
247	#[must_use]
248	pub const fn unicode_char_filled(self) -> char {
249		match self {
250			Self::Top => '▲',
251			Self::Bottom => '▼',
252		}
253	}
254	
255	/// A hollow character representing the inning half
256	#[must_use]
257	pub const fn unicode_char_empty(self) -> char {
258		match self {
259			Self::Top => '△',
260			Self::Bottom => '▽',
261		}
262	}
263
264	/// Three ascii characters representing the inning half
265	#[must_use]
266	pub const fn three_char(self) -> &'static str {
267		match self {
268			Self::Top => "Top",
269			Self::Bottom => "Bot",
270		}
271	}
272
273	/// The team that bats in this inning half
274	#[must_use]
275	pub const fn bats(self) -> TeamSide {
276		match self {
277			Self::Top => TeamSide::Away,
278			Self::Bottom => TeamSide::Home,
279		}
280	}
281
282	/// The team that pitches in this inning half
283	#[must_use]
284	pub const fn pitches(self) -> TeamSide {
285		match self {
286			Self::Top => TeamSide::Home,
287			Self::Bottom => TeamSide::Away,
288		}
289	}
290}
291
292/// The balls and strikes in a given at bat. Along with the number of outs (this technically can change during the AB due to pickoffs etc)
293#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display, Default)]
294#[display("{balls}-{strikes} ({outs} out)")]
295pub struct AtBatCount {
296	#[serde(default)]
297	pub balls: u8,
298	#[serde(default)]
299	pub strikes: u8,
300	#[serde(default)]
301	pub outs: u8,
302}
303
304#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
305pub struct SituationCount {
306	pub balls: u8,
307	pub strikes: u8,
308	pub outs: u8,
309	pub inning: Inning,
310	#[serde(rename = "isTopInning", deserialize_with = "InningHalf::deserialize_from_is_top_inning")]
311	pub inning_half: InningHalf,
312	#[serde(rename = "runnerOn1b")]
313	pub runner_on_first: bool,
314	#[serde(rename = "runnerOn2b")]
315	pub runner_on_second: bool,
316	#[serde(rename = "runnerOn3b")]
317	pub runner_on_third: bool,
318}
319
320/// The classic "R | H | E" and LOB in a scoreboard.
321#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
322#[serde(from = "__RHEStruct")]
323#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
324pub struct RHE {
325	pub runs: usize,
326	pub hits: usize,
327	pub errors: usize,
328	pub left_on_base: usize,
329	/// Ex: Home team wins and doesn't need to play Bot 9.
330	pub was_inning_half_played: bool,
331}
332
333#[doc(hidden)]
334#[derive(Deserialize)]
335#[serde(rename_all = "camelCase")]
336struct __RHEStruct {
337	pub runs: Option<usize>,
338	#[serde(default)]
339    pub hits: usize,
340    #[serde(default)]
341    pub errors: usize,
342    #[serde(default)]
343    pub left_on_base: usize,
344
345    // only sometimes present, regardless of whether a game is won
346    #[doc(hidden)]
347    #[serde(rename = "isWinner", default)]
348    pub __is_winner: IgnoredAny,
349}
350
351impl From<__RHEStruct> for RHE {
352	fn from(__RHEStruct { runs, hits, errors, left_on_base, .. }: __RHEStruct) -> Self {
353		Self {
354			runs: runs.unwrap_or(0),
355			hits,
356			errors,
357			left_on_base,
358			was_inning_half_played: runs.is_some(),
359		}
360	}
361}
362
363/// Unparsed miscellaneous data.
364///
365/// Some of these values might be handwritten per game so parsing them would prove rather difficult.
366/// 
367/// ## Examples
368/// | Name          | Value     |
369/// |---------------|-----------|
370/// | First pitch   | 8:10 PM.  |
371/// | Weather       | 68 degrees, Roof Closed |
372/// | Att           | 44,713.   |
373#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
374#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
375pub struct LabelledValue {
376	pub label: String,
377	#[serde(default)]
378	pub value: String,
379}
380
381#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
382#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
383pub struct SectionedLabelledValues {
384	#[serde(rename = "title")]
385	pub section: String,
386	#[serde(rename = "fieldList")]
387	pub values: Vec<LabelledValue>,
388}
389
390/// Various flags about the player in the current game
391#[allow(clippy::struct_excessive_bools, reason = "not what's happening here")]
392#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
393#[serde(rename_all = "camelCase")]
394#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
395pub struct PlayerGameStatusFlags {
396	pub is_current_batter: bool,
397	pub is_current_pitcher: bool,
398	pub is_on_bench: bool,
399	pub is_substitute: bool,
400}
401
402#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
403#[serde(rename_all = "camelCase")]
404#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
405pub struct Official {
406	pub official: NamedPerson,
407	pub official_type: OfficialType,
408}
409
410#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
411pub enum OfficialType {
412	#[serde(rename = "Home Plate")]
413	HomePlate,
414	#[serde(rename = "First Base")]
415	FirstBase,
416	#[serde(rename = "Second Base")]
417	SecondBase,
418	#[serde(rename = "Third Base")]
419	ThirdBase,
420	#[serde(rename = "Left Field")]
421	LeftField,
422	#[serde(rename = "Right Field")]
423	RightField,
424}
425
426/// A position in the batting order, 1st, 2nd, 3rd, 4th, etc.
427///
428/// Note that this number is split in two, the general batting order position is the `major` while if there is a lineup movement then the player would have an increased `minor` since they replace an existing batting order position.
429///
430/// Example:
431/// Alice bats 1st (major = 1, minor = 0)
432/// Bob pinch hits and bats 1st for Alice (major = 1, minor = 1)
433/// Alice somehow hits again (major = 1, minor = 0)
434/// Charlie pinch runs and takes over from then on (major = 1, minor = 2)
435///
436/// Note: These minors are [`Display`]ed incremented one more than is done internally, so (major = 1, minor = 1) displays as `1st (2)`.
437#[derive(Debug, PartialEq, Eq, Copy, Clone)]
438pub struct BattingOrderIndex {
439	pub major: usize,
440	pub minor: usize,
441}
442
443impl<'de> Deserialize<'de> for BattingOrderIndex {
444	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
445	where
446	    D: Deserializer<'de>
447	{
448		let v: usize = String::deserialize(deserializer)?.parse().map_err(D::Error::custom)?;
449		Ok(Self {
450			major: v / 100,
451			minor: v % 100,
452		})
453	}
454}
455
456impl Display for BattingOrderIndex {
457	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
458		crate::write_nth(self.major, f)?;
459		if self.minor > 0 {
460			write!(f, " ({})", self.minor + 1)?;
461		}
462		Ok(())
463	}
464}
465
466/// Decisions of winner & loser (and potentially the save)
467#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
468#[serde(rename_all = "camelCase")]
469#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
470pub struct Decisions {
471	pub winner: Option<NamedPerson>,
472	pub loser: Option<NamedPerson>,
473	pub save: Option<NamedPerson>,
474}
475
476/// Game records in stats like exit velocity, hit distance, etc.
477///
478/// Currently unable to actually get data for these though
479#[derive(Debug, Deserialize, PartialEq, Clone)]
480#[serde(rename_all = "camelCase")]
481#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
482pub struct GameStatLeaders {
483	#[doc(hidden)]
484	#[serde(rename = "hitDistance", default)]
485	pub __distance: IgnoredAny,
486	#[doc(hidden)]
487	#[serde(rename = "hitSpeed", default)]
488	pub __exit_velocity: IgnoredAny,
489	#[doc(hidden)]
490	#[serde(rename = "pitchSpeed", default)]
491	pub __velocity: IgnoredAny,
492}
493
494#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Display)]
495pub enum Base {
496	#[display("1B")]
497	First,
498	#[display("2B")]
499	Second,
500	#[display("3B")]
501	Third,
502	#[display("HP")]
503	Home,
504}
505
506impl<'de> Deserialize<'de> for Base {
507	#[allow(clippy::too_many_lines, reason = "Visitor impl takes up the bulk, is properly scoped")]
508	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
509	where
510		D: Deserializer<'de>
511	{
512		struct BaseVisitor;
513
514		impl Visitor<'_> for BaseVisitor {
515			type Value = Base;
516
517			fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
518				write!(f, "a string or integer representing the base")
519			}
520
521			fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
522			where
523				E: Error,
524			{
525				Ok(match v {
526					"1B" | "1" => Base::First,
527					"2B" | "2" => Base::Second,
528					"3B" | "3" => Base::Third,
529					"score" | "HP" | "4B" | "4" => Base::Home,
530					_ => return Err(E::unknown_variant(v, &["1B", "1", "2B" , "2", "3B", "3", "score", "HP", "4B", "4"]))
531				})
532			}
533
534			fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
535			where
536				E: Error,
537			{
538				Ok(match v {
539					1 => Base::First,
540					2 => Base::Second,
541					3 => Base::Third,
542					4 => Base::Home,
543					_ => return Err(E::unknown_variant("[a number]", &["1", "2", "3", "4"]))
544				})
545			}
546		}
547
548		deserializer.deserialize_any(BaseVisitor)
549	}
550}
551
552#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
553pub enum ContactHardness {
554	#[serde(rename = "soft")]
555	Soft,
556	#[serde(rename = "medium")]
557	Medium,
558	#[serde(rename = "hard")]
559	Hard,
560}
561
562pub(crate) fn deserialize_players_cache<'de, T: DeserializeOwned, D: Deserializer<'de>>(deserializer: D) -> Result<FxHashMap<PersonId, T>, D::Error> {
563	struct PlayersCacheVisitor<T2: DeserializeOwned>(PhantomData<T2>);
564
565	impl<'de2, T2: DeserializeOwned> serde::de::Visitor<'de2> for PlayersCacheVisitor<T2> {
566		type Value = FxHashMap<PersonId, T2>;
567
568		fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
569			formatter.write_str("a map")
570		}
571
572		fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
573		where
574			A: MapAccess<'de2>,
575		{
576			let mut values = FxHashMap::default();
577
578			while let Some((key, value)) = map.next_entry()? {
579				let key: String = key;
580				let key = PersonId::new(key.strip_prefix("ID").ok_or_else(|| A::Error::custom("invalid id format"))?.parse::<u32>().map_err(A::Error::custom)?);
581				values.insert(key, value);
582			}
583
584			Ok(values)
585		}
586	}
587
588	deserializer.deserialize_map(PlayersCacheVisitor::<T>(PhantomData))
589}
590
591/// Meant for active & live games, gives a streamable version of the plays in a game.
592///
593/// The [`PlayStream`] is meant to be for consistently polling the MLB API for live play-by-play updates.
594/// The list of events can be seen on [`PlayStreamEvent`]
595///
596/// Note that the [`Linescore`] and [`Boxscore`] are not timed to the current state of the game as of the event, to achieve that it is recommended the timecode argument to live feed and iterate through the timecodes on its endpoint; however is much slower, or using diffPatch.
597/// 
598/// ## Examples
599/// ```no_run
600/// PlayStream::new(/* game id */).run(|event: PlayStreamEvent, meta: &LiveFeedMetadata, data: &LiveFeedData, linescore: &Linescore, boxscore: &Boxscore| {
601///     match event {
602///         PlayStreamEvent::Start => println!("Start"),
603///         PlayStreamEvent::StartPlay(play) => println!("{} vs. {}", play.matchup.batter.full_name, play.matchup.pitcher.full_name),
604///         PlayStreamEvent::PlayEvent(play_event, _play) => {
605///             match play_event {
606///                 PlayEvent::Action { details, .. } => println!("{}", details.description),
607///                 PlayEvent::Pitch { details, common, .. } => println!("{} -> {}", details.call, common.count),
608///                 PlayEvent::Stepoff { .. } => println!("Stepoff"),
609///                 PlayEvent::NoPitch { .. } => println!("No Pitch"),
610///                 PlayEvent::Pickoff { .. } => println!("Pickoff"),
611///             }
612///         },
613///         PlayStreamEvent::PlayEventReviewStart(review, _play_event, _play) => println!("PlayEventReviewStart; {}", review.review_type),
614///         PlayStreamEvent::PlayEventReviewEnd(review, _play_event, _play) => println!("PlayEventReviewEnd; {}", review.review_type),
615///         PlayStreamEvent::PlayReviewStart(review, _play) => println!("PlayReviewStart; {}", review.review_type),
616///         PlayStreamEvent::PlayReviewEnd(review, _play) => println!("PlayReviewEnd; {}", review.review_type),
617///         PlayStreamEvent::EndPlay(play) => println!("{}", play.result.completed_play_details.as_ref().expect("Completed play").description),
618///         PlayStreamEvent::GameEnd(_, _) => println!("GameEnd"),
619///     }
620/// }).await?;
621/// ```
622#[derive(Debug)]
623pub struct PlayStream {
624	game_id: GameId,
625
626	current_play_idx: usize,
627	in_progress_current_play: bool,
628	current_play_review_idx: usize,
629	in_progress_current_play_review: bool,
630	
631	current_play_event_idx: usize,
632	current_play_event_review_idx: usize,
633	in_progress_current_play_event_review: bool,
634}
635
636impl PlayStream {
637	#[must_use]
638	pub fn new(game_id: impl Into<GameId>) -> Self {
639		Self {
640			game_id: game_id.into(),
641			
642			current_play_idx: 0,
643			in_progress_current_play: false,
644			current_play_review_idx: 0,
645			in_progress_current_play_review: false,
646			
647			current_play_event_idx: 0,
648			current_play_event_review_idx: 0,
649			in_progress_current_play_event_review: false,
650		}
651	}
652}
653
654/// An event in a game, such as the game starting, ending, a [`Play`] (At-Bat) starting, or a [`PlayEvent`] occuring, or a challenge on a play or play event.
655#[derive(Debug, PartialEq, Clone)]
656pub enum PlayStreamEvent<'a> {
657	/// Sent at the beginning of a stream
658	Start,
659	
660	StartPlay(&'a Play),
661	PlayReviewStart(&'a ReviewData, &'a Play),
662	PlayReviewEnd(&'a ReviewData, &'a Play),
663	EndPlay(&'a Play),
664	
665	PlayEvent(&'a PlayEvent, &'a Play),
666	PlayEventReviewStart(&'a ReviewData, &'a PlayEvent, &'a Play),
667	PlayEventReviewEnd(&'a ReviewData, &'a PlayEvent, &'a Play),
668	
669	GameEnd(&'a Decisions, &'a GameStatLeaders),
670}
671
672impl PlayStream {
673	/// Runs through plays until the game is over.
674	///
675	/// # Errors
676	/// See [`request::Error`]
677	pub async fn run<F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, request::Error>>(self, f: F) -> Result<(), request::Error> {
678		self.run_with_custom_error::<request::Error, F>(f).await
679	}
680
681	/// Evaluation for the current play
682	async fn run_current_play<E, F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, E>>(&self, mut f: F, current_play: &Play, meta: &LiveFeedMetadata, data: &LiveFeedData, linescore: &Linescore, boxscore: &Boxscore) -> Result<ControlFlow<()>, E> {
683		macro_rules! flow_try {
684			($($t:tt)*) => {
685				match ($($t)*).await? {
686					ControlFlow::Continue(()) => {},
687					ControlFlow::Break(()) => return Ok(ControlFlow::Break(())),
688				}
689			};
690		}
691		
692		if !self.in_progress_current_play {
693			flow_try!(f(PlayStreamEvent::StartPlay(current_play), meta, data, linescore, boxscore));
694		}
695		let mut play_events = current_play.play_events.iter().skip(self.current_play_event_idx);
696		if let Some(current_play_event) = play_events.next() {
697			flow_try!(f(PlayStreamEvent::PlayEvent(current_play_event, current_play), meta, data, linescore, boxscore));
698			let mut reviews = current_play_event.reviews.iter().skip(self.current_play_event_review_idx);
699			if let Some(current_review) = reviews.next() {
700				if !self.in_progress_current_play_event_review {
701					flow_try!(f(PlayStreamEvent::PlayEventReviewStart(current_review, current_play_event, current_play), meta, data, linescore, boxscore));
702				}
703				if !current_review.is_in_progress {
704					flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(current_review, current_play_event, current_play), meta, data, linescore, boxscore));
705				}
706			}
707			for review in reviews {
708				flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, current_play_event, current_play), meta, data, linescore, boxscore));
709				if !review.is_in_progress {
710					flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, current_play_event, current_play), meta, data, linescore, boxscore));
711				}
712			}
713		}
714		for play_event in play_events {
715			flow_try!(f(PlayStreamEvent::PlayEvent(play_event, current_play), meta, data, linescore, boxscore));
716			for review in &play_event.reviews {
717				flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, play_event, current_play), meta, data, linescore, boxscore));
718				if !review.is_in_progress {
719					flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, play_event, current_play), meta, data, linescore, boxscore));
720				}
721			}
722		}
723		let mut reviews = current_play.reviews.iter().skip(self.current_play_review_idx);
724		if let Some(current_review) = reviews.next() {
725			if !self.in_progress_current_play_review {
726				flow_try!(f(PlayStreamEvent::PlayReviewStart(current_review, current_play), meta, data, linescore, boxscore));
727			}
728			if !current_review.is_in_progress {
729				flow_try!(f(PlayStreamEvent::PlayReviewEnd(current_review, current_play), meta, data, linescore, boxscore));
730			}
731		}
732		
733		for review in reviews {
734			flow_try!(f(PlayStreamEvent::PlayReviewStart(review, current_play), meta, data, linescore, boxscore));
735			if !review.is_in_progress {
736				flow_try!(f(PlayStreamEvent::PlayReviewEnd(review, current_play), meta, data, linescore, boxscore));
737			}
738		}
739		if current_play.about.is_complete {
740			flow_try!(f(PlayStreamEvent::EndPlay(current_play), meta, data, linescore, boxscore));
741		}
742		
743		Ok(ControlFlow::Continue(()))
744	}
745
746	/// Evaluation for remaining plays
747	async fn run_next_plays<E, F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, E>>(&self, mut f: F, plays: impl Iterator<Item=&Play>, meta: &LiveFeedMetadata, data: &LiveFeedData, linescore: &Linescore, boxscore: &Boxscore) -> Result<ControlFlow<()>, E> {
748		macro_rules! flow_try {
749			($($t:tt)*) => {
750				match ($($t)*).await? {
751					ControlFlow::Continue(()) => {},
752					ControlFlow::Break(()) => return Ok(ControlFlow::Break(())),
753				}
754			};
755		}
756		
757		for play in plays {
758			flow_try!(f(PlayStreamEvent::StartPlay(play), meta, data, linescore, boxscore));
759			for play_event in &play.play_events {
760				flow_try!(f(PlayStreamEvent::PlayEvent(play_event, play), meta, data, linescore, boxscore));
761				for review in &play_event.reviews {
762					flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, play_event, play), meta, data, linescore, boxscore));
763					if !review.is_in_progress {
764						flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, play_event, play), meta, data, linescore, boxscore));
765					}
766				}
767			}
768			for review in &play.reviews {
769				flow_try!(f(PlayStreamEvent::PlayReviewStart(review, play), meta, data, linescore, boxscore));
770				if !review.is_in_progress {
771					flow_try!(f(PlayStreamEvent::PlayReviewEnd(review, play), meta, data, linescore, boxscore));
772				}
773			}
774			if play.about.is_complete {
775				flow_try!(f(PlayStreamEvent::EndPlay(play), meta, data, linescore, boxscore));
776			}
777		}
778
779		Ok(ControlFlow::Continue(()))
780	}
781
782	fn update_indices(&mut self, plays: &[Play]) {
783		let latest_play = plays.last();
784
785		self.in_progress_current_play = latest_play.is_some_and(|play| !play.about.is_complete);
786		self.current_play_idx = if self.in_progress_current_play { plays.len() - 1 } else { plays.len() };
787
788		let current_play = plays.get(self.current_play_idx);
789		let current_play_event = current_play.and_then(|play| play.play_events.last());
790		let current_play_review = current_play.and_then(|play| play.reviews.last());
791		let current_play_event_review = current_play_event.and_then(|play_event| play_event.reviews.last());
792		
793		self.in_progress_current_play_review = current_play_review.is_some_and(|review| review.is_in_progress);
794		self.current_play_review_idx = current_play.map_or(0, |play| if self.in_progress_current_play_review { play.reviews.len() - 1 } else { play.reviews.len() });
795
796		self.current_play_event_idx = current_play.map_or(0, |play| play.play_events.len());
797
798		self.in_progress_current_play_event_review = current_play_event_review.is_some_and(|review| review.is_in_progress);
799		self.current_play_event_review_idx = current_play_event.map_or(0, |play_event| if self.in_progress_current_play_event_review { play_event.reviews.len() - 1 } else { play_event.reviews.len() });
800	}
801
802	/// Variant of the ``run`` function that allows for custom error types.
803	///
804	/// # Errors
805	/// See [`request::Error`]
806	pub async fn run_with_custom_error<E: From<request::Error>, F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, E>>(self, f: F) -> Result<(), E> {
807		let feed = LiveFeedRequest::builder().id(self.game_id).build_and_get().await?;
808		Self::with_presupplied_feed(feed, f).await
809	}
810
811	/// Variant of the ``run`` function that begins with a pre-supplied [`LiveFeedResponse`], useful if preprocessing was done before the play stream.
812	///
813	/// # Errors
814	/// See [`request::Error`]
815	pub async fn with_presupplied_feed<E: From<request::Error>, F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, E>>(mut feed: LiveFeedResponse, mut f: F) -> Result<(), E> {
816		macro_rules! flow_try {
817			($($t:tt)*) => {
818				match ($($t)*).await? {
819					ControlFlow::Continue(()) => {},
820					ControlFlow::Break(()) => return Ok(()),
821				}
822			};
823		}
824
825		let mut this = Self::new(feed.id);
826		flow_try!(f(PlayStreamEvent::Start, &feed.meta, &feed.data, &feed.live.linescore, &feed.live.boxscore));
827		
828		loop {
829		    let since_last_request = Instant::now();
830		    
831			let LiveFeedResponse { meta, data, live, .. } = &feed;
832			let LiveFeedLiveData { linescore, boxscore, decisions, leaders, plays } = live;
833			let mut plays = plays.iter().skip(this.current_play_idx);
834
835			if let Some(current_play) = plays.next() {
836				flow_try!(this.run_current_play(&mut f, current_play, meta, data, linescore, boxscore));
837			}
838			
839			flow_try!(this.run_next_plays(&mut f, plays, meta, data, linescore, boxscore));
840			
841			if data.status.abstract_game_code.is_finished() && let Some(decisions) = decisions {
842				let _ = f(PlayStreamEvent::GameEnd(decisions, leaders), meta, data, linescore, boxscore).await?;
843				return Ok(())
844			}
845
846			this.update_indices(&live.plays);
847
848			let total_sleep_time = Duration::from_secs(meta.recommended_poll_rate as _);
849			drop(feed);
850			tokio::time::sleep(total_sleep_time.saturating_sub(since_last_request.elapsed())).await;
851		    feed = LiveFeedRequest::builder().id(this.game_id).build_and_get().await?;
852		}
853	}
854}
855	
856#[cfg(test)]
857mod tests {
858    use std::ops::ControlFlow;
859    use crate::{cache::RequestableEntrypoint, game::{PlayEvent, PlayStream, PlayStreamEvent}};
860
861	#[tokio::test]
862	async fn test_play_stream() {
863		Box::pin(PlayStream::new(822_834).run(async |event, _meta, _data, _linescore, _boxscore| {
864			match event {
865				PlayStreamEvent::Start => println!("GameStart"),
866				PlayStreamEvent::StartPlay(play) => println!("PlayStart; {} vs. {}", play.matchup.batter.full_name, play.matchup.pitcher.full_name),
867				PlayStreamEvent::PlayEvent(play_event, _play) => {
868					print!("PlayEvent; ");
869					match play_event {
870						PlayEvent::Action { details, .. } => println!("{}", details.description),
871						PlayEvent::Pitch { details, common, .. } => println!("{} -> {}", details.call, common.count),
872						PlayEvent::Stepoff { .. } => println!("Stepoff"),
873						PlayEvent::NoPitch { .. } => println!("No Pitch"),
874						PlayEvent::Pickoff { .. } => println!("Pickoff"),
875					}
876				},
877				PlayStreamEvent::PlayEventReviewStart(review, _, _) => println!("PlayEventReviewStart; {}", review.review_type),
878				PlayStreamEvent::PlayEventReviewEnd(review, _, _) => println!("PlayEventReviewEnd; {}", review.review_type),
879				PlayStreamEvent::PlayReviewStart(review, _) => println!("PlayReviewStart; {}", review.review_type),
880				PlayStreamEvent::PlayReviewEnd(review, _) => println!("PlayReviewEnd; {}", review.review_type),
881				PlayStreamEvent::EndPlay(play) => println!("PlayEnd; {}", play.result.completed_play_details.as_ref().expect("Completed play").description),
882				PlayStreamEvent::GameEnd(_, _) => println!("GameEnd"),
883			}
884			Ok(ControlFlow::Continue(()))
885		})).await.unwrap();
886	}
887}