mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 00:38:54 +00:00
345 lines
14 KiB
Rust
345 lines
14 KiB
Rust
use rosu_v2::model::{
|
|
self as rosu,
|
|
mods::{GameModIntermode, GameModsIntermode},
|
|
};
|
|
|
|
use super::*;
|
|
|
|
impl ApprovalStatus {
|
|
pub(crate) fn from_rosu(
|
|
rank_status: rosu_v2::model::beatmap::RankStatus,
|
|
ranked_date: Option<DateTime<Utc>>,
|
|
) -> Self {
|
|
use ApprovalStatus::*;
|
|
match rank_status {
|
|
rosu_v2::model::beatmap::RankStatus::Graveyard => Graveyarded,
|
|
rosu_v2::model::beatmap::RankStatus::WIP => WIP,
|
|
rosu_v2::model::beatmap::RankStatus::Pending => Pending,
|
|
rosu_v2::model::beatmap::RankStatus::Ranked => Ranked(ranked_date.unwrap()),
|
|
rosu_v2::model::beatmap::RankStatus::Approved => Approved,
|
|
rosu_v2::model::beatmap::RankStatus::Qualified => Qualified,
|
|
rosu_v2::model::beatmap::RankStatus::Loved => Loved,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn time_to_utc(s: time::OffsetDateTime) -> DateTime<Utc> {
|
|
chrono::DateTime::from_timestamp(s.unix_timestamp(), 0).unwrap()
|
|
}
|
|
|
|
impl Beatmap {
|
|
pub(crate) fn from_rosu(
|
|
bm: rosu::beatmap::BeatmapExtended,
|
|
set: &rosu::beatmap::BeatmapsetExtended,
|
|
) -> Self {
|
|
let last_updated = time_to_utc(bm.last_updated);
|
|
let difficulty = Difficulty::from_rosu(&bm);
|
|
Self {
|
|
approval: ApprovalStatus::from_rosu(bm.status, set.ranked_date.map(time_to_utc)),
|
|
submit_date: set.submitted_date.map(time_to_utc).unwrap_or(last_updated),
|
|
last_update: last_updated,
|
|
download_available: !set.availability.download_disabled, // don't think we have this stat
|
|
audio_available: !set.availability.download_disabled, // neither is this
|
|
artist: set.artist_unicode.as_ref().unwrap_or(&set.artist).clone(),
|
|
title: set.title_unicode.as_ref().unwrap_or(&set.title).clone(),
|
|
beatmapset_id: set.mapset_id as u64,
|
|
creator: set.creator_name.clone().into_string(),
|
|
creator_id: set.creator_id as u64,
|
|
source: Some(set.source.clone()).filter(|s| s != "").clone(),
|
|
genre: set.genre.map(|v| v.into()).unwrap_or(Genre::Unspecified),
|
|
language: set.language.map(|v| v.into()).unwrap_or(Language::Any),
|
|
tags: set.tags.split(", ").map(|v| v.to_owned()).collect(),
|
|
beatmap_id: bm.map_id as u64,
|
|
difficulty_name: bm.version,
|
|
difficulty,
|
|
file_hash: bm.checksum.unwrap_or_else(|| "none".to_owned()),
|
|
mode: bm.mode.into(),
|
|
favourite_count: set.favourite_count as u64,
|
|
rating: set
|
|
.ratings
|
|
.as_ref()
|
|
.map(|rs| {
|
|
(rs.iter()
|
|
.enumerate()
|
|
.map(|(r, id)| ((r + 1) as u32 * *id))
|
|
.sum::<u32>()) as f64
|
|
/ (rs.iter().sum::<u32>() as f64)
|
|
})
|
|
.unwrap_or(0.0),
|
|
play_count: bm.playcount as u64,
|
|
pass_count: bm.passcount as u64,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl User {
|
|
pub(crate) fn from_rosu(
|
|
user: rosu::user::UserExtended,
|
|
stats: rosu::user::UserStatistics,
|
|
events: Vec<rosu::recent_event::RecentEvent>,
|
|
) -> Self {
|
|
Self {
|
|
id: user.user_id as u64,
|
|
username: user.username.into_string(),
|
|
joined: time_to_utc(user.join_date),
|
|
country: user.country_code.to_string(),
|
|
count_300: 0, // why do we even want this
|
|
count_100: 0, // why do we even want this
|
|
count_50: 0, // why do we even want this
|
|
play_count: stats.playcount as u64,
|
|
played_time: Duration::from_secs(stats.playtime as u64),
|
|
ranked_score: stats.ranked_score,
|
|
total_score: stats.total_score,
|
|
count_ss: stats.grade_counts.ss as u64,
|
|
count_ssh: stats.grade_counts.ssh as u64,
|
|
count_s: stats.grade_counts.s as u64,
|
|
count_sh: stats.grade_counts.sh as u64,
|
|
count_a: stats.grade_counts.a as u64,
|
|
events: events.into_iter().map(UserEvent::from).collect(),
|
|
rank: stats.global_rank.unwrap_or(0) as u64,
|
|
country_rank: stats.country_rank.unwrap_or(0) as u64,
|
|
level: stats.level.current as f64 + stats.level.progress as f64 / 100.0,
|
|
pp: Some(stats.pp as f64),
|
|
accuracy: stats.accuracy as f64,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<rosu::recent_event::RecentEvent> for UserEvent {
|
|
fn from(value: rosu::recent_event::RecentEvent) -> Self {
|
|
match value.event_type {
|
|
rosu::recent_event::EventType::Rank {
|
|
grade: _,
|
|
rank,
|
|
mode,
|
|
beatmap,
|
|
user: _,
|
|
} => Self::Rank(UserEventRank {
|
|
beatmap_id: {
|
|
beatmap
|
|
.url
|
|
.trim_start_matches("/b/")
|
|
.trim_end_matches("?m=0")
|
|
.trim_end_matches("?m=1")
|
|
.trim_end_matches("?m=2")
|
|
.trim_end_matches("?m=3")
|
|
.parse::<u64>()
|
|
.unwrap()
|
|
},
|
|
rank: rank as u16,
|
|
mode: mode.into(),
|
|
date: time_to_utc(value.created_at),
|
|
}),
|
|
_ => Self::OtherV2(value),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<rosu::score::Score> for Score {
|
|
fn from(s: rosu::score::Score) -> Self {
|
|
let legacy_stats = s.statistics.as_legacy(s.mode);
|
|
Self {
|
|
id: Some(s.id),
|
|
user_id: s.user_id as u64,
|
|
date: time_to_utc(s.ended_at),
|
|
replay_available: s.replay,
|
|
beatmap_id: s.map_id as u64,
|
|
score: s.score as u64,
|
|
pp: s.pp.map(|v| v as f64),
|
|
rank: s.grade.into(),
|
|
mods: s
|
|
.mods
|
|
.iter()
|
|
.map(|v| v.intermode())
|
|
.collect::<GameModsIntermode>()
|
|
.into(),
|
|
count_300: legacy_stats.count_300 as u64,
|
|
count_100: legacy_stats.count_100 as u64,
|
|
count_50: legacy_stats.count_50 as u64,
|
|
count_miss: legacy_stats.count_miss as u64,
|
|
count_katu: legacy_stats.count_katu as u64,
|
|
count_geki: legacy_stats.count_geki as u64,
|
|
max_combo: s.max_combo as u64,
|
|
perfect: s.is_perfect_combo,
|
|
lazer_build_id: s.build_id,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Difficulty {
|
|
pub(crate) fn from_rosu(bm: &rosu::beatmap::BeatmapExtended) -> Self {
|
|
Self {
|
|
stars: bm.stars as f64,
|
|
aim: None,
|
|
speed: None,
|
|
cs: bm.cs as f64,
|
|
od: bm.od as f64,
|
|
ar: bm.ar as f64,
|
|
hp: bm.hp as f64,
|
|
count_normal: bm.count_circles as u64,
|
|
count_slider: bm.count_sliders as u64,
|
|
count_spinner: bm.count_sliders as u64,
|
|
max_combo: bm.max_combo.map(|v| v as u64),
|
|
bpm: bm.bpm as f64,
|
|
drain_length: Duration::from_secs(bm.seconds_drain as u64),
|
|
total_length: Duration::from_secs(bm.seconds_total as u64),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<rosu::GameMode> for Mode {
|
|
fn from(value: rosu::GameMode) -> Self {
|
|
match value {
|
|
rosu::GameMode::Osu => Mode::Std,
|
|
rosu::GameMode::Taiko => Mode::Taiko,
|
|
rosu::GameMode::Catch => Mode::Catch,
|
|
rosu::GameMode::Mania => Mode::Mania,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<Mode> for rosu::GameMode {
|
|
fn from(value: Mode) -> Self {
|
|
match value {
|
|
Mode::Std => rosu::GameMode::Osu,
|
|
Mode::Taiko => rosu::GameMode::Taiko,
|
|
Mode::Catch => rosu::GameMode::Catch,
|
|
Mode::Mania => rosu::GameMode::Mania,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<rosu::beatmap::Genre> for Genre {
|
|
fn from(value: rosu::beatmap::Genre) -> Self {
|
|
match value {
|
|
rosu::beatmap::Genre::Any => Genre::Any,
|
|
rosu::beatmap::Genre::Unspecified => Genre::Unspecified,
|
|
rosu::beatmap::Genre::VideoGame => Genre::VideoGame,
|
|
rosu::beatmap::Genre::Anime => Genre::Anime,
|
|
rosu::beatmap::Genre::Rock => Genre::Rock,
|
|
rosu::beatmap::Genre::Pop => Genre::Pop,
|
|
rosu::beatmap::Genre::Other => Genre::Other,
|
|
rosu::beatmap::Genre::Novelty => Genre::Novelty,
|
|
rosu::beatmap::Genre::HipHop => Genre::HipHop,
|
|
rosu::beatmap::Genre::Electronic => Genre::Electronic,
|
|
rosu::beatmap::Genre::Metal => Genre::Metal,
|
|
rosu::beatmap::Genre::Classical => Genre::Classical,
|
|
rosu::beatmap::Genre::Folk => Genre::Folk,
|
|
rosu::beatmap::Genre::Jazz => Genre::Jazz,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<rosu::beatmap::Language> for Language {
|
|
fn from(value: rosu::beatmap::Language) -> Self {
|
|
match value {
|
|
rosu::beatmap::Language::Any => Language::Any,
|
|
rosu::beatmap::Language::Other => Language::Other,
|
|
rosu::beatmap::Language::English => Language::English,
|
|
rosu::beatmap::Language::Japanese => Language::Japanese,
|
|
rosu::beatmap::Language::Chinese => Language::Chinese,
|
|
rosu::beatmap::Language::Instrumental => Language::Instrumental,
|
|
rosu::beatmap::Language::Korean => Language::Korean,
|
|
rosu::beatmap::Language::French => Language::French,
|
|
rosu::beatmap::Language::German => Language::German,
|
|
rosu::beatmap::Language::Swedish => Language::Swedish,
|
|
rosu::beatmap::Language::Spanish => Language::Spanish,
|
|
rosu::beatmap::Language::Italian => Language::Italian,
|
|
rosu::beatmap::Language::Russian => Language::Russian,
|
|
rosu::beatmap::Language::Polish => Language::Polish,
|
|
rosu::beatmap::Language::Unspecified => Language::Unspecified,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<rosu::Grade> for Rank {
|
|
fn from(value: rosu::Grade) -> Self {
|
|
match value {
|
|
rosu::Grade::F => Rank::F,
|
|
rosu::Grade::D => Rank::D,
|
|
rosu::Grade::C => Rank::C,
|
|
rosu::Grade::B => Rank::B,
|
|
rosu::Grade::A => Rank::A,
|
|
rosu::Grade::S => Rank::S,
|
|
rosu::Grade::SH => Rank::SH,
|
|
rosu::Grade::X => Rank::SS,
|
|
rosu::Grade::XH => Rank::SSH,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<Mods> for rosu::mods::GameModsIntermode {
|
|
fn from(value: Mods) -> Self {
|
|
let mut res = GameModsIntermode::new();
|
|
const MOD_MAP: &[(Mods, GameModIntermode)] = &[
|
|
(Mods::NF, GameModIntermode::NoFail),
|
|
(Mods::EZ, GameModIntermode::Easy),
|
|
(Mods::TD, GameModIntermode::TouchDevice),
|
|
(Mods::HD, GameModIntermode::Hidden),
|
|
(Mods::HR, GameModIntermode::HardRock),
|
|
(Mods::SD, GameModIntermode::SuddenDeath),
|
|
(Mods::DT, GameModIntermode::DoubleTime),
|
|
(Mods::RX, GameModIntermode::Relax),
|
|
(Mods::HT, GameModIntermode::HalfTime),
|
|
(Mods::NC, GameModIntermode::Nightcore),
|
|
(Mods::FL, GameModIntermode::Flashlight),
|
|
(Mods::AT, GameModIntermode::Autoplay),
|
|
(Mods::SO, GameModIntermode::SpunOut),
|
|
(Mods::AP, GameModIntermode::Autopilot),
|
|
(Mods::PF, GameModIntermode::Perfect),
|
|
(Mods::KEY1, GameModIntermode::OneKey),
|
|
(Mods::KEY2, GameModIntermode::TwoKeys),
|
|
(Mods::KEY3, GameModIntermode::ThreeKeys),
|
|
(Mods::KEY4, GameModIntermode::FourKeys),
|
|
(Mods::KEY5, GameModIntermode::FiveKeys),
|
|
(Mods::KEY6, GameModIntermode::SixKeys),
|
|
(Mods::KEY7, GameModIntermode::SevenKeys),
|
|
(Mods::KEY8, GameModIntermode::EightKeys),
|
|
(Mods::KEY9, GameModIntermode::NineKeys),
|
|
];
|
|
for (m1, m2) in MOD_MAP {
|
|
if value.contains(*m1) {
|
|
res.insert(*m2);
|
|
}
|
|
}
|
|
res
|
|
}
|
|
}
|
|
|
|
impl From<rosu::mods::GameModsIntermode> for Mods {
|
|
fn from(value: rosu_v2::prelude::GameModsIntermode) -> Self {
|
|
value
|
|
.into_iter()
|
|
.map(|m| match m {
|
|
GameModIntermode::NoFail => Mods::NF,
|
|
GameModIntermode::Easy => Mods::EZ,
|
|
GameModIntermode::TouchDevice => Mods::TD,
|
|
GameModIntermode::Hidden => Mods::HD,
|
|
GameModIntermode::HardRock => Mods::HR,
|
|
GameModIntermode::SuddenDeath => Mods::SD,
|
|
GameModIntermode::DoubleTime => Mods::DT,
|
|
GameModIntermode::Relax => Mods::RX,
|
|
GameModIntermode::HalfTime => Mods::HT,
|
|
GameModIntermode::Nightcore => Mods::NC,
|
|
GameModIntermode::Flashlight => Mods::FL,
|
|
GameModIntermode::Autoplay => Mods::AT,
|
|
GameModIntermode::SpunOut => Mods::SO,
|
|
GameModIntermode::Autopilot => Mods::AP,
|
|
GameModIntermode::Perfect => Mods::PF,
|
|
GameModIntermode::OneKey => Mods::KEY1,
|
|
GameModIntermode::TwoKeys => Mods::KEY2,
|
|
GameModIntermode::ThreeKeys => Mods::KEY3,
|
|
GameModIntermode::FourKeys => Mods::KEY4,
|
|
GameModIntermode::FiveKeys => Mods::KEY5,
|
|
GameModIntermode::SixKeys => Mods::KEY6,
|
|
GameModIntermode::SevenKeys => Mods::KEY7,
|
|
GameModIntermode::EightKeys => Mods::KEY8,
|
|
GameModIntermode::NineKeys => Mods::KEY9,
|
|
GameModIntermode::Classic => Mods::CLASSIC,
|
|
_ => Mods::UNKNOWN,
|
|
})
|
|
.fold(Mods::NOMOD, |a, b| a | b)
|
|
|
|
// Mods::from_bits_truncate(value.bits() as u64)
|
|
}
|
|
}
|