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