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#[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#[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#[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#[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#[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#[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}