Skip to main content

mlb_api/requests/game/
mod.rs

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