Skip to main content

rosu/model/
match.rs

1use crate::{
2    model::{GameMode, GameMods},
3    serde::*,
4};
5
6use serde::{
7    de::{Error, MapAccess, Unexpected, Visitor},
8    Deserialize, Deserializer,
9};
10use std::{
11    fmt::{Formatter, Result as FmtResult},
12    hash::Hash,
13};
14use time::{OffsetDateTime, PrimitiveDateTime};
15
16#[cfg(feature = "serialize")]
17use serde::Serialize;
18#[cfg(feature = "serialize")]
19use serde_repr::Serialize_repr;
20
21/// Match struct retrieved from the `/api/get_match` endpoint.
22#[derive(Debug, Clone, Eq, PartialEq, Hash)]
23#[cfg_attr(feature = "serialize", derive(Serialize))]
24pub struct Match {
25    pub match_id: u32,
26    pub name: String,
27    #[cfg_attr(feature = "serialize", serde(with = "serde_date"))]
28    pub start_time: OffsetDateTime,
29    #[cfg_attr(
30        feature = "serialize",
31        serde(with = "serde_maybe_date", skip_serializing_if = "Option::is_none")
32    )]
33    pub end_time: Option<OffsetDateTime>,
34    pub games: Vec<MatchGame>,
35}
36
37impl<'de> Deserialize<'de> for Match {
38    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
39    where
40        D: Deserializer<'de>,
41    {
42        #[derive(Deserialize)]
43        #[serde(field_identifier, rename_all = "snake_case")]
44        enum Field {
45            Match,
46            Games,
47            MatchId,
48            Name,
49            StartTime,
50            EndTime,
51        }
52
53        struct MatchVisitor;
54
55        impl<'de> Visitor<'de> for MatchVisitor {
56            type Value = Match;
57
58            fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
59                f.write_str("struct Match")
60            }
61
62            fn visit_map<V>(self, mut map: V) -> Result<Match, V::Error>
63            where
64                V: MapAccess<'de>,
65            {
66                #[derive(Deserialize)]
67                struct InnerMatch {
68                    #[serde(deserialize_with = "to_u32")]
69                    pub match_id: u32,
70                    pub name: String,
71                    #[serde(with = "serde_date")]
72                    pub start_time: OffsetDateTime,
73                    #[serde(with = "serde_maybe_date")]
74                    pub end_time: Option<OffsetDateTime>,
75                }
76
77                let mut inner_match: Option<InnerMatch> = None;
78                let mut games = None;
79                let mut match_id = None;
80                let mut name = None;
81                let mut start_time: Option<&'de str> = None;
82                let mut end_time: Option<&'de str> = None;
83
84                while let Some(key) = map.next_key()? {
85                    match key {
86                        Field::Match => inner_match = Some(map.next_value()?),
87                        Field::Games => games = Some(map.next_value()?),
88                        Field::MatchId => match_id = Some(map.next_value()?),
89                        Field::Name => name = Some(map.next_value()?),
90                        Field::StartTime => start_time = Some(map.next_value()?),
91                        Field::EndTime => end_time = Some(map.next_value()?),
92                    }
93                }
94
95                let games = games.ok_or_else(|| Error::missing_field("games"))?;
96
97                let osu_match = match inner_match {
98                    Some(inner_match) => Match {
99                        match_id: inner_match.match_id,
100                        name: inner_match.name,
101                        start_time: inner_match.start_time,
102                        end_time: inner_match.end_time,
103                        games,
104                    },
105                    None => {
106                        let Some(((match_id, name), start_time)) = match_id.zip(name).zip(start_time) else {
107                            return Err(Error::custom(
108                                "Deserializing Match requires either the field `match`, \
109                                or the fields `match_id`, `name`, and `start_time`",
110                            ));
111                        };
112
113                        let start_time =
114                            PrimitiveDateTime::parse(start_time, NAIVE_DATETIME_FORMAT)
115                                .map(PrimitiveDateTime::assume_utc)
116                                .map_err(|_| {
117                                    Error::invalid_value(
118                                        Unexpected::Str(start_time),
119                                        &"date time of the format YYYY-MM-DD HH:MM:SS",
120                                    )
121                                })?;
122
123                        let end_time = end_time
124                            .map(|end_time| {
125                                PrimitiveDateTime::parse(end_time, NAIVE_DATETIME_FORMAT)
126                                    .map(PrimitiveDateTime::assume_utc)
127                                    .map_err(|_| {
128                                        Error::invalid_value(
129                                            Unexpected::Str(end_time),
130                                            &"date time of the format YYYY-MM-DD HH:MM:SS",
131                                        )
132                                    })
133                            })
134                            .transpose()?;
135                        Match {
136                            match_id,
137                            name,
138                            start_time,
139                            end_time,
140                            games,
141                        }
142                    }
143                };
144
145                Ok(osu_match)
146            }
147        }
148
149        const FIELDS: &[&str] = &["match", "games"];
150
151        deserializer.deserialize_struct("Match", FIELDS, MatchVisitor)
152    }
153}
154
155/// Each map that was not aborted during a [`Match`] will
156/// produce a [`MatchGame`] which contains the data of
157/// the game and all its scores
158#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Hash)]
159#[cfg_attr(feature = "serialize", derive(Serialize))]
160pub struct MatchGame {
161    #[serde(deserialize_with = "to_u32")]
162    pub game_id: u32,
163    #[serde(with = "serde_date")]
164    pub start_time: OffsetDateTime,
165    #[serde(with = "serde_maybe_date", skip_serializing_if = "Option::is_none")]
166    pub end_time: Option<OffsetDateTime>,
167    #[serde(deserialize_with = "to_u32")]
168    pub beatmap_id: u32,
169    #[serde(alias = "play_mode")]
170    pub mode: GameMode,
171    pub scoring_type: ScoringType,
172    pub team_type: TeamType,
173    #[serde(
174        default,
175        deserialize_with = "to_maybe_mods",
176        skip_serializing_if = "Option::is_none"
177    )]
178    pub mods: Option<GameMods>,
179    pub scores: Vec<GameScore>,
180}
181
182/// Each participating user of a [`MatchGame`] will produce a [`GameScore`]
183/// which contains the data about the user's play
184#[derive(Debug, Clone, Hash, Deserialize, Eq, PartialEq)]
185#[cfg_attr(feature = "serialize", derive(Serialize))]
186pub struct GameScore {
187    #[serde(deserialize_with = "to_u32")]
188    pub slot: u32,
189    pub team: Team,
190    #[serde(deserialize_with = "to_u32")]
191    pub user_id: u32,
192    #[serde(deserialize_with = "to_u32")]
193    pub score: u32,
194    #[serde(alias = "maxcombo", deserialize_with = "to_u32")]
195    pub max_combo: u32,
196    #[serde(deserialize_with = "to_u32")]
197    pub count50: u32,
198    #[serde(deserialize_with = "to_u32")]
199    pub count100: u32,
200    #[serde(deserialize_with = "to_u32")]
201    pub count300: u32,
202    #[serde(alias = "countmiss", deserialize_with = "to_u32")]
203    pub count_miss: u32,
204    #[serde(alias = "countgeki", deserialize_with = "to_u32")]
205    pub count_geki: u32,
206    #[serde(alias = "countkatu", deserialize_with = "to_u32")]
207    pub count_katu: u32,
208    #[serde(deserialize_with = "to_bool")]
209    pub perfect: bool,
210    #[serde(deserialize_with = "to_bool")]
211    pub pass: bool,
212    #[serde(
213        default,
214        deserialize_with = "to_maybe_mods",
215        skip_serializing_if = "Option::is_none"
216    )]
217    pub enabled_mods: Option<GameMods>,
218}
219
220/// Basic enum to describe the scoring type of a [`Match`]
221/// i.e. the winning condition
222#[derive(Debug, Clone, Hash, Copy, Eq, PartialEq)]
223#[cfg_attr(feature = "serialize", derive(Serialize_repr))]
224#[repr(u8)]
225pub enum ScoringType {
226    Score = 0,
227    Accuracy = 1,
228    Combo = 2,
229    ScoreV2 = 3,
230}
231
232impl From<u8> for ScoringType {
233    #[inline]
234    fn from(t: u8) -> Self {
235        match t {
236            1 => Self::Accuracy,
237            2 => Self::Combo,
238            3 => Self::ScoreV2,
239            _ => Self::Score,
240        }
241    }
242}
243
244/// Basic enum to describe the team type of a [`Match`]
245#[derive(Debug, Clone, Hash, Copy, Eq, PartialEq)]
246#[cfg_attr(feature = "serialize", derive(Serialize_repr))]
247#[repr(u8)]
248pub enum TeamType {
249    HeadToHead = 0,
250    TagCoop = 1,
251    TeamVS = 2,
252    TagTeamVS = 3,
253}
254
255impl From<u8> for TeamType {
256    #[inline]
257    fn from(t: u8) -> Self {
258        match t {
259            1 => Self::TagCoop,
260            2 => Self::TeamVS,
261            3 => Self::TagTeamVS,
262            _ => Self::HeadToHead,
263        }
264    }
265}
266
267/// Basic enum to declare a team of a [`Match`]
268#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
269#[cfg_attr(feature = "serialize", derive(Serialize_repr))]
270#[repr(u8)]
271pub enum Team {
272    None = 0,
273    Blue = 1,
274    Red = 2,
275}
276
277impl From<u8> for Team {
278    #[inline]
279    fn from(t: u8) -> Self {
280        match t {
281            1 => Self::Blue,
282            2 => Self::Red,
283            _ => Self::None,
284        }
285    }
286}