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