use crate::{
model::{GameMode, GameMods},
serde::*,
};
use serde::{
de::{Error, MapAccess, Unexpected, Visitor},
Deserialize, Deserializer,
};
use std::{
fmt::{Formatter, Result as FmtResult},
hash::Hash,
};
use time::{OffsetDateTime, PrimitiveDateTime};
#[cfg(feature = "serialize")]
use serde::Serialize;
#[cfg(feature = "serialize")]
use serde_repr::Serialize_repr;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
pub struct Match {
pub match_id: u32,
pub name: String,
#[cfg_attr(feature = "serialize", serde(with = "serde_date"))]
pub start_time: OffsetDateTime,
#[cfg_attr(
feature = "serialize",
serde(with = "serde_maybe_date", skip_serializing_if = "Option::is_none")
)]
pub end_time: Option<OffsetDateTime>,
pub games: Vec<MatchGame>,
}
impl<'de> Deserialize<'de> for Match {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
enum Field {
Match,
Games,
MatchId,
Name,
StartTime,
EndTime,
}
struct MatchVisitor;
impl<'de> Visitor<'de> for MatchVisitor {
type Value = Match;
fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
f.write_str("struct Match")
}
fn visit_map<V>(self, mut map: V) -> Result<Match, V::Error>
where
V: MapAccess<'de>,
{
#[derive(Deserialize)]
struct InnerMatch {
#[serde(deserialize_with = "to_u32")]
pub match_id: u32,
pub name: String,
#[serde(with = "serde_date")]
pub start_time: OffsetDateTime,
#[serde(with = "serde_maybe_date")]
pub end_time: Option<OffsetDateTime>,
}
let mut inner_match: Option<InnerMatch> = None;
let mut games = None;
let mut match_id = None;
let mut name = None;
let mut start_time: Option<&'de str> = None;
let mut end_time: Option<&'de str> = None;
while let Some(key) = map.next_key()? {
match key {
Field::Match => inner_match = Some(map.next_value()?),
Field::Games => games = Some(map.next_value()?),
Field::MatchId => match_id = Some(map.next_value()?),
Field::Name => name = Some(map.next_value()?),
Field::StartTime => start_time = Some(map.next_value()?),
Field::EndTime => end_time = Some(map.next_value()?),
}
}
let games = games.ok_or_else(|| Error::missing_field("games"))?;
let osu_match = match inner_match {
Some(inner_match) => Match {
match_id: inner_match.match_id,
name: inner_match.name,
start_time: inner_match.start_time,
end_time: inner_match.end_time,
games,
},
None => {
let Some(((match_id, name), start_time)) = match_id.zip(name).zip(start_time) else {
return Err(Error::custom(
"Deserializing Match requires either the field `match`, \
or the fields `match_id`, `name`, and `start_time`",
));
};
let start_time =
PrimitiveDateTime::parse(start_time, NAIVE_DATETIME_FORMAT)
.map(PrimitiveDateTime::assume_utc)
.map_err(|_| {
Error::invalid_value(
Unexpected::Str(start_time),
&"date time of the format YYYY-MM-DD HH:MM:SS",
)
})?;
let end_time = end_time
.map(|end_time| {
PrimitiveDateTime::parse(end_time, NAIVE_DATETIME_FORMAT)
.map(PrimitiveDateTime::assume_utc)
.map_err(|_| {
Error::invalid_value(
Unexpected::Str(end_time),
&"date time of the format YYYY-MM-DD HH:MM:SS",
)
})
})
.transpose()?;
Match {
match_id,
name,
start_time,
end_time,
games,
}
}
};
Ok(osu_match)
}
}
const FIELDS: &[&str] = &["match", "games"];
deserializer.deserialize_struct("Match", FIELDS, MatchVisitor)
}
}
#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
pub struct MatchGame {
#[serde(deserialize_with = "to_u32")]
pub game_id: u32,
#[serde(with = "serde_date")]
pub start_time: OffsetDateTime,
#[serde(with = "serde_maybe_date", skip_serializing_if = "Option::is_none")]
pub end_time: Option<OffsetDateTime>,
#[serde(deserialize_with = "to_u32")]
pub beatmap_id: u32,
#[serde(alias = "play_mode")]
pub mode: GameMode,
pub scoring_type: ScoringType,
pub team_type: TeamType,
#[serde(
default,
deserialize_with = "to_maybe_mods",
skip_serializing_if = "Option::is_none"
)]
pub mods: Option<GameMods>,
pub scores: Vec<GameScore>,
}
#[derive(Debug, Clone, Hash, Deserialize, Eq, PartialEq)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
pub struct GameScore {
#[serde(deserialize_with = "to_u32")]
pub slot: u32,
pub team: Team,
#[serde(deserialize_with = "to_u32")]
pub user_id: u32,
#[serde(deserialize_with = "to_u32")]
pub score: u32,
#[serde(alias = "maxcombo", deserialize_with = "to_u32")]
pub max_combo: u32,
#[serde(deserialize_with = "to_u32")]
pub count50: u32,
#[serde(deserialize_with = "to_u32")]
pub count100: u32,
#[serde(deserialize_with = "to_u32")]
pub count300: u32,
#[serde(alias = "countmiss", deserialize_with = "to_u32")]
pub count_miss: u32,
#[serde(alias = "countgeki", deserialize_with = "to_u32")]
pub count_geki: u32,
#[serde(alias = "countkatu", deserialize_with = "to_u32")]
pub count_katu: u32,
#[serde(deserialize_with = "to_bool")]
pub perfect: bool,
#[serde(deserialize_with = "to_bool")]
pub pass: bool,
#[serde(
default,
deserialize_with = "to_maybe_mods",
skip_serializing_if = "Option::is_none"
)]
pub enabled_mods: Option<GameMods>,
}
#[derive(Debug, Clone, Hash, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serialize", derive(Serialize_repr))]
#[repr(u8)]
pub enum ScoringType {
Score = 0,
Accuracy = 1,
Combo = 2,
ScoreV2 = 3,
}
impl From<u8> for ScoringType {
#[inline]
fn from(t: u8) -> Self {
match t {
1 => Self::Accuracy,
2 => Self::Combo,
3 => Self::ScoreV2,
_ => Self::Score,
}
}
}
#[derive(Debug, Clone, Hash, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serialize", derive(Serialize_repr))]
#[repr(u8)]
pub enum TeamType {
HeadToHead = 0,
TagCoop = 1,
TeamVS = 2,
TagTeamVS = 3,
}
impl From<u8> for TeamType {
#[inline]
fn from(t: u8) -> Self {
match t {
1 => Self::TagCoop,
2 => Self::TeamVS,
3 => Self::TagTeamVS,
_ => Self::HeadToHead,
}
}
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
#[cfg_attr(feature = "serialize", derive(Serialize_repr))]
#[repr(u8)]
pub enum Team {
None = 0,
Blue = 1,
Red = 2,
}
impl From<u8> for Team {
#[inline]
fn from(t: u8) -> Self {
match t {
1 => Self::Blue,
2 => Self::Red,
_ => Self::None,
}
}
}