Implement score fetching

This commit is contained in:
Natsu Kagami 2024-02-13 23:13:20 +01:00 committed by Natsu Kagami
parent f5cc201f1c
commit 84b13dcef3
8 changed files with 192 additions and 58 deletions

31
Cargo.lock generated
View file

@ -899,20 +899,6 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper-rustls"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c"
dependencies = [
"http",
"hyper",
"rustls 0.20.9",
"rustls-native-certs",
"tokio",
"tokio-rustls 0.23.4",
]
[[package]] [[package]]
name = "hyper-rustls" name = "hyper-rustls"
version = "0.24.2" version = "0.24.2"
@ -923,6 +909,7 @@ dependencies = [
"http", "http",
"hyper", "hyper",
"rustls 0.21.10", "rustls 0.21.10",
"rustls-native-certs",
"tokio", "tokio",
"tokio-rustls 0.24.1", "tokio-rustls 0.24.1",
] ]
@ -1600,7 +1587,7 @@ dependencies = [
"http", "http",
"http-body", "http-body",
"hyper", "hyper",
"hyper-rustls 0.24.2", "hyper-rustls",
"hyper-tls", "hyper-tls",
"ipnet", "ipnet",
"js-sys", "js-sys",
@ -1670,20 +1657,22 @@ checksum = "be9e281b71d3797817a1e6615dd8fb081dd61359b4c41d08792cc7c3c1c13b4e"
[[package]] [[package]]
name = "rosu-v2" name = "rosu-v2"
version = "0.8.0" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/natsukagami/rosu-v2?rev=6f6731cb2f0d235b006ab375dd94b446dde894ac#6f6731cb2f0d235b006ab375dd94b446dde894ac"
checksum = "ef969d8cb87f8dab58193bd722c7831fc839c9852c6e2aec64da3d2e87d447f3"
dependencies = [ dependencies = [
"bitflags 1.3.2",
"bytes", "bytes",
"dashmap", "dashmap",
"futures", "futures",
"hyper", "hyper",
"hyper-rustls 0.23.2", "hyper-rustls",
"itoa",
"leaky-bucket-lite", "leaky-bucket-lite",
"log", "log",
"paste",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded",
"smallstr", "smallstr",
"thiserror",
"time", "time",
"tokio", "tokio",
"url", "url",
@ -2010,9 +1999,9 @@ dependencies = [
[[package]] [[package]]
name = "smallstr" name = "smallstr"
version = "0.2.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e922794d168678729ffc7e07182721a14219c65814e66e91b839a272fe5ae4f" checksum = "63b1aefdf380735ff8ded0b15f31aab05daf1f70216c01c02a12926badd1df9d"
dependencies = [ dependencies = [
"serde", "serde",
"smallvec", "smallvec",

View file

@ -16,7 +16,7 @@ osuparse = { git = "https://github.com/eltrufas/osuparse", rev = "ad8f6e5e7771e7
regex = "1.5.6" regex = "1.5.6"
reqwest = "0.11.10" reqwest = "0.11.10"
rosu-pp = "0.9.1" rosu-pp = "0.9.1"
rosu-v2 = "0.8" rosu-v2 = { git = "https://github.com/natsukagami/rosu-v2", rev = "6f6731cb2f0d235b006ab375dd94b446dde894ac" }
time = "0.3" time = "0.3"
serde = { version = "1.0.137", features = ["derive"] } serde = { version = "1.0.137", features = ["derive"] }
serenity = "0.11.2" serenity = "0.11.2"

View file

@ -90,14 +90,7 @@ impl Client {
) -> Result<Vec<Score>, Error> { ) -> Result<Vec<Score>, Error> {
let mut r = ScoreRequestBuilder::new(beatmap_id); let mut r = ScoreRequestBuilder::new(beatmap_id);
f(&mut r); f(&mut r);
let res: Vec<raw::Score> = r.build(self).await?.json().await?; r.build(self).await
let mut res: Vec<Score> = vec_try_into(res)?;
// with a scores request you need to fill the beatmap ids yourself
res.iter_mut().for_each(|v| {
v.beatmap_id = beatmap_id;
});
Ok(res)
} }
pub async fn user_best( pub async fn user_best(

View file

@ -558,6 +558,8 @@ pub struct Score {
pub count_geki: u64, pub count_geki: u64,
pub max_combo: u64, pub max_combo: u64,
pub perfect: bool, pub perfect: bool,
pub lazer_build_id: Option<u32>,
} }
impl Score { impl Score {

View file

@ -40,6 +40,10 @@ bitflags::bitflags! {
const NOVIDEO = Self::TD.bits; /* never forget */ const NOVIDEO = Self::TD.bits; /* never forget */
const SPEED_CHANGING = Self::DT.bits | Self::HT.bits | Self::NC.bits; const SPEED_CHANGING = Self::DT.bits | Self::HT.bits | Self::NC.bits;
const MAP_CHANGING = Self::HR.bits | Self::EZ.bits | Self::SPEED_CHANGING.bits; const MAP_CHANGING = Self::HR.bits | Self::EZ.bits | Self::SPEED_CHANGING.bits;
// Made up flags
const CLASSIC = 1 << 59;
const UNKNOWN = 1 << 60;
} }
} }
@ -68,6 +72,8 @@ const MODS_WITH_NAMES: &[(Mods, &str)] = &[
(Mods::KEY7, "7K"), (Mods::KEY7, "7K"),
(Mods::KEY8, "8K"), (Mods::KEY8, "8K"),
(Mods::KEY9, "9K"), (Mods::KEY9, "9K"),
(Mods::CLASSIC, "CL"),
(Mods::UNKNOWN, "??"),
]; ];
impl std::str::FromStr for Mods { impl std::str::FromStr for Mods {
@ -106,6 +112,8 @@ impl std::str::FromStr for Mods {
"7K" => res |= Mods::KEY7, "7K" => res |= Mods::KEY7,
"8K" => res |= Mods::KEY8, "8K" => res |= Mods::KEY8,
"9K" => res |= Mods::KEY9, "9K" => res |= Mods::KEY9,
"CL" => res |= Mods::CLASSIC,
"??" => res |= Mods::UNKNOWN,
v => return Err(format!("{} is not a valid mod", v)), v => return Err(format!("{} is not a valid mod", v)),
} }
} }

View file

@ -69,6 +69,8 @@ impl TryFrom<raw::Score> for Score {
count_geki: parse_from_str(&raw.countgeki)?, count_geki: parse_from_str(&raw.countgeki)?,
max_combo: parse_from_str(&raw.maxcombo)?, max_combo: parse_from_str(&raw.maxcombo)?,
perfect: parse_bool(&raw.perfect)?, perfect: parse_bool(&raw.perfect)?,
lazer_build_id: None,
}) })
} }
} }

View file

@ -1,4 +1,7 @@
use rosu_v2::model as rosu; use rosu_v2::model::{
self as rosu,
mods::{GameModIntermode, GameModsIntermode},
};
use super::*; use super::*;
@ -25,7 +28,10 @@ fn time_to_utc(s: time::OffsetDateTime) -> DateTime<Utc> {
} }
impl Beatmap { impl Beatmap {
pub(crate) fn from_rosu(bm: rosu::beatmap::Beatmap, set: &rosu::beatmap::Beatmapset) -> Self { pub(crate) fn from_rosu(
bm: rosu::beatmap::BeatmapExtended,
set: &rosu::beatmap::BeatmapsetExtended,
) -> Self {
let last_updated = time_to_utc(bm.last_updated); let last_updated = time_to_utc(bm.last_updated);
let difficulty = Difficulty::from_rosu(&bm); let difficulty = Difficulty::from_rosu(&bm);
Self { Self {
@ -68,7 +74,7 @@ impl Beatmap {
impl User { impl User {
pub(crate) fn from_rosu( pub(crate) fn from_rosu(
user: rosu::user::User, user: rosu::user::UserExtended,
stats: rosu::user::UserStatistics, stats: rosu::user::UserStatistics,
events: Vec<rosu::recent_event::RecentEvent>, events: Vec<rosu::recent_event::RecentEvent>,
) -> Self { ) -> Self {
@ -129,8 +135,39 @@ impl From<rosu::recent_event::RecentEvent> for UserEvent {
} }
} }
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 { impl Difficulty {
pub(crate) fn from_rosu(bm: &rosu::beatmap::Beatmap) -> Self { pub(crate) fn from_rosu(bm: &rosu::beatmap::BeatmapExtended) -> Self {
Self { Self {
stars: bm.stars as f64, stars: bm.stars as f64,
aim: None, aim: None,
@ -214,3 +251,95 @@ impl From<rosu::beatmap::Language> for Language {
} }
} }
} }
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)
}
}

View file

@ -110,6 +110,7 @@ fn handle_not_found<T>(v: Result<T, OsuError>) -> Result<Option<T>, OsuError> {
pub mod builders { pub mod builders {
use reqwest::Response; use reqwest::Response;
use rosu_v2::model::mods::GameModsIntermode;
use crate::models; use crate::models;
@ -213,19 +214,6 @@ pub mod builders {
events.retain(|e| (now <= e.created_at)); events.retain(|e| (now <= e.created_at));
let stats = user.statistics.take().unwrap(); let stats = user.statistics.take().unwrap();
Ok(Some(models::User::from_rosu(user, stats, events))) Ok(Some(models::User::from_rosu(user, stats, events)))
// Ok(client
// .build_request("https://osu.ppy.sh/api/get_user")
// .await?
// .query(&self.user.to_query())
// .query(&self.mode.to_query())
// .query(
// &self
// .event_days
// .map(|v| ("event_days", v.to_string()))
// .to_query(),
// )
// .send()
// .await?)
} }
} }
@ -268,17 +256,40 @@ pub mod builders {
self self
} }
pub(crate) async fn build(&self, client: &Client) -> Result<Response> { pub(crate) async fn build(self, client: &Client) -> Result<Vec<models::Score>> {
Ok(client let scores = handle_not_found(match self.user {
.build_request("https://osu.ppy.sh/api/get_scores") Some(user) => {
.await? let mut r = client
.query(&[("b", self.beatmap_id)]) .rosu
.query(&self.user.to_query()) .beatmap_user_scores(self.beatmap_id as u32, user);
.query(&self.mode.to_query()) if let Some(mode) = self.mode {
.query(&self.mods.to_query()) r = r.mode(mode.into());
.query(&self.limit.map(|v| ("limit", v.to_string())).to_query()) }
.send() match self.mods {
.await?) Some(mods) => r.await.map(|mut ss| {
let mods = GameModsIntermode::from(mods);
ss.retain(|s| mods.iter().all(|m| s.mods.contains_intermode(m)));
ss
}),
None => r.await,
}
}
None => {
let mut r = client.rosu.beatmap_scores(self.beatmap_id as u32).global();
if let Some(mode) = self.mode {
r = r.mode(mode.into());
}
if let Some(mods) = self.mods {
r = r.mods(GameModsIntermode::from(mods));
}
if let Some(limit) = self.limit {
r = r.limit(limit as u32);
}
r.await
}
})?
.unwrap_or(vec![]);
Ok(scores.into_iter().map(|v| v.into()).collect())
} }
} }