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